add expanded hidden field context

This commit is contained in:
Josh Hawkins 2026-05-19 08:09:56 -05:00
parent b2ea1fe592
commit 702cc7133d
6 changed files with 83 additions and 20 deletions

View File

@ -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

View File

@ -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[] = [];

View File

@ -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

View File

@ -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";

View File

@ -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.<attr>` for each
* `model.all_attributes` entry on the objects section).
* patterns from the loaded config and scope (e.g. `filters.<attr>` 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);
}

View File

@ -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],
);