diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 897b5f262..bd16a98bd 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -65,10 +65,14 @@ import { globalCameraDefaultSections, buildOverrides, buildConfigDataForPath, + flattenOverrides, getBaseCameraSectionValue, sanitizeSectionData as sharedSanitizeSectionData, requiresRestartForOverrides as sharedRequiresRestartForOverrides, } from "@/utils/configUtil"; +import SaveAllPreviewPopover, { + type SaveAllPreviewItem, +} from "@/components/overlay/detail/SaveAllPreviewPopover"; import RestartDialog from "@/components/overlay/dialog/RestartDialog"; import { useRestart } from "@/api/ws"; import type { @@ -913,6 +917,34 @@ export function ConfigSection({ ); }, [sectionConfig?.renderers, sectionPath, cameraName, setPendingData]); + // Build a flat list of pending field changes for this section only. + // Mirrors the global Save All preview but scoped to the current section so + // users can inspect what will be saved without leaving the section. + const sectionPreviewItems = useMemo(() => { + if (!hasChanges) return []; + if (!effectiveOverrides || typeof effectiveOverrides !== "object") { + return []; + } + const flattened = flattenOverrides(effectiveOverrides as JsonValue); + return flattened.map(({ path, value }) => ({ + scope: effectiveLevel, + cameraName, + profileName: profileName + ? (profileFriendlyName ?? profileName) + : undefined, + fieldPath: path ? `${sectionPath}.${path}` : sectionPath, + value, + })); + }, [ + hasChanges, + effectiveOverrides, + effectiveLevel, + cameraName, + profileName, + profileFriendlyName, + sectionPath, + ]); + if (!modifiedSchema) { return null; } @@ -1018,6 +1050,12 @@ export function ConfigSection({ defaultValue: "You have unsaved changes", })} + )}
diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index fd19da4f9..a4b7f2245 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -28,11 +28,7 @@ import useOptimisticState from "@/hooks/use-optimistic-state"; import { isMobile } from "react-device-detect"; import { FaVideo } from "react-icons/fa"; import { CameraConfig, FrigateConfig } from "@/types/frigateConfig"; -import type { - ConfigSectionData, - JsonObject, - JsonValue, -} from "@/types/configForm"; +import type { ConfigSectionData, JsonObject } from "@/types/configForm"; import useSWR from "swr"; import FilterSwitch from "@/components/filter/FilterSwitch"; import { ZoneMaskFilterButton } from "@/components/filter/ZoneMaskFilter"; @@ -93,6 +89,7 @@ import { mutate } from "swr"; import { RJSFSchema } from "@rjsf/utils"; import { buildConfigDataForPath, + flattenOverrides, parseProfileFromSectionPath, prepareSectionSavePayload, PROFILE_ELIGIBLE_SECTIONS, @@ -190,25 +187,6 @@ const parsePendingDataKey = (pendingDataKey: string) => { }; }; -const flattenOverrides = ( - value: JsonValue | undefined, - path: string[] = [], -): Array<{ path: string; value: JsonValue }> => { - if (value === undefined) return []; - if (value === null || typeof value !== "object" || Array.isArray(value)) { - return [{ path: path.join("."), value }]; - } - - const entries = Object.entries(value); - if (entries.length === 0) { - return [{ path: path.join("."), value: {} }]; - } - - return entries.flatMap(([key, entryValue]) => - flattenOverrides(entryValue, [...path, key]), - ); -}; - const createSectionPage = ( sectionKey: string, level: "global" | "camera", diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index fb233a457..82e54f784 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -219,6 +219,32 @@ export function buildOverrides( return current; } +// --------------------------------------------------------------------------- +// flattenOverrides — turn an overrides object into a list of leaf paths +// --------------------------------------------------------------------------- + +// Walks a nested overrides value and produces a flat list of `{ path, value }` +// entries, one per leaf. Used by save/preview UIs to enumerate the individual +// fields that will be changed. +export function flattenOverrides( + value: JsonValue | undefined, + path: string[] = [], +): Array<{ path: string; value: JsonValue }> { + if (value === undefined) return []; + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return [{ path: path.join("."), value }]; + } + + const entries = Object.entries(value); + if (entries.length === 0) { + return [{ path: path.join("."), value: {} }]; + } + + return entries.flatMap(([key, entryValue]) => + flattenOverrides(entryValue, [...path, key]), + ); +} + // --------------------------------------------------------------------------- // sanitizeSectionData — normalize config values and strip hidden fields // ---------------------------------------------------------------------------