mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-03-10 10:33:11 +03:00
add array field item template and fix ffmpeg section
This commit is contained in:
parent
8c65cbce22
commit
40b59c5d0c
@ -1,5 +1,5 @@
|
||||
{
|
||||
"label": "Object tracking",
|
||||
"label": "Object Detection",
|
||||
"description": "Settings for the detection/detect role used to run object detection and initialize trackers.",
|
||||
"groups": {
|
||||
"resolution": "Resolution",
|
||||
|
||||
@ -38,6 +38,100 @@ export const FfmpegSection = createConfigSection({
|
||||
"apple_compatibility",
|
||||
"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,
|
||||
},
|
||||
},
|
||||
items: {
|
||||
detect: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
record: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
inputs: {
|
||||
items: {
|
||||
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,
|
||||
},
|
||||
items: {
|
||||
detect: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
record: {
|
||||
"ui:widget": "ArrayAsTextWidget",
|
||||
"ui:options": {
|
||||
suppressMultiSchema: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import type {
|
||||
RegistryFieldsType,
|
||||
TemplatesType,
|
||||
} from "@rjsf/utils";
|
||||
import { getDefaultRegistry } from "@rjsf/core";
|
||||
|
||||
import { SwitchWidget } from "./widgets/SwitchWidget";
|
||||
import { SelectWidget } from "./widgets/SelectWidget";
|
||||
@ -27,6 +26,7 @@ import { ArrayAsTextWidget } from "./widgets/ArrayAsTextWidget";
|
||||
import { FieldTemplate } from "./templates/FieldTemplate";
|
||||
import { ObjectFieldTemplate } from "./templates/ObjectFieldTemplate";
|
||||
import { ArrayFieldTemplate } from "./templates/ArrayFieldTemplate";
|
||||
import { ArrayFieldItemTemplate } from "./templates/ArrayFieldItemTemplate";
|
||||
import { BaseInputTemplate } from "./templates/BaseInputTemplate";
|
||||
import { DescriptionFieldTemplate } from "./templates/DescriptionFieldTemplate";
|
||||
import { TitleFieldTemplate } from "./templates/TitleFieldTemplate";
|
||||
@ -39,11 +39,8 @@ export interface FrigateTheme {
|
||||
fields: RegistryFieldsType;
|
||||
}
|
||||
|
||||
const defaultRegistry = getDefaultRegistry();
|
||||
|
||||
export const frigateTheme: FrigateTheme = {
|
||||
widgets: {
|
||||
...defaultRegistry.widgets,
|
||||
// Override default widgets with shadcn/ui styled versions
|
||||
TextWidget: TextWidget,
|
||||
PasswordWidget: PasswordWidget,
|
||||
@ -64,20 +61,15 @@ export const frigateTheme: FrigateTheme = {
|
||||
zoneNames: ZoneSwitchesWidget,
|
||||
},
|
||||
templates: {
|
||||
...defaultRegistry.templates,
|
||||
FieldTemplate: FieldTemplate as React.ComponentType<FieldTemplateProps>,
|
||||
ObjectFieldTemplate: ObjectFieldTemplate,
|
||||
ArrayFieldTemplate: ArrayFieldTemplate,
|
||||
ArrayFieldItemTemplate: ArrayFieldItemTemplate,
|
||||
BaseInputTemplate: BaseInputTemplate as React.ComponentType<WidgetProps>,
|
||||
DescriptionFieldTemplate: DescriptionFieldTemplate,
|
||||
TitleFieldTemplate: TitleFieldTemplate,
|
||||
ErrorListTemplate: ErrorListTemplate,
|
||||
MultiSchemaFieldTemplate: MultiSchemaFieldTemplate,
|
||||
ButtonTemplates: {
|
||||
...defaultRegistry.templates.ButtonTemplates,
|
||||
},
|
||||
},
|
||||
fields: {
|
||||
...defaultRegistry.fields,
|
||||
},
|
||||
fields: {},
|
||||
};
|
||||
|
||||
@ -0,0 +1,58 @@
|
||||
import type {
|
||||
ArrayFieldItemTemplateProps,
|
||||
FormContextType,
|
||||
RJSFSchema,
|
||||
StrictRJSFSchema,
|
||||
} from "@rjsf/utils";
|
||||
import { getTemplate, getUiOptions } from "@rjsf/utils";
|
||||
|
||||
/**
|
||||
* Custom ArrayFieldItemTemplate to ensure array item content uses full width
|
||||
* while keeping action buttons aligned to the right.
|
||||
*/
|
||||
export function ArrayFieldItemTemplate<
|
||||
T = unknown,
|
||||
S extends StrictRJSFSchema = RJSFSchema,
|
||||
F extends FormContextType = FormContextType,
|
||||
>(props: ArrayFieldItemTemplateProps<T, S, F>) {
|
||||
const {
|
||||
children,
|
||||
buttonsProps,
|
||||
displayLabel,
|
||||
hasDescription,
|
||||
hasToolbar,
|
||||
uiSchema,
|
||||
registry,
|
||||
} = props;
|
||||
|
||||
const uiOptions = getUiOptions<T, S, F>(uiSchema);
|
||||
const ArrayFieldItemButtonsTemplate = getTemplate<
|
||||
"ArrayFieldItemButtonsTemplate",
|
||||
T,
|
||||
S,
|
||||
F
|
||||
>("ArrayFieldItemButtonsTemplate", registry, uiOptions);
|
||||
|
||||
const margin = hasDescription ? -6 : 22;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="mb-2 flex w-full flex-row items-start gap-2">
|
||||
<div className="min-w-0 flex-1">{children}</div>
|
||||
{hasToolbar && (
|
||||
<div
|
||||
className="flex shrink-0 items-start justify-end gap-2"
|
||||
style={{
|
||||
marginLeft: "5px",
|
||||
marginTop: displayLabel ? `${margin}px` : undefined,
|
||||
}}
|
||||
>
|
||||
<ArrayFieldItemButtonsTemplate {...buttonsProps} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ArrayFieldItemTemplate;
|
||||
@ -33,7 +33,7 @@ export function ArrayFieldTemplate(props: ArrayFieldTemplateProps) {
|
||||
<div
|
||||
key={element.key || index}
|
||||
className={cn(
|
||||
"flex items-start gap-2",
|
||||
"flex w-full items-start gap-2",
|
||||
!isSimpleType && "flex-col",
|
||||
)}
|
||||
>
|
||||
|
||||
@ -43,11 +43,11 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
|
||||
// Get UI options
|
||||
const uiOptions = uiSchema?.["ui:options"] || {};
|
||||
|
||||
// Determine field characteristics
|
||||
const isAdvanced = uiOptions.advanced === true;
|
||||
|
||||
// Boolean fields (switches) render label inline
|
||||
const isBoolean = schema.type === "boolean";
|
||||
|
||||
const isObjectField = schema.type === "object";
|
||||
const isNullableUnion = isNullableUnionSchema(schema as StrictRJSFSchema);
|
||||
const suppressMultiSchema =
|
||||
(uiSchema?.["ui:options"] as Record<string, unknown> | undefined)
|
||||
@ -144,7 +144,7 @@ export function FieldTemplate(props: FieldTemplateProps) {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{finalDescription && !isMultiSchemaWrapper && (
|
||||
{finalDescription && !isMultiSchemaWrapper && !isObjectField && (
|
||||
<p className="text-xs text-muted-foreground">{finalDescription}</p>
|
||||
)}
|
||||
{children}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
// Object Field Template - renders nested object fields
|
||||
// Object Field Template - renders nested object fields with i18n support
|
||||
import type { ObjectFieldTemplateProps } from "@rjsf/utils";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
@ -14,21 +14,15 @@ import { cn } from "@/lib/utils";
|
||||
|
||||
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const { title, description, properties, uiSchema, registry, schema } = props;
|
||||
const { idSchema } = props as ObjectFieldTemplateProps & {
|
||||
idSchema?: { $id?: string };
|
||||
};
|
||||
const formContext = (props as Record<string, unknown>).formContext as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
type FormContext = { i18nNamespace?: string };
|
||||
const formContext = registry?.formContext as FormContext | undefined;
|
||||
|
||||
// Check if this is a root-level object
|
||||
const isRoot = idSchema?.$id === "root" || registry?.rootSchema === schema;
|
||||
const isRoot = registry?.rootSchema === schema;
|
||||
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const { t } = useTranslation([
|
||||
(formContext?.i18nNamespace as string | undefined) || "common",
|
||||
]);
|
||||
const { t } = useTranslation([formContext?.i18nNamespace || "common"]);
|
||||
|
||||
const groupDefinitions =
|
||||
(uiSchema?.["ui:groups"] as Record<string, string[]> | undefined) || {};
|
||||
@ -51,6 +45,52 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
const toTitle = (value: string) =>
|
||||
value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
|
||||
|
||||
// Get the property name from the field path (e.g., "alerts" from path)
|
||||
const fieldPathId = (
|
||||
props as { fieldPathId?: { path?: (string | number)[] } }
|
||||
).fieldPathId;
|
||||
let propertyName: string | undefined;
|
||||
const path = fieldPathId?.path;
|
||||
if (path) {
|
||||
for (let i = path.length - 1; i >= 0; i -= 1) {
|
||||
const segment = path[i];
|
||||
if (typeof segment === "string") {
|
||||
propertyName = segment;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Try i18n translation, fall back to schema or original values
|
||||
const i18nNs = formContext?.i18nNamespace;
|
||||
|
||||
let inferredLabel: string | undefined;
|
||||
if (i18nNs && propertyName) {
|
||||
const translated = t(`${propertyName}.label`, {
|
||||
ns: i18nNs,
|
||||
defaultValue: "",
|
||||
});
|
||||
inferredLabel = translated || undefined;
|
||||
}
|
||||
const schemaTitle = schema?.title;
|
||||
const fallbackLabel =
|
||||
title || schemaTitle || (propertyName ? toTitle(propertyName) : undefined);
|
||||
inferredLabel = inferredLabel ?? fallbackLabel;
|
||||
|
||||
let inferredDescription: string | undefined;
|
||||
if (i18nNs && propertyName) {
|
||||
const translated = t(`${propertyName}.description`, {
|
||||
ns: i18nNs,
|
||||
defaultValue: "",
|
||||
});
|
||||
inferredDescription = translated || undefined;
|
||||
}
|
||||
const schemaDescription = schema?.description;
|
||||
const fallbackDescription =
|
||||
(typeof description === "string" ? description : undefined) ||
|
||||
schemaDescription;
|
||||
inferredDescription = inferredDescription ?? fallbackDescription;
|
||||
|
||||
const renderGroupedFields = (items: (typeof properties)[number][]) => {
|
||||
if (!items.length) {
|
||||
return null;
|
||||
@ -142,16 +182,16 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
|
||||
|
||||
// Nested objects render as collapsible cards
|
||||
return (
|
||||
<Card>
|
||||
<Card className="w-full">
|
||||
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
||||
<CollapsibleTrigger asChild>
|
||||
<CardHeader className="cursor-pointer transition-colors hover:bg-muted/50">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle className="text-base">{title}</CardTitle>
|
||||
{description && (
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{description}
|
||||
<CardTitle className="text-sm">{inferredLabel}</CardTitle>
|
||||
{inferredDescription && (
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{inferredDescription}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -6,9 +6,13 @@ 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(" ") : "";
|
||||
// Convert array or string to text
|
||||
let textValue = "";
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
textValue = value;
|
||||
} else if (Array.isArray(value) && value.length > 0) {
|
||||
textValue = value.join(" ");
|
||||
}
|
||||
|
||||
const handleChange = useCallback(
|
||||
(event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
@ -325,7 +325,7 @@ const CameraConfigContent = memo(function CameraConfigContent({
|
||||
return (
|
||||
<div className="flex flex-1 gap-6 overflow-hidden">
|
||||
{/* Section Navigation */}
|
||||
<nav className="w-48 shrink-0">
|
||||
<nav className="w-64 shrink-0">
|
||||
<ul className="space-y-1">
|
||||
{sections.map((section) => {
|
||||
const isOverridden = overriddenSections.includes(section.key);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user