fix array field template

This commit is contained in:
Josh Hawkins 2026-01-27 14:49:32 -06:00
parent ab727089d2
commit 0b148d66f3
6 changed files with 105 additions and 63 deletions

View File

@ -1123,6 +1123,14 @@
} }
}, },
"configForm": { "configForm": {
"global": {
"title": "Global Settings",
"description": "These settings apply to all cameras unless overridden in the camera-specific settings."
},
"camera": {
"title": "Camera Settings",
"description": "These settings apply only to this camera and override the global settings."
},
"showAdvanced": "Show Advanced Settings", "showAdvanced": "Show Advanced Settings",
"tabs": { "tabs": {
"sharedDefaults": "Shared Defaults", "sharedDefaults": "Shared Defaults",

View File

@ -17,6 +17,7 @@ export const ReviewSection = createConfigSection({
"detections.labels", "detections.labels",
"detections.enabled_in_config", "detections.enabled_in_config",
"detections.required_zones", "detections.required_zones",
"genai.enabled_in_config",
], ],
advancedFields: [], advancedFields: [],
}, },

View File

@ -22,6 +22,7 @@ import { SwitchesWidget } from "./widgets/SwitchesWidget";
import { ObjectLabelSwitchesWidget } from "./widgets/ObjectLabelSwitchesWidget"; import { ObjectLabelSwitchesWidget } from "./widgets/ObjectLabelSwitchesWidget";
import { AudioLabelSwitchesWidget } from "./widgets/AudioLabelSwitchesWidget"; import { AudioLabelSwitchesWidget } from "./widgets/AudioLabelSwitchesWidget";
import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget"; import { ZoneSwitchesWidget } from "./widgets/ZoneSwitchesWidget";
import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
import { FieldTemplate } from "./templates/FieldTemplate"; import { FieldTemplate } from "./templates/FieldTemplate";
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate"; import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
@ -48,6 +49,7 @@ export const frigateTheme: FrigateTheme = {
PasswordWidget: PasswordWidget, PasswordWidget: PasswordWidget,
SelectWidget: SelectWidget, SelectWidget: SelectWidget,
CheckboxWidget: SwitchWidget, CheckboxWidget: SwitchWidget,
ArrayAsTextWidget: ArrayAsTextWidget,
// Custom widgets // Custom widgets
switch: SwitchWidget, switch: SwitchWidget,
password: PasswordWidget, password: PasswordWidget,

View File

@ -1,19 +1,10 @@
// Array Field Template - renders array fields with add/remove controls // Array Field Template - renders array fields with add/remove controls
import type { ArrayFieldTemplateProps } from "@rjsf/utils"; import type { ArrayFieldTemplateProps } from "@rjsf/utils";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card"; import { LuPlus } from "react-icons/lu";
import { LuPlus, LuTrash2, LuGripVertical } from "react-icons/lu";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface ArrayItem {
key: string;
index: number;
children: React.ReactNode;
hasRemove: boolean;
onDropIndexClick: (index: number) => () => void;
}
export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) { export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
const { items, canAdd, onAddClick, disabled, readonly, schema } = props; const { items, canAdd, onAddClick, disabled, readonly, schema } = props;
@ -36,52 +27,20 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
</p> </p>
)} )}
{(items as unknown as ArrayItem[]).map((element: ArrayItem) => ( {items.map((element, index) => {
<div // RJSF items are pre-rendered React elements, render them directly
key={element.key} return (
className={cn("flex items-start gap-2", !isSimpleType && "flex-col")} <div
> key={element.key || index}
{isSimpleType ? ( className={cn(
<div className="flex flex-1 items-center gap-2"> "flex items-start gap-2",
<LuGripVertical className="h-4 w-4 cursor-move text-muted-foreground" /> !isSimpleType && "flex-col",
<div className="flex-1">{element.children}</div> )}
{element.hasRemove && ( >
<Button {element}
type="button" </div>
variant="ghost" );
size="icon" })}
onClick={element.onDropIndexClick(element.index)}
disabled={disabled || readonly}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<LuTrash2 className="h-4 w-4" />
</Button>
)}
</div>
) : (
<Card className="w-full">
<CardContent className="pt-4">
<div className="flex items-start gap-2">
<LuGripVertical className="mt-2 h-4 w-4 cursor-move text-muted-foreground" />
<div className="flex-1">{element.children}</div>
{element.hasRemove && (
<Button
type="button"
variant="ghost"
size="icon"
onClick={element.onDropIndexClick(element.index)}
disabled={disabled || readonly}
className="h-8 w-8 text-destructive hover:text-destructive"
>
<LuTrash2 className="h-4 w-4" />
</Button>
)}
</div>
</CardContent>
</Card>
)}
</div>
))}
{canAdd && ( {canAdd && (
<Button <Button

View File

@ -0,0 +1,32 @@
// Widget that displays an array as a concatenated text string
import type { WidgetProps } from "@rjsf/utils";
import { Input } from "@/components/ui/input";
import { useCallback } from "react";
export function ArrayAsTextWidget(props: WidgetProps) {
const { value, onChange, disabled, readonly, placeholder } = props;
// Convert array to space-separated string
const textValue =
Array.isArray(value) && value.length > 0 ? value.join(" ") : "";
const handleChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
const newText = event.target.value;
// Convert space-separated string back to array
const newArray = newText.trim() ? newText.trim().split(/\s+/) : [];
onChange(newArray);
},
[onChange],
);
return (
<Input
value={textValue}
onChange={handleChange}
disabled={disabled}
readOnly={readonly}
placeholder={placeholder}
/>
);
}

View File

@ -62,6 +62,7 @@ const globalSectionConfigs: Record<
advancedFields?: string[]; advancedFields?: string[];
liveValidate?: boolean; liveValidate?: boolean;
i18nNamespace: string; i18nNamespace: string;
uiSchema?: Record<string, unknown>;
} }
> = { > = {
mqtt: { mqtt: {
@ -122,6 +123,11 @@ const globalSectionConfigs: Record<
"hash_iterations", "hash_iterations",
"roles", "roles",
], ],
uiSchema: {
reset_admin_password: {
"ui:widget": "switch",
},
},
}, },
tls: { tls: {
i18nNamespace: "config/tls", i18nNamespace: "config/tls",
@ -207,6 +213,44 @@ const globalSectionConfigs: Record<
"apple_compatibility", "apple_compatibility",
"gpu", "gpu",
], ],
uiSchema: {
global_args: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
},
hwaccel_args: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
},
input_args: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
},
output_args: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
detect: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
},
record: {
"ui:widget": "ArrayAsTextWidget",
"ui:options": {
suppressMultiSchema: true,
},
},
},
},
}, },
detectors: { detectors: {
i18nNamespace: "config/detectors", i18nNamespace: "config/detectors",
@ -247,10 +291,11 @@ const globalSectionConfigs: Record<
"runtime_options", "runtime_options",
], ],
advancedFields: ["base_url", "provider_options", "runtime_options"], advancedFields: ["base_url", "provider_options", "runtime_options"],
hiddenFields: ["genai.enabled_in_config"],
}, },
classification: { classification: {
i18nNamespace: "config/classification", i18nNamespace: "config/classification",
fieldOrder: ["bird", "custom"], hiddenFields: ["custom"],
advancedFields: [], advancedFields: [],
}, },
semantic_search: { semantic_search: {
@ -318,11 +363,6 @@ const globalSectionConfigs: Record<
"replace_rules", "replace_rules",
], ],
}, },
go2rtc: {
i18nNamespace: "config/go2rtc",
fieldOrder: [],
advancedFields: [],
},
}; };
// System sections (global only) // System sections (global only)
@ -341,7 +381,6 @@ const systemSections = [
"detectors", "detectors",
"model", "model",
"classification", "classification",
"go2rtc",
]; ];
// Integration sections (global only) // Integration sections (global only)
@ -481,6 +520,7 @@ function GlobalConfigSection({
hiddenFields={sectionConfig.hiddenFields} hiddenFields={sectionConfig.hiddenFields}
advancedFields={sectionConfig.advancedFields} advancedFields={sectionConfig.advancedFields}
liveValidate={sectionConfig.liveValidate} liveValidate={sectionConfig.liveValidate}
uiSchema={sectionConfig.uiSchema}
showSubmit={false} showSubmit={false}
i18nNamespace={sectionConfig.i18nNamespace} i18nNamespace={sectionConfig.i18nNamespace}
disabled={isSaving} disabled={isSaving}