disable save all when any section is invalid

This commit is contained in:
Josh Hawkins 2026-02-27 08:55:55 -06:00
parent 3b5f84c601
commit 49f4bc48b6
3 changed files with 33 additions and 7 deletions

View File

@ -121,6 +121,7 @@ export interface BaseSectionProps {
onStatusChange?: (status: { onStatusChange?: (status: {
hasChanges: boolean; hasChanges: boolean;
isOverridden: boolean; isOverridden: boolean;
hasValidationErrors: boolean;
}) => void; }) => void;
/** Pending form data keyed by "sectionKey" or "cameraName::sectionKey" */ /** Pending form data keyed by "sectionKey" or "cameraName::sectionKey" */
pendingDataBySection?: Record<string, unknown>; pendingDataBySection?: Record<string, unknown>;
@ -371,8 +372,8 @@ export function ConfigSection({
}, [formData, pendingData, extraHasChanges]); }, [formData, pendingData, extraHasChanges]);
useEffect(() => { useEffect(() => {
onStatusChange?.({ hasChanges, isOverridden }); onStatusChange?.({ hasChanges, isOverridden, hasValidationErrors });
}, [hasChanges, isOverridden, onStatusChange]); }, [hasChanges, isOverridden, hasValidationErrors, onStatusChange]);
// Handle form data change // Handle form data change
const handleChange = useCallback( const handleChange = useCallback(

View File

@ -668,6 +668,13 @@ export default function Settings() {
const { data: fullSchema } = useSWR<RJSFSchema>("config/schema.json"); const { data: fullSchema } = useSWR<RJSFSchema>("config/schema.json");
const hasPendingChanges = Object.keys(pendingDataBySection).length > 0; const hasPendingChanges = Object.keys(pendingDataBySection).length > 0;
const hasPendingValidationErrors = useMemo(
() =>
Object.values(sectionStatusByKey).some(
(status) => !!status && status.hasChanges && status.hasValidationErrors,
),
[sectionStatusByKey],
);
const pendingChangesPreview = useMemo<SaveAllPreviewItem[]>(() => { const pendingChangesPreview = useMemo<SaveAllPreviewItem[]>(() => {
if (!config || !fullSchema) return []; if (!config || !fullSchema) return [];
@ -734,7 +741,13 @@ export default function Settings() {
); );
const handleSaveAll = useCallback(async () => { const handleSaveAll = useCallback(async () => {
if (!config || !fullSchema || !hasPendingChanges) return; if (
!config ||
!fullSchema ||
!hasPendingChanges ||
hasPendingValidationErrors
)
return;
setIsSavingAll(true); setIsSavingAll(true);
let successCount = 0; let successCount = 0;
@ -812,6 +825,7 @@ export default function Settings() {
updated[menuKey] = { updated[menuKey] = {
...updated[menuKey], ...updated[menuKey],
hasChanges: false, hasChanges: false,
hasValidationErrors: false,
}; };
} }
} }
@ -865,6 +879,7 @@ export default function Settings() {
config, config,
fullSchema, fullSchema,
hasPendingChanges, hasPendingChanges,
hasPendingValidationErrors,
pendingDataBySection, pendingDataBySection,
pendingKeyToMenuKey, pendingKeyToMenuKey,
t, t,
@ -885,6 +900,7 @@ export default function Settings() {
updated[menuKey] = { updated[menuKey] = {
...updated[menuKey], ...updated[menuKey],
hasChanges: false, hasChanges: false,
hasValidationErrors: false,
}; };
} }
} }
@ -1011,7 +1027,9 @@ export default function Settings() {
useEffect(() => { useEffect(() => {
if (!selectedCamera || !cameraOverrides) return; if (!selectedCamera || !cameraOverrides) return;
const overrideMap: Partial<Record<SettingsType, SectionStatus>> = {}; const overrideMap: Partial<
Record<SettingsType, Pick<SectionStatus, "hasChanges" | "isOverridden">>
> = {};
// Set override status for all camera sections using the shared mapping // Set override status for all camera sections using the shared mapping
Object.entries(CAMERA_SECTION_MAPPING).forEach( Object.entries(CAMERA_SECTION_MAPPING).forEach(
@ -1031,7 +1049,12 @@ export default function Settings() {
// Merge and update both hasChanges and isOverridden for camera sections // Merge and update both hasChanges and isOverridden for camera sections
const merged = { ...prev }; const merged = { ...prev };
Object.entries(overrideMap).forEach(([key, status]) => { Object.entries(overrideMap).forEach(([key, status]) => {
merged[key as SettingsType] = status; const existingStatus = merged[key as SettingsType];
merged[key as SettingsType] = {
hasChanges: status.hasChanges,
isOverridden: status.isOverridden,
hasValidationErrors: existingStatus?.hasValidationErrors ?? false,
};
}); });
return merged; return merged;
}); });
@ -1164,7 +1187,7 @@ export default function Settings() {
onClick={handleSaveAll} onClick={handleSaveAll}
variant="select" variant="select"
size="sm" size="sm"
disabled={isSavingAll} disabled={isSavingAll || hasPendingValidationErrors}
className="flex w-full items-center justify-center gap-2" className="flex w-full items-center justify-center gap-2"
> >
{isSavingAll ? ( {isSavingAll ? (
@ -1306,7 +1329,7 @@ export default function Settings() {
variant="select" variant="select"
size="sm" size="sm"
onClick={handleSaveAll} onClick={handleSaveAll}
disabled={isSavingAll} disabled={isSavingAll || hasPendingValidationErrors}
className="flex items-center justify-center gap-2" className="flex items-center justify-center gap-2"
> >
{isSavingAll ? ( {isSavingAll ? (

View File

@ -31,6 +31,7 @@ export type SettingsPageProps = {
export type SectionStatus = { export type SectionStatus = {
hasChanges: boolean; hasChanges: boolean;
isOverridden: boolean; isOverridden: boolean;
hasValidationErrors: boolean;
}; };
export type SingleSectionPageOptions = { export type SingleSectionPageOptions = {
@ -67,6 +68,7 @@ export function SingleSectionPage({
const [sectionStatus, setSectionStatus] = useState<SectionStatus>({ const [sectionStatus, setSectionStatus] = useState<SectionStatus>({
hasChanges: false, hasChanges: false,
isOverridden: false, isOverridden: false,
hasValidationErrors: false,
}); });
const resolvedSectionConfig = useMemo( const resolvedSectionConfig = useMemo(
() => sectionConfig ?? getSectionConfig(sectionKey, level), () => sectionConfig ?? getSectionConfig(sectionKey, level),