tweak grouping to prioritize fieldOrder before groups

previously, groups were always rendered first. now fieldOrder is respected, and any fields in a group will cause the group and all the fields in that group to be rendered in order. this allows moving the enabled switches to the top of the section
This commit is contained in:
Josh Hawkins 2026-03-20 07:55:25 -05:00
parent b4fb90bcbd
commit 705becba8c
8 changed files with 62 additions and 63 deletions

View File

@ -13,7 +13,7 @@ const audio: SectionConfigOverrides = {
"num_threads", "num_threads",
], ],
fieldGroups: { fieldGroups: {
detection: ["enabled", "listen", "filters"], detection: ["listen", "filters"],
sensitivity: ["min_volume", "max_not_heard"], sensitivity: ["min_volume", "max_not_heard"],
}, },
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],

View File

@ -18,7 +18,7 @@ const detect: SectionConfigOverrides = {
], ],
restartRequired: [], restartRequired: [],
fieldGroups: { fieldGroups: {
resolution: ["enabled", "width", "height", "fps"], resolution: ["width", "height", "fps"],
tracking: ["min_initialized", "max_disappeared"], tracking: ["min_initialized", "max_disappeared"],
}, },
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],

View File

@ -6,7 +6,7 @@ const faceRecognition: SectionConfigOverrides = {
restartRequired: [], restartRequired: [],
fieldOrder: ["enabled", "min_area"], fieldOrder: ["enabled", "min_area"],
hiddenFields: [], hiddenFields: [],
advancedFields: ["min_area"], advancedFields: [],
overrideFields: ["enabled", "min_area"], overrideFields: ["enabled", "min_area"],
}, },
global: { global: {

View File

@ -7,9 +7,9 @@ const lpr: SectionConfigOverrides = {
enhancement: "/configuration/license_plate_recognition#enhancement", enhancement: "/configuration/license_plate_recognition#enhancement",
}, },
restartRequired: [], restartRequired: [],
fieldOrder: ["enabled", "expire_time", "min_area", "enhancement"], fieldOrder: ["enabled", "min_area", "enhancement", "expire_time"],
hiddenFields: [], hiddenFields: [],
advancedFields: ["expire_time", "min_area", "enhancement"], advancedFields: ["expire_time", "enhancement"],
overrideFields: ["enabled", "min_area", "enhancement"], overrideFields: ["enabled", "min_area", "enhancement"],
}, },
global: { global: {

View File

@ -23,7 +23,7 @@ const motion: SectionConfigOverrides = {
"mqtt_off_delay", "mqtt_off_delay",
], ],
fieldGroups: { fieldGroups: {
sensitivity: ["enabled", "threshold", "contour_area"], sensitivity: ["threshold", "contour_area"],
algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"], algorithm: ["improve_contrast", "delta_alpha", "frame_alpha"],
}, },
uiSchema: { uiSchema: {

View File

@ -15,7 +15,7 @@ const record: SectionConfigOverrides = {
"export", "export",
], ],
fieldGroups: { fieldGroups: {
retention: ["enabled", "continuous", "motion"], retention: ["continuous", "motion"],
events: ["alerts", "detections"], events: ["alerts", "detections"],
}, },
hiddenFields: ["enabled_in_config", "sync_recordings"], hiddenFields: ["enabled_in_config", "sync_recordings"],

View File

@ -13,7 +13,7 @@ const snapshots: SectionConfigOverrides = {
"retain", "retain",
], ],
fieldGroups: { fieldGroups: {
display: ["enabled", "bounding_box", "crop", "quality", "timestamp"], display: ["bounding_box", "crop", "quality", "timestamp"],
}, },
hiddenFields: ["enabled_in_config"], hiddenFields: ["enabled_in_config"],
advancedFields: ["height", "quality", "retain"], advancedFields: ["height", "quality", "retain"],

View File

@ -311,51 +311,54 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
return null; return null;
} }
const grouped = new Set<string>(); // Build a lookup: field name → group info
const groups = Object.entries(groupDefinitions) const fieldToGroup = new Map<
.map(([groupKey, fields]) => { string,
const ordered = fields { groupKey: string; label: string; items: (typeof properties)[number][] }
.map((field) => items.find((item) => item.name === field)) >();
.filter(Boolean) as (typeof properties)[number][]; const hasGroups = Object.keys(groupDefinitions).length > 0;
if (ordered.length === 0) { for (const [groupKey, fields] of Object.entries(groupDefinitions)) {
return null; const ordered = fields
} .map((field) => items.find((item) => item.name === field))
.filter(Boolean) as (typeof properties)[number][];
ordered.forEach((item) => grouped.add(item.name)); if (ordered.length === 0) continue;
const label = domain const label = domain
? t(`${sectionI18nPrefix}.${domain}.${groupKey}`, { ? t(`${sectionI18nPrefix}.${domain}.${groupKey}`, {
ns: "config/groups", ns: "config/groups",
defaultValue: humanizeKey(groupKey), defaultValue: humanizeKey(groupKey),
}) })
: t(`groups.${groupKey}`, { : t(`groups.${groupKey}`, {
defaultValue: humanizeKey(groupKey), defaultValue: humanizeKey(groupKey),
}); });
return { const groupInfo = { groupKey, label, items: ordered };
key: groupKey, for (const item of ordered) {
label, fieldToGroup.set(item.name, groupInfo);
items: ordered, }
}; }
})
.filter(Boolean) as Array<{
key: string;
label: string;
items: (typeof properties)[number][];
}>;
const ungrouped = items.filter((item) => !grouped.has(item.name));
const isObjectLikeField = (item: (typeof properties)[number]) => { const isObjectLikeField = (item: (typeof properties)[number]) => {
const fieldSchema = (item.content.props as RjsfElementProps)?.schema; const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
return fieldSchema?.type === "object"; return fieldSchema?.type === "object";
}; };
return ( // Walk items in order (respects fieldOrder / ui:order).
<div className="space-y-6"> // When we hit the first field of a group, render the whole group block.
{groups.map((group) => ( // Skip subsequent fields that belong to an already-rendered group.
const renderedGroups = new Set<string>();
const elements: React.ReactNode[] = [];
for (const item of items) {
const group = fieldToGroup.get(item.name);
if (group) {
if (renderedGroups.has(group.groupKey)) continue;
renderedGroups.add(group.groupKey);
elements.push(
<div <div
key={group.key} key={group.groupKey}
className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4" className="space-y-4 rounded-lg border border-border/70 bg-card/30 p-4"
> >
<div className="text-md border-b border-border/60 pb-4 font-semibold text-primary-variant"> <div className="text-md border-b border-border/60 pb-4 font-semibold text-primary-variant">
@ -366,25 +369,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
<div key={element.name}>{element.content}</div> <div key={element.name}>{element.content}</div>
))} ))}
</div> </div>
</div> </div>,
))} );
} else {
elements.push(
<div
key={item.name}
className={cn(hasGroups && !isObjectLikeField(item) && "px-4")}
>
{item.content}
</div>,
);
}
}
{ungrouped.length > 0 && ( return <div className="space-y-6">{elements}</div>;
<div className={cn("space-y-6", groups.length > 0 && "pt-2")}>
{ungrouped.map((element) => (
<div
key={element.name}
className={cn(
groups.length > 0 && !isObjectLikeField(element) && "px-4",
)}
>
{element.content}
</div>
))}
</div>
)}
</div>
);
}; };
// Root level renders children directly // Root level renders children directly
@ -456,7 +455,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50"> <CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div className="min-w-0 pr-3">
<CardTitle <CardTitle
className={cn( className={cn(
"flex items-center text-sm", "flex items-center text-sm",
@ -475,9 +474,9 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
)} )}
</div> </div>
{isOpen ? ( {isOpen ? (
<LuChevronDown className="h-4 w-4" /> <LuChevronDown className="h-4 w-4 shrink-0" />
) : ( ) : (
<LuChevronRight className="h-4 w-4" /> <LuChevronRight className="h-4 w-4 shrink-0" />
)} )}
</div> </div>
</CardHeader> </CardHeader>