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",
],
fieldGroups: {
detection: ["enabled", "listen", "filters"],
detection: ["listen", "filters"],
sensitivity: ["min_volume", "max_not_heard"],
},
hiddenFields: ["enabled_in_config"],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -311,51 +311,54 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
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][];
// Build a lookup: field name → group info
const fieldToGroup = new Map<
string,
{ groupKey: string; label: string; items: (typeof properties)[number][] }
>();
const hasGroups = Object.keys(groupDefinitions).length > 0;
if (ordered.length === 0) {
return null;
}
for (const [groupKey, fields] of Object.entries(groupDefinitions)) {
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
? t(`${sectionI18nPrefix}.${domain}.${groupKey}`, {
ns: "config/groups",
defaultValue: humanizeKey(groupKey),
})
: t(`groups.${groupKey}`, {
defaultValue: humanizeKey(groupKey),
});
const label = domain
? t(`${sectionI18nPrefix}.${domain}.${groupKey}`, {
ns: "config/groups",
defaultValue: humanizeKey(groupKey),
})
: t(`groups.${groupKey}`, {
defaultValue: humanizeKey(groupKey),
});
return {
key: groupKey,
label,
items: ordered,
};
})
.filter(Boolean) as Array<{
key: string;
label: string;
items: (typeof properties)[number][];
}>;
const groupInfo = { groupKey, label, items: ordered };
for (const item of ordered) {
fieldToGroup.set(item.name, groupInfo);
}
}
const ungrouped = items.filter((item) => !grouped.has(item.name));
const isObjectLikeField = (item: (typeof properties)[number]) => {
const fieldSchema = (item.content.props as RjsfElementProps)?.schema;
return fieldSchema?.type === "object";
};
return (
<div className="space-y-6">
{groups.map((group) => (
// Walk items in order (respects fieldOrder / ui:order).
// When we hit the first field of a group, render the whole group block.
// 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
key={group.key}
key={group.groupKey}
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">
@ -366,25 +369,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
<div key={element.name}>{element.content}</div>
))}
</div>
</div>
))}
</div>,
);
} else {
elements.push(
<div
key={item.name}
className={cn(hasGroups && !isObjectLikeField(item) && "px-4")}
>
{item.content}
</div>,
);
}
}
{ungrouped.length > 0 && (
<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>
);
return <div className="space-y-6">{elements}</div>;
};
// Root level renders children directly
@ -456,7 +455,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
<CollapsibleTrigger asChild>
<CardHeader className="cursor-pointer p-4 transition-colors hover:bg-muted/50">
<div className="flex items-center justify-between">
<div>
<div className="min-w-0 pr-3">
<CardTitle
className={cn(
"flex items-center text-sm",
@ -475,9 +474,9 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
)}
</div>
{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>
</CardHeader>