From 223eb89dc4f5586e69f472affc50273e5327d32f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 24 Jan 2026 09:42:59 -0600 Subject: [PATCH] fix nullable fields --- .../config-form/theme/fields/nullableUtils.ts | 47 ++++++++----------- .../theme/templates/FieldTemplate.tsx | 16 ++++--- .../templates/MultiSchemaFieldTemplate.tsx | 4 +- web/src/hooks/use-config-override.ts | 47 +++++++++++++++---- 4 files changed, 69 insertions(+), 45 deletions(-) diff --git a/web/src/components/config-form/theme/fields/nullableUtils.ts b/web/src/components/config-form/theme/fields/nullableUtils.ts index db1d891fd..ed3939b88 100644 --- a/web/src/components/config-form/theme/fields/nullableUtils.ts +++ b/web/src/components/config-form/theme/fields/nullableUtils.ts @@ -2,24 +2,19 @@ import type { StrictRJSFSchema } from "@rjsf/utils"; /** - * Checks if a schema is anyOf with exactly [PrimitiveType, null] - * where the primitive has no additional constraints + * Checks if a schema is anyOf/oneOf with exactly [Type, null]. + * This indicates a nullable field in Pydantic schemas. */ -export function isSimpleNullableField(schema: StrictRJSFSchema): boolean { - if ( - !schema.anyOf || - !Array.isArray(schema.anyOf) || - schema.anyOf.length !== 2 - ) { +export function isNullableUnionSchema(schema: StrictRJSFSchema): boolean { + const union = schema.anyOf ?? schema.oneOf; + if (!union || !Array.isArray(union) || union.length !== 2) { return false; } - const items = schema.anyOf; let hasNull = false; - let simpleType: StrictRJSFSchema | null = null; + let nonNullCount = 0; - // eslint-disable-next-line no-restricted-syntax - for (const item of items) { + for (const item of union) { if (typeof item !== "object" || item === null) { return false; } @@ -28,22 +23,19 @@ export function isSimpleNullableField(schema: StrictRJSFSchema): boolean { if (itemSchema.type === "null") { hasNull = true; - } else if ( - itemSchema.type && - !("$ref" in itemSchema) && - !("additionalProperties" in itemSchema) && - !("items" in itemSchema) && - !("pattern" in itemSchema) && - !("minimum" in itemSchema) && - !("maximum" in itemSchema) && - !("exclusiveMinimum" in itemSchema) && - !("exclusiveMaximum" in itemSchema) - ) { - simpleType = itemSchema; + } else { + nonNullCount += 1; } } - return hasNull && simpleType !== null; + return hasNull && nonNullCount === 1; +} + +/** + * Backwards-compatible alias for nullable fields + */ +export function isSimpleNullableField(schema: StrictRJSFSchema): boolean { + return isNullableUnionSchema(schema); } /** @@ -52,12 +44,13 @@ export function isSimpleNullableField(schema: StrictRJSFSchema): boolean { export function getNonNullSchema( schema: StrictRJSFSchema, ): StrictRJSFSchema | null { - if (!schema.anyOf || !Array.isArray(schema.anyOf)) { + const union = schema.anyOf ?? schema.oneOf; + if (!union || !Array.isArray(union)) { return null; } return ( - (schema.anyOf.find( + (union.find( (item) => typeof item === "object" && item !== null && diff --git a/web/src/components/config-form/theme/templates/FieldTemplate.tsx b/web/src/components/config-form/theme/templates/FieldTemplate.tsx index 9f4f06e0d..0d988f1fb 100644 --- a/web/src/components/config-form/theme/templates/FieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/FieldTemplate.tsx @@ -1,8 +1,9 @@ // Field Template - wraps each form field with label and description -import type { FieldTemplateProps } from "@rjsf/utils"; +import type { FieldTemplateProps, StrictRJSFSchema } from "@rjsf/utils"; import { Label } from "@/components/ui/label"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; +import { isNullableUnionSchema } from "../fields/nullableUtils"; /** * Build the i18n translation key path for nested fields using the field path @@ -47,6 +48,8 @@ export function FieldTemplate(props: FieldTemplateProps) { // Boolean fields (switches) render label inline const isBoolean = schema.type === "boolean"; + const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema); + // Get translation path for this field const translationPath = buildTranslationPath(fieldPathId.path); @@ -99,8 +102,9 @@ export function FieldTemplate(props: FieldTemplateProps) { isAdvanced && "border-l-2 border-muted pl-4", isBoolean && "flex items-center justify-between gap-4", )} + data-field-id={translationPath} > - {displayLabel && finalLabel && !isBoolean && ( + {displayLabel && finalLabel && !isBoolean && !isNullableUnion && ( )} - {finalDescription && ( + {finalDescription && !isNullableUnion && (
{String(finalDescription)}
@@ -132,10 +136,8 @@ export function FieldTemplate(props: FieldTemplateProps) { ) : ( <> - {finalDescription && ( -- {String(finalDescription)} -
+ {finalDescription && !isNullableUnion && ( +{finalDescription}
)} {children} > diff --git a/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx b/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx index 4760f47ab..a21fe5068 100644 --- a/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/MultiSchemaFieldTemplate.tsx @@ -6,7 +6,7 @@ import type { StrictRJSFSchema, FormContextType, } from "@rjsf/utils"; -import { isSimpleNullableField } from "../fields/nullableUtils"; +import { isNullableUnionSchema } from "../fields/nullableUtils"; /** * Custom MultiSchemaFieldTemplate that: @@ -23,7 +23,7 @@ export function MultiSchemaFieldTemplate< const { schema, selector, optionSchemaField } = props; // Check if this is a simple nullable field that should be handled specially - if (isSimpleNullableField(schema)) { + if (isNullableUnionSchema(schema)) { // For simple nullable fields, just render the field directly without the dropdown selector // This handles the case where empty input = null return <>{optionSchemaField}>; diff --git a/web/src/hooks/use-config-override.ts b/web/src/hooks/use-config-override.ts index 9478fac81..c6fc41975 100644 --- a/web/src/hooks/use-config-override.ts +++ b/web/src/hooks/use-config-override.ts @@ -4,6 +4,32 @@ import isEqual from "lodash/isEqual"; import get from "lodash/get"; import type { FrigateConfig } from "@/types/frigateConfig"; +const INTERNAL_FIELD_SUFFIXES = ["enabled_in_config", "raw_mask"]; + +function stripInternalFields(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map(stripInternalFields); + } + + if (value && typeof value === "object") { + const obj = value as Record