respect section hiddenFields when detecting config overrides

This commit is contained in:
Josh Hawkins 2026-05-07 09:05:32 -05:00
parent d0f44de6bc
commit 9c44a2f662
2 changed files with 50 additions and 7 deletions

View File

@ -1,6 +1,7 @@
// Hook to detect when camera config overrides global defaults // Hook to detect when camera config overrides global defaults
import { useMemo } from "react"; import { useMemo } from "react";
import useSWR from "swr"; import useSWR from "swr";
import cloneDeep from "lodash/cloneDeep";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import get from "lodash/get"; import get from "lodash/get";
import set from "lodash/set"; import set from "lodash/set";
@ -8,7 +9,11 @@ import type { RJSFSchema } from "@rjsf/utils";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { JsonObject, JsonValue } from "@/types/configForm"; import { JsonObject, JsonValue } from "@/types/configForm";
import { isJsonObject } from "@/lib/utils"; import { isJsonObject } from "@/lib/utils";
import { getBaseCameraSectionValue } from "@/utils/configUtil"; import {
getBaseCameraSectionValue,
getEffectiveHiddenFields,
unsetWithWildcard,
} from "@/utils/configUtil";
import { extractSectionSchema } from "@/hooks/use-config-schema"; import { extractSectionSchema } from "@/hooks/use-config-schema";
import { applySchemaDefaults } from "@/lib/config-schema"; import { applySchemaDefaults } from "@/lib/config-schema";
@ -38,6 +43,21 @@ export function normalizeConfigValue(value: unknown): JsonValue {
return stripInternalFields(value as JsonValue); return stripInternalFields(value as JsonValue);
} }
/**
* Remove hidden-field paths from a value before comparison so fields the
* user can't change in the UI (e.g. motion masks, attribute filters) don't
* trigger override badges. Operates on a clone so the input is unchanged.
*/
function stripHiddenPaths(value: JsonValue, hiddenFields: string[]): JsonValue {
if (hiddenFields.length === 0 || !isJsonObject(value)) return value;
const cloned = cloneDeep(value) as JsonObject;
for (const path of hiddenFields) {
if (!path) continue;
unsetWithWildcard(cloned as Record<string, unknown>, path);
}
return cloned;
}
/** /**
* Collapse null and empty-object values for override comparisons so * Collapse null and empty-object values for override comparisons so
* semantically equivalent shapes match. The schema may default `mask: None` * semantically equivalent shapes match. The schema may default `mask: None`
@ -45,7 +65,7 @@ export function normalizeConfigValue(value: unknown): JsonValue {
* masks", so collapsing them here keeps the equality check honest. We * masks", so collapsing them here keeps the equality check honest. We
* keep this off the public `normalizeConfigValue` so save-flow code paths * keep this off the public `normalizeConfigValue` so save-flow code paths
* (which serialize form data) aren't affected. * (which serialize form data) aren't affected.
*/ **/
function collapseEmpty(value: JsonValue): JsonValue { function collapseEmpty(value: JsonValue): JsonValue {
if (Array.isArray(value)) { if (Array.isArray(value)) {
return value.map(collapseEmpty); return value.map(collapseEmpty);
@ -202,8 +222,21 @@ export function useConfigOverride({
// Collapse empty/null values for comparison so semantically equivalent // Collapse empty/null values for comparison so semantically equivalent
// shapes (e.g. schema default `mask: null` vs runtime `mask: {}`) match. // shapes (e.g. schema default `mask: null` vs runtime `mask: {}`) match.
const collapsedGlobal = collapseEmpty(normalizedGlobalValue); // Also strip hidden-field paths (motion masks, attribute filters, etc.)
const collapsedCamera = collapseEmpty(normalizedCameraValue); // so fields the user can't edit in the UI don't trigger override badges.
const hiddenFields = getEffectiveHiddenFields(
sectionPath,
"camera",
config,
);
const collapsedGlobal = stripHiddenPaths(
collapseEmpty(normalizedGlobalValue),
hiddenFields,
);
const collapsedCamera = stripHiddenPaths(
collapseEmpty(normalizedCameraValue),
hiddenFields,
);
const comparisonGlobal = compareFields const comparisonGlobal = compareFields
? pickFields(collapsedGlobal, compareFields) ? pickFields(collapsedGlobal, compareFields)
@ -328,8 +361,15 @@ export function useAllCameraOverrides(
getBaseCameraSectionValue(config, cameraName, key), getBaseCameraSectionValue(config, cameraName, key),
); );
const collapsedGlobal = collapseEmpty(globalValue); const hiddenFields = getEffectiveHiddenFields(key, "camera", config);
const collapsedCamera = collapseEmpty(cameraValue); const collapsedGlobal = stripHiddenPaths(
collapseEmpty(globalValue),
hiddenFields,
);
const collapsedCamera = stripHiddenPaths(
collapseEmpty(cameraValue),
hiddenFields,
);
const comparisonGlobal = compareFields const comparisonGlobal = compareFields
? pickFields(collapsedGlobal, compareFields) ? pickFields(collapsedGlobal, compareFields)
: collapsedGlobal; : collapsedGlobal;

View File

@ -254,7 +254,10 @@ export function flattenOverrides(
// lodash `unset` treats `*` as a literal key. This helper expands wildcard // lodash `unset` treats `*` as a literal key. This helper expands wildcard
// segments so that e.g. `"filters.*.mask"` unsets `filters.<each key>.mask`. // segments so that e.g. `"filters.*.mask"` unsets `filters.<each key>.mask`.
function unsetWithWildcard(obj: Record<string, unknown>, path: string): void { export function unsetWithWildcard(
obj: Record<string, unknown>,
path: string,
): void {
if (!path.includes("*")) { if (!path.includes("*")) {
unset(obj, path); unset(obj, path);
return; return;