mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-13 03:47:34 +03:00
fix nullable fields
This commit is contained in:
parent
6a664f4624
commit
223eb89dc4
@ -2,24 +2,19 @@
|
|||||||
import type { StrictRJSFSchema } from "@rjsf/utils";
|
import type { StrictRJSFSchema } from "@rjsf/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a schema is anyOf with exactly [PrimitiveType, null]
|
* Checks if a schema is anyOf/oneOf with exactly [Type, null].
|
||||||
* where the primitive has no additional constraints
|
* This indicates a nullable field in Pydantic schemas.
|
||||||
*/
|
*/
|
||||||
export function isSimpleNullableField(schema: StrictRJSFSchema): boolean {
|
export function isNullableUnionSchema(schema: StrictRJSFSchema): boolean {
|
||||||
if (
|
const union = schema.anyOf ?? schema.oneOf;
|
||||||
!schema.anyOf ||
|
if (!union || !Array.isArray(union) || union.length !== 2) {
|
||||||
!Array.isArray(schema.anyOf) ||
|
|
||||||
schema.anyOf.length !== 2
|
|
||||||
) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const items = schema.anyOf;
|
|
||||||
let hasNull = false;
|
let hasNull = false;
|
||||||
let simpleType: StrictRJSFSchema | null = null;
|
let nonNullCount = 0;
|
||||||
|
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
for (const item of union) {
|
||||||
for (const item of items) {
|
|
||||||
if (typeof item !== "object" || item === null) {
|
if (typeof item !== "object" || item === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -28,22 +23,19 @@ export function isSimpleNullableField(schema: StrictRJSFSchema): boolean {
|
|||||||
|
|
||||||
if (itemSchema.type === "null") {
|
if (itemSchema.type === "null") {
|
||||||
hasNull = true;
|
hasNull = true;
|
||||||
} else if (
|
} else {
|
||||||
itemSchema.type &&
|
nonNullCount += 1;
|
||||||
!("$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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
export function getNonNullSchema(
|
||||||
schema: StrictRJSFSchema,
|
schema: StrictRJSFSchema,
|
||||||
): StrictRJSFSchema | null {
|
): StrictRJSFSchema | null {
|
||||||
if (!schema.anyOf || !Array.isArray(schema.anyOf)) {
|
const union = schema.anyOf ?? schema.oneOf;
|
||||||
|
if (!union || !Array.isArray(union)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(schema.anyOf.find(
|
(union.find(
|
||||||
(item) =>
|
(item) =>
|
||||||
typeof item === "object" &&
|
typeof item === "object" &&
|
||||||
item !== null &&
|
item !== null &&
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
// Field Template - wraps each form field with label and description
|
// 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 { Label } from "@/components/ui/label";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
import { isNullableUnionSchema } from "../fields/nullableUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the i18n translation key path for nested fields using the field path
|
* 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
|
// Boolean fields (switches) render label inline
|
||||||
const isBoolean = schema.type === "boolean";
|
const isBoolean = schema.type === "boolean";
|
||||||
|
|
||||||
|
const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema);
|
||||||
|
|
||||||
// Get translation path for this field
|
// Get translation path for this field
|
||||||
const translationPath = buildTranslationPath(fieldPathId.path);
|
const translationPath = buildTranslationPath(fieldPathId.path);
|
||||||
|
|
||||||
@ -99,8 +102,9 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
isAdvanced && "border-l-2 border-muted pl-4",
|
isAdvanced && "border-l-2 border-muted pl-4",
|
||||||
isBoolean && "flex items-center justify-between gap-4",
|
isBoolean && "flex items-center justify-between gap-4",
|
||||||
)}
|
)}
|
||||||
|
data-field-id={translationPath}
|
||||||
>
|
>
|
||||||
{displayLabel && finalLabel && !isBoolean && (
|
{displayLabel && finalLabel && !isBoolean && !isNullableUnion && (
|
||||||
<Label
|
<Label
|
||||||
htmlFor={id}
|
htmlFor={id}
|
||||||
className={cn(
|
className={cn(
|
||||||
@ -122,7 +126,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
{required && <span className="ml-1 text-destructive">*</span>}
|
{required && <span className="ml-1 text-destructive">*</span>}
|
||||||
</Label>
|
</Label>
|
||||||
)}
|
)}
|
||||||
{finalDescription && (
|
{finalDescription && !isNullableUnion && (
|
||||||
<p className="max-w-md text-sm text-muted-foreground">
|
<p className="max-w-md text-sm text-muted-foreground">
|
||||||
{String(finalDescription)}
|
{String(finalDescription)}
|
||||||
</p>
|
</p>
|
||||||
@ -132,10 +136,8 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{finalDescription && (
|
{finalDescription && !isNullableUnion && (
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">{finalDescription}</p>
|
||||||
{String(finalDescription)}
|
|
||||||
</p>
|
|
||||||
)}
|
)}
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import type {
|
|||||||
StrictRJSFSchema,
|
StrictRJSFSchema,
|
||||||
FormContextType,
|
FormContextType,
|
||||||
} from "@rjsf/utils";
|
} from "@rjsf/utils";
|
||||||
import { isSimpleNullableField } from "../fields/nullableUtils";
|
import { isNullableUnionSchema } from "../fields/nullableUtils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom MultiSchemaFieldTemplate that:
|
* Custom MultiSchemaFieldTemplate that:
|
||||||
@ -23,7 +23,7 @@ export function MultiSchemaFieldTemplate<
|
|||||||
const { schema, selector, optionSchemaField } = props;
|
const { schema, selector, optionSchemaField } = props;
|
||||||
|
|
||||||
// Check if this is a simple nullable field that should be handled specially
|
// 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
|
// For simple nullable fields, just render the field directly without the dropdown selector
|
||||||
// This handles the case where empty input = null
|
// This handles the case where empty input = null
|
||||||
return <>{optionSchemaField}</>;
|
return <>{optionSchemaField}</>;
|
||||||
|
|||||||
@ -4,6 +4,32 @@ import isEqual from "lodash/isEqual";
|
|||||||
import get from "lodash/get";
|
import get from "lodash/get";
|
||||||
import type { FrigateConfig } from "@/types/frigateConfig";
|
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<string, unknown>;
|
||||||
|
const cleaned: Record<string, unknown> = {};
|
||||||
|
for (const [key, val] of Object.entries(obj)) {
|
||||||
|
if (INTERNAL_FIELD_SUFFIXES.some((suffix) => key.endsWith(suffix))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
cleaned[key] = stripInternalFields(val);
|
||||||
|
}
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeConfigValue(value: unknown): unknown {
|
||||||
|
return stripInternalFields(value);
|
||||||
|
}
|
||||||
|
|
||||||
export interface OverrideStatus {
|
export interface OverrideStatus {
|
||||||
/** Whether the field is overridden from global */
|
/** Whether the field is overridden from global */
|
||||||
isOverridden: boolean;
|
isOverridden: boolean;
|
||||||
@ -98,15 +124,18 @@ export function useConfigOverride({
|
|||||||
|
|
||||||
const cameraValue = get(cameraConfig, sectionPath);
|
const cameraValue = get(cameraConfig, sectionPath);
|
||||||
|
|
||||||
|
const normalizedGlobalValue = normalizeConfigValue(globalValue);
|
||||||
|
const normalizedCameraValue = normalizeConfigValue(cameraValue);
|
||||||
|
|
||||||
// Check if the entire section is overridden
|
// Check if the entire section is overridden
|
||||||
const isOverridden = !isEqual(globalValue, cameraValue);
|
const isOverridden = !isEqual(normalizedGlobalValue, normalizedCameraValue);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get override status for a specific field within the section
|
* Get override status for a specific field within the section
|
||||||
*/
|
*/
|
||||||
const getFieldOverride = (fieldPath: string): OverrideStatus => {
|
const getFieldOverride = (fieldPath: string): OverrideStatus => {
|
||||||
const globalFieldValue = get(globalValue, fieldPath);
|
const globalFieldValue = get(normalizedGlobalValue, fieldPath);
|
||||||
const cameraFieldValue = get(cameraValue, fieldPath);
|
const cameraFieldValue = get(normalizedCameraValue, fieldPath);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOverridden: !isEqual(globalFieldValue, cameraFieldValue),
|
isOverridden: !isEqual(globalFieldValue, cameraFieldValue),
|
||||||
@ -120,15 +149,15 @@ export function useConfigOverride({
|
|||||||
*/
|
*/
|
||||||
const resetToGlobal = (fieldPath?: string) => {
|
const resetToGlobal = (fieldPath?: string) => {
|
||||||
if (fieldPath) {
|
if (fieldPath) {
|
||||||
return get(globalValue, fieldPath);
|
return get(normalizedGlobalValue, fieldPath);
|
||||||
}
|
}
|
||||||
return globalValue;
|
return normalizedGlobalValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
isOverridden,
|
isOverridden,
|
||||||
globalValue,
|
globalValue: normalizedGlobalValue,
|
||||||
cameraValue,
|
cameraValue: normalizedCameraValue,
|
||||||
getFieldOverride,
|
getFieldOverride,
|
||||||
resetToGlobal,
|
resetToGlobal,
|
||||||
};
|
};
|
||||||
@ -169,8 +198,8 @@ export function useAllCameraOverrides(
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const section of sectionsToCheck) {
|
for (const section of sectionsToCheck) {
|
||||||
const globalValue = get(config, section);
|
const globalValue = normalizeConfigValue(get(config, section));
|
||||||
const cameraValue = get(cameraConfig, section);
|
const cameraValue = normalizeConfigValue(get(cameraConfig, section));
|
||||||
|
|
||||||
if (!isEqual(globalValue, cameraValue)) {
|
if (!isEqual(globalValue, cameraValue)) {
|
||||||
overriddenSections.push(section);
|
overriddenSections.push(section);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user