frigate/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx

193 lines
6.2 KiB
TypeScript
Raw Normal View History

// Object Field Template - renders nested object fields
import type { ObjectFieldTemplateProps } from "@rjsf/utils";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { LuChevronDown, LuChevronRight } from "react-icons/lu";
2026-01-26 02:29:52 +03:00
import { useTranslation } from "react-i18next";
import { cn } from "@/lib/utils";
export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
2026-01-26 02:29:52 +03:00
const { title, description, properties, uiSchema } = props;
const formContext = (props as Record<string, unknown>).formContext as
| Record<string, unknown>
| undefined;
// Check if this is a root-level object
const isRoot = !title;
const [isOpen, setIsOpen] = useState(true);
2026-01-26 02:29:52 +03:00
const { t } = useTranslation([
(formContext?.i18nNamespace as string | undefined) || "common",
]);
const groupDefinitions =
(uiSchema?.["ui:groups"] as Record<string, string[]> | undefined) || {};
const isHiddenProp = (prop: (typeof properties)[number]) =>
prop.content.props.uiSchema?.["ui:widget"] === "hidden";
const visibleProps = properties.filter((prop) => !isHiddenProp(prop));
// Check for advanced section grouping
2026-01-26 02:29:52 +03:00
const advancedProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced === true,
);
2026-01-26 02:29:52 +03:00
const regularProps = visibleProps.filter(
(p) => p.content.props.uiSchema?.["ui:options"]?.advanced !== true,
);
const [showAdvanced, setShowAdvanced] = useState(false);
2026-01-26 02:29:52 +03:00
const toTitle = (value: string) =>
value.replace(/_/g, " ").replace(/\b\w/g, (char) => char.toUpperCase());
const renderGroupedFields = (items: (typeof properties)[number][]) => {
if (!items.length) {
return null;
}
const grouped = new Set<string>();
const groups = Object.entries(groupDefinitions)
.map(([groupKey, fields]) => {
const ordered = fields
.map((field) => items.find((item) => item.name === field))
.filter(Boolean) as (typeof properties)[number][];
if (ordered.length === 0) {
return null;
}
ordered.forEach((item) => grouped.add(item.name));
const label = t(`groups.${groupKey}`, {
defaultValue: toTitle(groupKey),
});
return {
key: groupKey,
label,
items: ordered,
};
})
.filter(Boolean) as Array<{
key: string;
label: string;
items: (typeof properties)[number][];
}>;
const ungrouped = items.filter((item) => !grouped.has(item.name));
return (
<div className="space-y-6">
{groups.map((group) => (
<div key={group.key} className="space-y-4">
<div className="text-sm font-medium text-muted-foreground">
{group.label}
</div>
<div className="space-y-4">
{group.items.map((element) => (
<div key={element.name}>{element.content}</div>
))}
</div>
</div>
))}
{ungrouped.length > 0 && (
<div className={cn("space-y-4", groups.length > 0 && "pt-2")}>
{ungrouped.map((element) => (
<div key={element.name}>{element.content}</div>
))}
</div>
)}
</div>
);
};
// Root level renders children directly
if (isRoot) {
return (
<div className="space-y-6">
2026-01-26 02:29:52 +03:00
{renderGroupedFields(regularProps)}
{advancedProps.length > 0 && (
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
<CollapsibleTrigger asChild>
<Button variant="ghost" className="w-full justify-start gap-2">
{showAdvanced ? (
<LuChevronDown className="h-4 w-4" />
) : (
<LuChevronRight className="h-4 w-4" />
)}
Advanced Settings ({advancedProps.length})
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
2026-01-26 02:29:52 +03:00
{renderGroupedFields(advancedProps)}
</CollapsibleContent>
</Collapsible>
)}
</div>
);
}
// Nested objects render as collapsible cards
return (
<Card>
<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}
</p>
)}
</div>
{isOpen ? (
<LuChevronDown className="h-4 w-4" />
) : (
<LuChevronRight className="h-4 w-4" />
)}
</div>
</CardHeader>
</CollapsibleTrigger>
<CollapsibleContent>
<CardContent className="space-y-4 pt-0">
2026-01-26 02:29:52 +03:00
{renderGroupedFields(regularProps)}
{advancedProps.length > 0 && (
<Collapsible open={showAdvanced} onOpenChange={setShowAdvanced}>
<CollapsibleTrigger asChild>
<Button
variant="ghost"
size="sm"
className="w-full justify-start gap-2"
>
{showAdvanced ? (
<LuChevronDown className="h-4 w-4" />
) : (
<LuChevronRight className="h-4 w-4" />
)}
Advanced ({advancedProps.length})
</Button>
</CollapsibleTrigger>
<CollapsibleContent className="space-y-4 pt-2">
2026-01-26 02:29:52 +03:00
{renderGroupedFields(advancedProps)}
</CollapsibleContent>
</Collapsible>
)}
</CardContent>
</CollapsibleContent>
</Collapsible>
</Card>
);
}