mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-26 18:18:22 +03:00
add section form special cases
This commit is contained in:
parent
f886846253
commit
f82a721477
@ -10,7 +10,11 @@ import sectionRenderers, {
|
|||||||
RendererComponent,
|
RendererComponent,
|
||||||
} from "@/components/config-form/sectionExtras/registry";
|
} from "@/components/config-form/sectionExtras/registry";
|
||||||
import { ConfigForm } from "../ConfigForm";
|
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 { getSectionValidation } from "../section-validations";
|
||||||
import {
|
import {
|
||||||
useConfigOverride,
|
useConfigOverride,
|
||||||
@ -225,6 +229,13 @@ export function ConfigSection({
|
|||||||
// Get section schema using cached hook
|
// Get section schema using cached hook
|
||||||
const sectionSchema = useSectionSchema(sectionPath, level);
|
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
|
// Get override status
|
||||||
const { isOverridden, globalValue, cameraValue } = useConfigOverride({
|
const { isOverridden, globalValue, cameraValue } = useConfigOverride({
|
||||||
config,
|
config,
|
||||||
@ -275,59 +286,31 @@ export function ConfigSection({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const formData = useMemo(() => {
|
const formData = useMemo(() => {
|
||||||
const baseData = sectionSchema
|
const baseData = modifiedSchema
|
||||||
? applySchemaDefaults(sectionSchema, rawFormData)
|
? applySchemaDefaults(modifiedSchema, rawFormData)
|
||||||
: rawFormData;
|
: rawFormData;
|
||||||
return sanitizeSectionData(baseData);
|
return sanitizeSectionData(baseData);
|
||||||
}, [rawFormData, sectionSchema, sanitizeSectionData]);
|
}, [rawFormData, modifiedSchema, sanitizeSectionData]);
|
||||||
|
|
||||||
const schemaDefaults = useMemo(() => {
|
const schemaDefaults = useMemo(() => {
|
||||||
if (!sectionSchema) {
|
if (!modifiedSchema) {
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
return applySchemaDefaults(sectionSchema, {});
|
return applySchemaDefaults(modifiedSchema, {});
|
||||||
}, [sectionSchema]);
|
}, [modifiedSchema]);
|
||||||
|
|
||||||
const effectiveSchemaDefaults = useMemo(() => {
|
// Get effective defaults, handling special cases where schema defaults
|
||||||
// Special-case: the server JSON Schema for the top-level `motion` global
|
// don't match semantic intent
|
||||||
// value is expressed as `anyOf` including `null` (default: null). The
|
const effectiveSchemaDefaults = useMemo(
|
||||||
// backend intentionally allows `motion` to be omitted/`null` so that an
|
() =>
|
||||||
// absent global motion config does not get merged into every camera's
|
getEffectiveDefaultsForSection(
|
||||||
// config. However, the form renderer materializes schema defaults into
|
sectionPath,
|
||||||
// an object for display. That object-vs-null mismatch would cause our
|
level,
|
||||||
// override-detection to see a difference on first render and show the
|
modifiedSchema,
|
||||||
// false "Modified" badge.
|
schemaDefaults,
|
||||||
//
|
),
|
||||||
// To avoid changing backend semantics we derive the effective defaults
|
[level, schemaDefaults, sectionPath, modifiedSchema],
|
||||||
// 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]);
|
|
||||||
|
|
||||||
// Clear pendingData whenever formData changes (e.g., from server refresh)
|
// Clear pendingData whenever formData changes (e.g., from server refresh)
|
||||||
// This prevents RJSF's initial onChange call from being treated as a user edit
|
// This prevents RJSF's initial onChange call from being treated as a user edit
|
||||||
@ -705,7 +688,7 @@ export function ConfigSection({
|
|||||||
);
|
);
|
||||||
}, [sectionConfig.customValidate, sectionValidation]);
|
}, [sectionConfig.customValidate, sectionValidation]);
|
||||||
|
|
||||||
if (!sectionSchema) {
|
if (!modifiedSchema) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -734,7 +717,7 @@ export function ConfigSection({
|
|||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<ConfigForm
|
<ConfigForm
|
||||||
key={formKey}
|
key={formKey}
|
||||||
schema={sectionSchema}
|
schema={modifiedSchema}
|
||||||
formData={pendingData || formData}
|
formData={pendingData || formData}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
fieldOrder={sectionConfig.fieldOrder}
|
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