mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +03:00
add section form special cases
This commit is contained in:
parent
f886846253
commit
f82a721477
@ -10,7 +10,11 @@ import sectionRenderers, {
|
||||
RendererComponent,
|
||||
} from "@/components/config-form/sectionExtras/registry";
|
||||
import { ConfigForm } from "../ConfigForm";
|
||||
import type { FormValidation, RJSFSchema, UiSchema } from "@rjsf/utils";
|
||||
import type { FormValidation, UiSchema } from "@rjsf/utils";
|
||||
import {
|
||||
modifySchemaForSection,
|
||||
getEffectiveDefaultsForSection,
|
||||
} from "./section-special-cases";
|
||||
import { getSectionValidation } from "../section-validations";
|
||||
import {
|
||||
useConfigOverride,
|
||||
@ -225,6 +229,13 @@ export function ConfigSection({
|
||||
// Get section schema using cached hook
|
||||
const sectionSchema = useSectionSchema(sectionPath, level);
|
||||
|
||||
// Apply special case handling for sections with problematic schema defaults
|
||||
const modifiedSchema = useMemo(
|
||||
() =>
|
||||
modifySchemaForSection(sectionPath, level, sectionSchema ?? undefined),
|
||||
[sectionPath, level, sectionSchema],
|
||||
);
|
||||
|
||||
// Get override status
|
||||
const { isOverridden, globalValue, cameraValue } = useConfigOverride({
|
||||
config,
|
||||
@ -275,59 +286,31 @@ export function ConfigSection({
|
||||
);
|
||||
|
||||
const formData = useMemo(() => {
|
||||
const baseData = sectionSchema
|
||||
? applySchemaDefaults(sectionSchema, rawFormData)
|
||||
const baseData = modifiedSchema
|
||||
? applySchemaDefaults(modifiedSchema, rawFormData)
|
||||
: rawFormData;
|
||||
return sanitizeSectionData(baseData);
|
||||
}, [rawFormData, sectionSchema, sanitizeSectionData]);
|
||||
}, [rawFormData, modifiedSchema, sanitizeSectionData]);
|
||||
|
||||
const schemaDefaults = useMemo(() => {
|
||||
if (!sectionSchema) {
|
||||
if (!modifiedSchema) {
|
||||
return {};
|
||||
}
|
||||
return applySchemaDefaults(sectionSchema, {});
|
||||
}, [sectionSchema]);
|
||||
return applySchemaDefaults(modifiedSchema, {});
|
||||
}, [modifiedSchema]);
|
||||
|
||||
const effectiveSchemaDefaults = useMemo(() => {
|
||||
// Special-case: the server JSON Schema for the top-level `motion` global
|
||||
// value is expressed as `anyOf` including `null` (default: null). The
|
||||
// backend intentionally allows `motion` to be omitted/`null` so that an
|
||||
// absent global motion config does not get merged into every camera's
|
||||
// config. However, the form renderer materializes schema defaults into
|
||||
// an object for display. That object-vs-null mismatch would cause our
|
||||
// override-detection to see a difference on first render and show the
|
||||
// false "Modified" badge.
|
||||
//
|
||||
// To avoid changing backend semantics we derive the effective defaults
|
||||
// from the non-null anyOf branch for the `motion` global section and
|
||||
// use those defaults as the comparison baseline in the UI. This ensures
|
||||
// the displayed form and the comparison baseline match while leaving
|
||||
// server behavior unchanged.
|
||||
if (sectionPath !== "motion" || level !== "global" || !sectionSchema) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
const defaultsKeys = Object.keys(schemaDefaults);
|
||||
if (defaultsKeys.length > 0) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
const anyOfSchemas = (sectionSchema as { anyOf?: unknown[] }).anyOf;
|
||||
if (!Array.isArray(anyOfSchemas)) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
const motionSchema = anyOfSchemas.find(
|
||||
(schema) =>
|
||||
typeof schema === "object" && schema !== null && "properties" in schema,
|
||||
);
|
||||
|
||||
if (!motionSchema || typeof motionSchema !== "object") {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
return applySchemaDefaults(motionSchema as RJSFSchema, {});
|
||||
}, [level, schemaDefaults, sectionPath, sectionSchema]);
|
||||
// Get effective defaults, handling special cases where schema defaults
|
||||
// don't match semantic intent
|
||||
const effectiveSchemaDefaults = useMemo(
|
||||
() =>
|
||||
getEffectiveDefaultsForSection(
|
||||
sectionPath,
|
||||
level,
|
||||
modifiedSchema,
|
||||
schemaDefaults,
|
||||
),
|
||||
[level, schemaDefaults, sectionPath, modifiedSchema],
|
||||
);
|
||||
|
||||
// Clear pendingData whenever formData changes (e.g., from server refresh)
|
||||
// This prevents RJSF's initial onChange call from being treated as a user edit
|
||||
@ -705,7 +688,7 @@ export function ConfigSection({
|
||||
);
|
||||
}, [sectionConfig.customValidate, sectionValidation]);
|
||||
|
||||
if (!sectionSchema) {
|
||||
if (!modifiedSchema) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -734,7 +717,7 @@ export function ConfigSection({
|
||||
<div className="space-y-6">
|
||||
<ConfigForm
|
||||
key={formKey}
|
||||
schema={sectionSchema}
|
||||
schema={modifiedSchema}
|
||||
formData={pendingData || formData}
|
||||
onChange={handleChange}
|
||||
fieldOrder={sectionConfig.fieldOrder}
|
||||
|
||||
104
web/src/components/config-form/sections/section-special-cases.ts
Normal file
104
web/src/components/config-form/sections/section-special-cases.ts
Normal file
@ -0,0 +1,104 @@
|
||||
/**
|
||||
* Special case handling for config sections with schema/default issues.
|
||||
*
|
||||
* Some sections have schema patterns that cause false "Modified" indicators
|
||||
* when navigating to them due to how defaults are applied. This utility
|
||||
* centralizes the logic for detecting and handling these cases.
|
||||
*/
|
||||
|
||||
import { RJSFSchema } from "@rjsf/utils";
|
||||
import { applySchemaDefaults } from "@/lib/config-schema";
|
||||
|
||||
/**
|
||||
* Sections that require special handling at the global level.
|
||||
* Add new section paths here as needed.
|
||||
*/
|
||||
const SPECIAL_CASE_SECTIONS = ["motion", "detectors"] as const;
|
||||
|
||||
/**
|
||||
* Check if a section requires special case handling.
|
||||
*/
|
||||
export function isSpecialCaseSection(
|
||||
sectionPath: string,
|
||||
level: string,
|
||||
): boolean {
|
||||
return (
|
||||
level === "global" &&
|
||||
SPECIAL_CASE_SECTIONS.includes(
|
||||
sectionPath as (typeof SPECIAL_CASE_SECTIONS)[number],
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modify schema for sections that need defaults stripped or other modifications.
|
||||
*
|
||||
* - detectors: Strip the "default" field to prevent RJSF from merging the
|
||||
* default {"cpu": {"type": "cpu"}} with stored detector keys.
|
||||
*/
|
||||
export function modifySchemaForSection(
|
||||
sectionPath: string,
|
||||
level: string,
|
||||
schema: RJSFSchema | undefined,
|
||||
): RJSFSchema | undefined {
|
||||
if (!schema || !isSpecialCaseSection(sectionPath, level)) {
|
||||
return schema;
|
||||
}
|
||||
|
||||
// detectors: Remove default to prevent merging with stored keys
|
||||
if (sectionPath === "detectors" && "default" in schema) {
|
||||
const { default: _, ...schemaWithoutDefault } = schema;
|
||||
return schemaWithoutDefault;
|
||||
}
|
||||
|
||||
return schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get effective defaults for sections with special schema patterns.
|
||||
*
|
||||
* - motion: Has anyOf schema with [null, MotionConfig]. When stored value is
|
||||
* null, derive defaults from the non-null anyOf branch to avoid showing
|
||||
* changes when navigating to the page.
|
||||
* - detectors: Return empty object since the schema default would add unwanted
|
||||
* keys to the stored configuration.
|
||||
*/
|
||||
export function getEffectiveDefaultsForSection(
|
||||
sectionPath: string,
|
||||
level: string,
|
||||
schema: RJSFSchema | undefined,
|
||||
schemaDefaults: unknown,
|
||||
): unknown {
|
||||
if (!isSpecialCaseSection(sectionPath, level) || !schema) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
// motion: Derive defaults from non-null anyOf branch
|
||||
if (sectionPath === "motion") {
|
||||
const anyOfSchemas = (schema as { anyOf?: unknown[] }).anyOf;
|
||||
if (!anyOfSchemas || !Array.isArray(anyOfSchemas)) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
// Find the non-null motion config schema
|
||||
const motionSchema = anyOfSchemas.find(
|
||||
(s) =>
|
||||
typeof s === "object" &&
|
||||
s !== null &&
|
||||
(s as { type?: string }).type !== "null",
|
||||
);
|
||||
|
||||
if (!motionSchema) {
|
||||
return schemaDefaults;
|
||||
}
|
||||
|
||||
return applySchemaDefaults(motionSchema as RJSFSchema, {});
|
||||
}
|
||||
|
||||
// detectors: Return empty object to avoid adding default keys
|
||||
if (sectionPath === "detectors") {
|
||||
return {};
|
||||
}
|
||||
|
||||
return schemaDefaults;
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user