From 702cc7133d2efc85f933980f487a2ad23bcd84e3 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Tue, 19 May 2026 08:09:56 -0500 Subject: [PATCH] add expanded hidden field context --- .../sections/CameraOverridesBadge.tsx | 3 +- web/src/hooks/use-config-override.ts | 13 ++-- web/src/pages/Settings.tsx | 5 +- web/src/types/configForm.ts | 14 ++++- web/src/utils/configUtil.ts | 63 ++++++++++++++++--- .../DetectorsAndModelSettingsView.tsx | 5 +- 6 files changed, 83 insertions(+), 20 deletions(-) diff --git a/web/src/components/config-form/sections/CameraOverridesBadge.tsx b/web/src/components/config-form/sections/CameraOverridesBadge.tsx index 466934a770..9d3dde29d6 100644 --- a/web/src/components/config-form/sections/CameraOverridesBadge.tsx +++ b/web/src/components/config-form/sections/CameraOverridesBadge.tsx @@ -20,6 +20,7 @@ import type { ProfilesApiResponse } from "@/types/profile"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { formatList } from "@/utils/stringUtil"; import { + buildHiddenFieldContext, getEffectiveHiddenFields, pathMatchesHiddenPattern, } from "@/utils/configUtil"; @@ -187,7 +188,7 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) { const hiddenFields = getEffectiveHiddenFields( sectionPath, "global", - config, + buildHiddenFieldContext(config, "global"), ); if (hiddenFields.length === 0) return rawEntries; return rawEntries diff --git a/web/src/hooks/use-config-override.ts b/web/src/hooks/use-config-override.ts index 90ce717293..2b0ed2cbd4 100644 --- a/web/src/hooks/use-config-override.ts +++ b/web/src/hooks/use-config-override.ts @@ -10,6 +10,7 @@ import { FrigateConfig } from "@/types/frigateConfig"; import { JsonObject, JsonValue } from "@/types/configForm"; import { isJsonObject } from "@/lib/utils"; import { + buildHiddenFieldContext, getBaseCameraSectionValue, getEffectiveHiddenFields, pathMatchesHiddenPattern, @@ -286,7 +287,7 @@ export function useConfigOverride({ const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const collapsedGlobal = stripHiddenPaths( collapseEmpty(normalizedGlobalValue), @@ -439,7 +440,11 @@ export function useAllCameraOverrides( getBaseCameraSectionValue(config, cameraName, key), ); - const hiddenFields = getEffectiveHiddenFields(key, "camera", config); + const hiddenFields = getEffectiveHiddenFields( + key, + "camera", + buildHiddenFieldContext(config, "camera", cameraName), + ); const collapsedGlobal = stripHiddenPaths( collapseEmpty(globalValue), hiddenFields, @@ -795,7 +800,7 @@ export function useCameraSectionDeltas( const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const deltas: FieldDelta[] = []; @@ -864,7 +869,7 @@ export function useProfileSectionDeltas( const hiddenFields = getEffectiveHiddenFields( sectionPath, "camera", - config, + buildHiddenFieldContext(config, "camera", cameraName), ); const deltas: FieldDelta[] = []; diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 6ebfa92638..c83dbcc1c9 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -89,6 +89,7 @@ import { mutate } from "swr"; import { RJSFSchema } from "@rjsf/utils"; import { buildConfigDataForPath, + buildHiddenFieldContext, flattenOverrides, getSectionConfig, parseProfileFromSectionPath, @@ -851,11 +852,11 @@ export default function Settings() { // they stay in sync with what the embedded forms strip on render const detectorHiddenFields = resolveHiddenFieldEntries( getSectionConfig("detectors", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ); const modelHiddenFields = resolveHiddenFieldEntries( getSectionConfig("model", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ); const sanitizedDetectors = pendingDetectors !== undefined diff --git a/web/src/types/configForm.ts b/web/src/types/configForm.ts index 03ecd3e4d9..1f4c57c393 100644 --- a/web/src/types/configForm.ts +++ b/web/src/types/configForm.ts @@ -13,7 +13,19 @@ export type JsonArray = JsonValue[]; export type ConfigSectionData = JsonObject; -export type HiddenFieldEntry = string | ((config: FrigateConfig) => string[]); +export type HiddenFieldContext = { + fullConfig: FrigateConfig; + fullCameraConfig?: CameraConfig; + level: "global" | "camera" | "replay"; + cameraName?: string; + // Saved form data for the current section/scope (i.e. rawFormData in + // BaseSection.tsx). Not the user's in-flight RJSF edits. Optional because + // most hidden-field callsites compute patterns without a specific section + // value on hand; resolvers fall back to fullCameraConfig / fullConfig. + formData?: ConfigSectionData; +}; + +export type HiddenFieldEntry = string | ((ctx: HiddenFieldContext) => string[]); export type ConfigFormContext = { level?: "global" | "camera"; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index 80c940cb70..e69ae86ee7 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -22,6 +22,7 @@ import type { RJSFSchema } from "@rjsf/utils"; import type { FrigateConfig } from "@/types/frigateConfig"; import type { ConfigSectionData, + HiddenFieldContext, JsonObject, JsonValue, } from "@/types/configForm"; @@ -568,6 +569,17 @@ export function prepareSectionSavePayload(opts: { schemaSection, level, sectionSchema, + config + ? { + fullConfig: config, + fullCameraConfig: + level === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + } + : undefined, ); // Compute rawFormData (the current stored value for this section) @@ -615,10 +627,16 @@ export function prepareSectionSavePayload(opts: { // For profile sections, also hide restart-required fields to match // effectiveHiddenFields in BaseSection (prevents spurious deletion markers // for fields that are hidden from the form during profile editing). - const resolvedHidden = resolveHiddenFieldEntries( - sectionConfig.hiddenFields, - config, - ); + const resolvedHidden = resolveHiddenFieldEntries(sectionConfig.hiddenFields, { + fullConfig: config, + fullCameraConfig: + level === "camera" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + formData: rawFormData as ConfigSectionData, + }); const hiddenFieldsForSanitize = profileInfo.isProfile && sectionConfig.restartRequired?.length ? [...new Set([...resolvedHidden, ...sectionConfig.restartRequired])] @@ -731,32 +749,57 @@ export function getSectionConfig( return mergeSectionConfig(entry.base, overrides); } +/** + * Build a `HiddenFieldContext` for the common case where a callsite has + * `config`, an optional `cameraName`, and a level, but no per-section + * saved form data to thread through. Resolvers that don't read `formData` + * (which is most of them) just fall through to `fullCameraConfig` / + * `fullConfig`. + */ +export function buildHiddenFieldContext( + config: FrigateConfig | undefined, + level: "global" | "camera" | "replay", + cameraName?: string, +): HiddenFieldContext | undefined { + if (!config) return undefined; + return { + fullConfig: config, + fullCameraConfig: + level !== "global" && cameraName + ? config.cameras?.[cameraName] + : undefined, + level, + cameraName, + }; +} + /** * Resolve the effective hidden-field patterns for a section. Each entry in * `hiddenFields` is either a literal pattern or a function that produces - * patterns from the loaded config (e.g. `filters.` for each - * `model.all_attributes` entry on the objects section). + * patterns from the loaded config and scope (e.g. `filters.` for each + * `model.all_attributes` entry on the objects section, gated by the + * effective `objects.track` list at the current scope). */ export function getEffectiveHiddenFields( sectionKey: string, level: "global" | "camera" | "replay", - config: FrigateConfig | undefined, + ctx: HiddenFieldContext | undefined, ): string[] { return resolveHiddenFieldEntries( getSectionConfig(sectionKey, level).hiddenFields, - config, + ctx, ); } export function resolveHiddenFieldEntries( entries: SectionConfig["hiddenFields"] | undefined, - config: FrigateConfig | undefined, + ctx: HiddenFieldContext | undefined, ): string[] { if (!entries || entries.length === 0) return []; const result: string[] = []; for (const entry of entries) { if (typeof entry === "function") { - if (config) result.push(...entry(config)); + if (ctx) result.push(...entry(ctx)); } else { result.push(entry); } diff --git a/web/src/views/settings/DetectorsAndModelSettingsView.tsx b/web/src/views/settings/DetectorsAndModelSettingsView.tsx index 615ab3296c..ebbad2b523 100644 --- a/web/src/views/settings/DetectorsAndModelSettingsView.tsx +++ b/web/src/views/settings/DetectorsAndModelSettingsView.tsx @@ -51,6 +51,7 @@ import { ConfigSectionTemplate } from "@/components/config-form/sections"; import { ConfigMessageBanner } from "@/components/config-form/ConfigMessageBanner"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { + buildHiddenFieldContext, getSectionConfig, resolveHiddenFieldEntries, sanitizeSectionData, @@ -226,7 +227,7 @@ export default function DetectorsAndModelSettingsView({ () => resolveHiddenFieldEntries( getSectionConfig("detectors", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ), [config], ); @@ -234,7 +235,7 @@ export default function DetectorsAndModelSettingsView({ () => resolveHiddenFieldEntries( getSectionConfig("model", "global").hiddenFields, - config, + buildHiddenFieldContext(config, "global"), ), [config], );