diff --git a/web/src/components/config-form/section-configs/audio.ts b/web/src/components/config-form/section-configs/audio.ts index 09fe4e974d..740d76f786 100644 --- a/web/src/components/config-form/section-configs/audio.ts +++ b/web/src/components/config-form/section-configs/audio.ts @@ -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"], diff --git a/web/src/components/config-form/section-configs/detect.ts b/web/src/components/config-form/section-configs/detect.ts index ecf5102b40..778620f1c0 100644 --- a/web/src/components/config-form/section-configs/detect.ts +++ b/web/src/components/config-form/section-configs/detect.ts @@ -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"], diff --git a/web/src/components/config-form/section-configs/face_recognition.ts b/web/src/components/config-form/section-configs/face_recognition.ts index 18e963940f..ef9e435061 100644 --- a/web/src/components/config-form/section-configs/face_recognition.ts +++ b/web/src/components/config-form/section-configs/face_recognition.ts @@ -6,7 +6,7 @@ const faceRecognition: SectionConfigOverrides = { restartRequired: [], fieldOrder: ["enabled", "min_area"], hiddenFields: [], - advancedFields: ["min_area"], + advancedFields: [], overrideFields: ["enabled", "min_area"], }, global: { diff --git a/web/src/components/config-form/section-configs/lpr.ts b/web/src/components/config-form/section-configs/lpr.ts index c5e16eb23e..514dba9bef 100644 --- a/web/src/components/config-form/section-configs/lpr.ts +++ b/web/src/components/config-form/section-configs/lpr.ts @@ -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: { diff --git a/web/src/components/config-form/section-configs/motion.ts b/web/src/components/config-form/section-configs/motion.ts index 38755ee207..4e9676bb21 100644 --- a/web/src/components/config-form/section-configs/motion.ts +++ b/web/src/components/config-form/section-configs/motion.ts @@ -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: { diff --git a/web/src/components/config-form/section-configs/record.ts b/web/src/components/config-form/section-configs/record.ts index 53803eed9a..9cfc921275 100644 --- a/web/src/components/config-form/section-configs/record.ts +++ b/web/src/components/config-form/section-configs/record.ts @@ -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"], diff --git a/web/src/components/config-form/section-configs/snapshots.ts b/web/src/components/config-form/section-configs/snapshots.ts index 8f08fa8437..126ecd4969 100644 --- a/web/src/components/config-form/section-configs/snapshots.ts +++ b/web/src/components/config-form/section-configs/snapshots.ts @@ -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"], diff --git a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx index 3ba4cb0bc5..f028a566f3 100644 --- a/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx +++ b/web/src/components/config-form/theme/templates/ObjectFieldTemplate.tsx @@ -311,51 +311,54 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) { return null; } - const grouped = new Set(); - 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 ( -
- {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(); + 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(
@@ -366,25 +369,21 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
{element.content}
))}
-
- ))} +
, + ); + } else { + elements.push( +
+ {item.content} +
, + ); + } + } - {ungrouped.length > 0 && ( -
0 && "pt-2")}> - {ungrouped.map((element) => ( -
0 && !isObjectLikeField(element) && "px-4", - )} - > - {element.content} -
- ))} -
- )} - - ); + return
{elements}
; }; // Root level renders children directly @@ -456,7 +455,7 @@ export function ObjectFieldTemplate(props: ObjectFieldTemplateProps) {
-
+
{isOpen ? ( - + ) : ( - + )}