sync object filter entries with tracked labels in camera config form

Filter sub-collapsibles in the camera Objects section are driven by `filters` dict keys, but profile merges and live track-switch edits don't add matching entries, so newly tracked labels (like from a profile override) had no collapsible. Synthesize default filter entries from `track` in the form data so every tracked label renders a collapsible; baseline data also gets the synthesized entries, so save payloads are unchanged.
This commit is contained in:
Josh Hawkins 2026-05-06 14:08:33 -05:00
parent 9e3f42444b
commit b317f6b8ad
2 changed files with 61 additions and 5 deletions

View File

@ -22,6 +22,7 @@ import {
modifySchemaForSection, modifySchemaForSection,
getEffectiveDefaultsForSection, getEffectiveDefaultsForSection,
sanitizeOverridesForSection, sanitizeOverridesForSection,
synthesizeMissingObjectFilters,
} from "./section-special-cases"; } from "./section-special-cases";
import { getSectionValidation } from "../section-validations"; import { getSectionValidation } from "../section-validations";
import { useConfigOverride } from "@/hooks/use-config-override"; import { useConfigOverride } from "@/hooks/use-config-override";
@ -357,15 +358,19 @@ export function ConfigSection({
return get(config, sectionPath); return get(config, sectionPath);
}, [config, cameraName, sectionPath, effectiveLevel, profileName]); }, [config, cameraName, sectionPath, effectiveLevel, profileName]);
const rawFormData = useMemo(() => { const rawFormData = useMemo<ConfigSectionData>(() => {
if (!config) return {}; if (!config) return {};
if (rawSectionValue === undefined || rawSectionValue === null) { if (rawSectionValue === undefined || rawSectionValue === null) {
return {}; return {};
} }
return rawSectionValue; return synthesizeMissingObjectFilters(
}, [config, rawSectionValue]); sectionPath,
rawSectionValue,
modifiedSchema ?? undefined,
) as ConfigSectionData;
}, [config, rawSectionValue, sectionPath, modifiedSchema]);
// When editing a profile, hide fields that require a restart since they // When editing a profile, hide fields that require a restart since they
// cannot take effect via profile switching alone. // cannot take effect via profile switching alone.
@ -387,7 +392,7 @@ export function ConfigSection({
const baseData = modifiedSchema const baseData = modifiedSchema
? applySchemaDefaults(modifiedSchema, rawFormData) ? applySchemaDefaults(modifiedSchema, rawFormData)
: rawFormData; : rawFormData;
return sanitizeSectionData(baseData); return sanitizeSectionData(baseData as ConfigSectionData);
}, [rawFormData, modifiedSchema, sanitizeSectionData]); }, [rawFormData, modifiedSchema, sanitizeSectionData]);
const baselineSnapshot = useMemo(() => { const baselineSnapshot = useMemo(() => {
@ -506,7 +511,11 @@ export function ConfigSection({
setPendingOverrides(undefined); setPendingOverrides(undefined);
return; return;
} }
const sanitizedData = sanitizeSectionData(data as ConfigSectionData); const sanitizedData = synthesizeMissingObjectFilters(
sectionPath,
sanitizeSectionData(data as ConfigSectionData),
modifiedSchema ?? undefined,
) as ConfigSectionData;
const nextBaselineFormData = baselineSnapshot; const nextBaselineFormData = baselineSnapshot;
const overrides = buildOverrides( const overrides = buildOverrides(
sanitizedData, sanitizedData,
@ -546,6 +555,8 @@ export function ConfigSection({
setPendingOverrides, setPendingOverrides,
setDirtyOverrides, setDirtyOverrides,
baselineSnapshot, baselineSnapshot,
sectionPath,
modifiedSchema,
], ],
); );

View File

@ -105,6 +105,51 @@ export function getEffectiveDefaultsForSection(
return schemaDefaults; return schemaDefaults;
} }
/**
* Add default filter entries for any label in `objects.track` that isn't
* already in `objects.filters`, so each tracked label gets a collapsible.
* The backend only auto-populates filters at config init, not after profile
* merges or live track edits.
*/
export function synthesizeMissingObjectFilters(
sectionPath: string,
data: unknown,
sectionSchema: RJSFSchema | undefined,
): unknown {
if (sectionPath !== "objects") return data;
if (!isJsonObject(data)) return data;
const trackValue = (data as JsonObject).track;
if (!Array.isArray(trackValue) || trackValue.length === 0) return data;
const properties = (sectionSchema as { properties?: Record<string, unknown> })
?.properties;
const filtersSchema = isJsonObject(properties)
? (properties.filters as { additionalProperties?: unknown } | undefined)
: undefined;
const filterEntrySchema = isJsonObject(filtersSchema?.additionalProperties)
? (filtersSchema.additionalProperties as RJSFSchema)
: undefined;
const existingFilters = isJsonObject((data as JsonObject).filters)
? ((data as JsonObject).filters as JsonObject)
: {};
const newFilters: JsonObject = { ...existingFilters };
let added = false;
for (const label of trackValue) {
if (typeof label !== "string") continue;
if (Object.prototype.hasOwnProperty.call(newFilters, label)) continue;
newFilters[label] = (
filterEntrySchema ? applySchemaDefaults(filterEntrySchema, {}) : {}
) as JsonValue;
added = true;
}
if (!added) return data;
return { ...(data as JsonObject), filters: newFilters };
}
/** /**
* Sanitize overrides payloads for section-specific quirks. * Sanitize overrides payloads for section-specific quirks.
*/ */