From ddba2dd6e7b98999c328e8c2ef7fe510f66dc50f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 8 Mar 2026 11:47:26 -0500 Subject: [PATCH] add optional field widget adds a switch to enable nullable fields like skip_motion_threshold --- .../config-form/theme/frigateTheme.ts | 2 + .../theme/widgets/OptionalFieldWidget.tsx | 64 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx diff --git a/web/src/components/config-form/theme/frigateTheme.ts b/web/src/components/config-form/theme/frigateTheme.ts index 3baa2f3ad..79bc14b84 100644 --- a/web/src/components/config-form/theme/frigateTheme.ts +++ b/web/src/components/config-form/theme/frigateTheme.ts @@ -26,6 +26,7 @@ import { FfmpegArgsWidget } from "./widgets/FfmpegArgsWidget"; import { InputRolesWidget } from "./widgets/InputRolesWidget"; import { TimezoneSelectWidget } from "./widgets/TimezoneSelectWidget"; import { CameraPathWidget } from "./widgets/CameraPathWidget"; +import { OptionalFieldWidget } from "./widgets/OptionalFieldWidget"; import { FieldTemplate } from "./templates/FieldTemplate"; import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate"; @@ -73,6 +74,7 @@ export const frigateTheme: FrigateTheme = { audioLabels: AudioLabelSwitchesWidget, zoneNames: ZoneSwitchesWidget, timezoneSelect: TimezoneSelectWidget, + optionalField: OptionalFieldWidget, }, templates: { FieldTemplate: FieldTemplate as React.ComponentType, diff --git a/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx b/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx new file mode 100644 index 000000000..7f05d6466 --- /dev/null +++ b/web/src/components/config-form/theme/widgets/OptionalFieldWidget.tsx @@ -0,0 +1,64 @@ +// Optional Field Widget - wraps any inner widget with an enable/disable switch +// Used for nullable fields where None means "disabled" (not the same as 0) + +import type { WidgetProps } from "@rjsf/utils"; +import { getWidget } from "@rjsf/utils"; +import { Switch } from "@/components/ui/switch"; +import { cn } from "@/lib/utils"; +import { getNonNullSchema } from "../fields/nullableUtils"; + +export function OptionalFieldWidget(props: WidgetProps) { + const { id, value, disabled, readonly, onChange, schema, options, registry } = + props; + + const innerWidgetName = (options.innerWidget as string) || undefined; + const isEnabled = value !== undefined && value !== null; + + // Extract the non-null branch from anyOf [Type, null] + const innerSchema = getNonNullSchema(schema) ?? schema; + + const InnerWidget = getWidget(innerSchema, innerWidgetName, registry.widgets); + + const getDefaultValue = () => { + if (innerSchema.default !== undefined && innerSchema.default !== null) { + return innerSchema.default; + } + if (innerSchema.minimum !== undefined) { + return innerSchema.minimum; + } + if (innerSchema.type === "integer" || innerSchema.type === "number") { + return 0; + } + if (innerSchema.type === "string") { + return ""; + } + return 0; + }; + + const handleToggle = (checked: boolean) => { + onChange(checked ? getDefaultValue() : undefined); + }; + + const innerProps: WidgetProps = { + ...props, + schema: innerSchema, + disabled: disabled || readonly || !isEnabled, + value: isEnabled ? value : getDefaultValue(), + }; + + return ( +
+ +
+ +
+
+ ); +}