From feff214d73ea14dbd8eb3320f0e9e69d0ec7a792 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 6 May 2026 21:34:55 -0500 Subject: [PATCH] add support for config-aware patterns in section hiddenFields Section configs can now declare dynamic hidden-field entries as functions of the loaded config; objects.ts uses this to hide auto-populated attribute filters (DHL, face, license_plate, etc.) from the form, save flow, and override popover when those labels aren't user-settable --- .../config-form/section-configs/objects.ts | 11 +++++ .../config-form/sections/BaseSection.tsx | 20 ++++++-- .../sections/CameraOverridesBadge.tsx | 11 +++-- web/src/types/configForm.ts | 2 + web/src/utils/configUtil.ts | 48 ++++++++++++++++--- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/web/src/components/config-form/section-configs/objects.ts b/web/src/components/config-form/section-configs/objects.ts index e30ddf9d92..5a87bdc623 100644 --- a/web/src/components/config-form/section-configs/objects.ts +++ b/web/src/components/config-form/section-configs/objects.ts @@ -1,5 +1,13 @@ +import type { FrigateConfig } from "@/types/frigateConfig"; import type { SectionConfigOverrides } from "./types"; +// Attribute labels (face, license_plate, Frigate+ couriers like DHL/Amazon, +// etc.) are populated into objects.filters by the backend even when the +// model can't actually detect them. They aren't user-settable, so hide any +// `filters.` patterns from forms and override comparisons. +const hideAttributeFilters = (config: FrigateConfig): string[] => + (config.model?.all_attributes ?? []).map((attr) => `filters.${attr}`); + const objects: SectionConfigOverrides = { base: { sectionDocs: "/configuration/object_filters", @@ -26,6 +34,7 @@ const objects: SectionConfigOverrides = { "filters.*.raw_mask", "filters.mask", "filters.raw_mask", + hideAttributeFilters, ], advancedFields: ["genai"], uiSchema: { @@ -99,6 +108,7 @@ const objects: SectionConfigOverrides = { "filters.mask", "filters.raw_mask", "genai.required_zones", + hideAttributeFilters, ], }, camera: { @@ -123,6 +133,7 @@ const objects: SectionConfigOverrides = { "filters.*.raw_mask", "filters.mask", "filters.raw_mask", + hideAttributeFilters, ], advancedFields: [], }, diff --git a/web/src/components/config-form/sections/BaseSection.tsx b/web/src/components/config-form/sections/BaseSection.tsx index 63fdc1e4c5..588742ac55 100644 --- a/web/src/components/config-form/sections/BaseSection.tsx +++ b/web/src/components/config-form/sections/BaseSection.tsx @@ -59,7 +59,11 @@ import { } from "@/components/ui/alert-dialog"; import { applySchemaDefaults } from "@/lib/config-schema"; import { cn } from "@/lib/utils"; -import { ConfigSectionData, JsonValue } from "@/types/configForm"; +import { + ConfigSectionData, + HiddenFieldEntry, + JsonValue, +} from "@/types/configForm"; import ActivityIndicator from "@/components/indicators/activity-indicator"; import { StatusBarMessagesContext } from "@/context/statusbar-provider"; import { @@ -69,6 +73,7 @@ import { buildConfigDataForPath, flattenOverrides, getBaseCameraSectionValue, + resolveHiddenFieldEntries, sanitizeSectionData as sharedSanitizeSectionData, requiresRestartForOverrides as sharedRequiresRestartForOverrides, } from "@/utils/configUtil"; @@ -91,7 +96,7 @@ export interface SectionConfig { /** Fields to group together */ fieldGroups?: Record; /** Fields to hide from UI */ - hiddenFields?: string[]; + hiddenFields?: HiddenFieldEntry[]; /** Fields to show in advanced section */ advancedFields?: string[]; /** Fields to compare for override detection */ @@ -375,12 +380,17 @@ export function ConfigSection({ // When editing a profile, hide fields that require a restart since they // cannot take effect via profile switching alone. const effectiveHiddenFields = useMemo(() => { + const base = resolveHiddenFieldEntries(sectionConfig.hiddenFields, config); if (!profileName || !sectionConfig.restartRequired?.length) { - return sectionConfig.hiddenFields; + return base; } - const base = sectionConfig.hiddenFields ?? []; return [...new Set([...base, ...sectionConfig.restartRequired])]; - }, [profileName, sectionConfig.hiddenFields, sectionConfig.restartRequired]); + }, [ + profileName, + sectionConfig.hiddenFields, + sectionConfig.restartRequired, + config, + ]); const sanitizeSectionData = useCallback( (data: ConfigSectionData) => diff --git a/web/src/components/config-form/sections/CameraOverridesBadge.tsx b/web/src/components/config-form/sections/CameraOverridesBadge.tsx index 00621d15b6..6ccbb028ca 100644 --- a/web/src/components/config-form/sections/CameraOverridesBadge.tsx +++ b/web/src/components/config-form/sections/CameraOverridesBadge.tsx @@ -20,7 +20,7 @@ import type { ProfilesApiResponse } from "@/types/profile"; import { humanizeKey } from "@/components/config-form/theme/utils/i18n"; import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name"; import { formatList } from "@/utils/stringUtil"; -import { getSectionConfig } from "@/utils/configUtil"; +import { getEffectiveHiddenFields } from "@/utils/configUtil"; const CAMERA_PAGE_BY_SECTION: Record = { detect: "cameraDetect", @@ -247,8 +247,11 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) { const rawEntries = useCamerasOverridingSection(config, sectionPath); const entries = useMemo(() => { - const hiddenFields = - getSectionConfig(sectionPath, "global").hiddenFields ?? []; + const hiddenFields = getEffectiveHiddenFields( + sectionPath, + "global", + config, + ); if (hiddenFields.length === 0) return rawEntries; return rawEntries .map((entry) => ({ @@ -261,7 +264,7 @@ export function CameraOverridesBadge({ sectionPath, className }: Props) { ), })) .filter((entry) => entry.fieldDeltas.length > 0); - }, [rawEntries, sectionPath]); + }, [rawEntries, sectionPath, config]); if (SECTIONS_WITHOUT_OVERRIDE_BADGE.has(sectionPath)) { return null; diff --git a/web/src/types/configForm.ts b/web/src/types/configForm.ts index f228de4306..9e6181266a 100644 --- a/web/src/types/configForm.ts +++ b/web/src/types/configForm.ts @@ -13,6 +13,8 @@ export type JsonArray = JsonValue[]; export type ConfigSectionData = JsonObject; +export type HiddenFieldEntry = string | ((config: FrigateConfig) => string[]); + export type ConfigFormContext = { level?: "global" | "camera"; cameraName?: string; diff --git a/web/src/utils/configUtil.ts b/web/src/utils/configUtil.ts index 82e54f784d..e8c682a5bf 100644 --- a/web/src/utils/configUtil.ts +++ b/web/src/utils/configUtil.ts @@ -587,13 +587,14 @@ 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). - let hiddenFieldsForSanitize = sectionConfig.hiddenFields; - if (profileInfo.isProfile && sectionConfig.restartRequired?.length) { - const base = sectionConfig.hiddenFields ?? []; - hiddenFieldsForSanitize = [ - ...new Set([...base, ...sectionConfig.restartRequired]), - ]; - } + const resolvedHidden = resolveHiddenFieldEntries( + sectionConfig.hiddenFields, + config, + ); + const hiddenFieldsForSanitize = + profileInfo.isProfile && sectionConfig.restartRequired?.length + ? [...new Set([...resolvedHidden, ...sectionConfig.restartRequired])] + : resolvedHidden; // Sanitize raw form data const rawData = sanitizeSectionData( @@ -702,3 +703,36 @@ export function getSectionConfig( : entry.camera; return mergeSectionConfig(entry.base, overrides); } + +/** + * 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). + */ +export function getEffectiveHiddenFields( + sectionKey: string, + level: "global" | "camera" | "replay", + config: FrigateConfig | undefined, +): string[] { + return resolveHiddenFieldEntries( + getSectionConfig(sectionKey, level).hiddenFields, + config, + ); +} + +export function resolveHiddenFieldEntries( + entries: SectionConfig["hiddenFields"] | undefined, + config: FrigateConfig | 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)); + } else { + result.push(entry); + } + } + return result; +}