From d3924e2cffe225e04a0f6f94192d0169f01a8597 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Thu, 12 Feb 2026 11:50:09 -0600 Subject: [PATCH] default column layout and add field sizing --- web/src/components/config-form/ConfigForm.tsx | 35 ++- .../config-form/section-configs/auth.ts | 6 + .../config-form/section-configs/database.ts | 5 + .../config-form/section-configs/detect.ts | 55 ++--- .../section-configs/environment_vars.ts | 5 + .../config-form/section-configs/ffmpeg.ts | 9 + .../config-form/section-configs/genai.ts | 21 ++ .../config-form/section-configs/lpr.ts | 3 + .../config-form/section-configs/model.ts | 8 + .../config-form/section-configs/mqtt.ts | 21 +- .../config-form/section-configs/networking.ts | 2 + .../config-form/section-configs/objects.ts | 10 + .../config-form/section-configs/onvif.ts | 3 + .../config-form/section-configs/proxy.ts | 6 + .../config-form/section-configs/record.ts | 7 + .../config-form/section-configs/review.ts | 6 + .../config-form/section-configs/tls.ts | 8 + .../config-form/theme/components/index.tsx | 2 +- .../theme/fields/LayoutGridField.tsx | 36 ++- .../theme/templates/BaseInputTemplate.tsx | 4 + .../theme/templates/FieldTemplate.tsx | 217 +++++++++++++++--- .../theme/templates/ObjectFieldTemplate.tsx | 22 +- .../config-form/theme/utils/fieldSizing.ts | 37 +++ .../config-form/theme/utils/index.ts | 1 + .../theme/widgets/PasswordWidget.tsx | 8 +- .../theme/widgets/SelectWidget.tsx | 4 +- .../config-form/theme/widgets/TextWidget.tsx | 5 +- .../theme/widgets/TextareaWidget.tsx | 5 +- 28 files changed, 467 insertions(+), 84 deletions(-) create mode 100644 web/src/components/config-form/theme/utils/fieldSizing.ts diff --git a/web/src/components/config-form/ConfigForm.tsx b/web/src/components/config-form/ConfigForm.tsx index b0ae75014..36e430da2 100644 --- a/web/src/components/config-form/ConfigForm.tsx +++ b/web/src/components/config-form/ConfigForm.tsx @@ -147,6 +147,35 @@ const applyUiSchemaPathOverrides = ( return updated; }; +const applyLayoutGridFieldDefaults = (uiSchema: UiSchema): UiSchema => { + const applyDefaults = (node: unknown): unknown => { + if (Array.isArray(node)) { + return node.map((item) => applyDefaults(item)); + } + + if (typeof node !== "object" || node === null) { + return node; + } + + const nextNode: Record = {}; + + Object.entries(node).forEach(([key, value]) => { + nextNode[key] = applyDefaults(value); + }); + + if ( + Array.isArray(nextNode["ui:layoutGrid"]) && + nextNode["ui:field"] === undefined + ) { + nextNode["ui:field"] = "LayoutGridField"; + } + + return nextNode; + }; + + return applyDefaults(uiSchema) as UiSchema; +}; + export interface ConfigFormProps { /** JSON Schema for the form */ schema: RJSFSchema; @@ -245,7 +274,9 @@ export function ConfigForm({ transformedSchema, pathOverrides, ); - const merged = mergeUiSchema(expandedUiSchema, baseUiSchema); + const merged = applyLayoutGridFieldDefaults( + mergeUiSchema(expandedUiSchema, baseUiSchema), + ); // Add field groups if (fieldGroups) { @@ -311,7 +342,7 @@ export function ConfigForm({ ); return ( -
+
- + {children} diff --git a/web/src/components/config-form/theme/fields/LayoutGridField.tsx b/web/src/components/config-form/theme/fields/LayoutGridField.tsx index 54b028ba3..9953794d0 100644 --- a/web/src/components/config-form/theme/fields/LayoutGridField.tsx +++ b/web/src/components/config-form/theme/fields/LayoutGridField.tsx @@ -114,6 +114,13 @@ interface PropertyElement { content: React.ReactElement; } +function isObjectLikeElement(item: PropertyElement) { + const fieldSchema = item.content.props?.schema as + | { type?: string | string[] } + | undefined; + return fieldSchema?.type === "object"; +} + // Custom ObjectFieldTemplate wrapper that applies grid layout function GridLayoutObjectFieldTemplate( props: ObjectFieldTemplateProps, @@ -361,9 +368,16 @@ function GridLayoutObjectFieldTemplate( } return ( -
+
{showGroupLabel && ( -
+
{getGroupLabel(rowGroupKey)}
)} @@ -404,9 +418,12 @@ function GridLayoutObjectFieldTemplate( } leftoverSections.push( -
+
{showGroupLabel && ( -
+
{getGroupLabel(groupKey)}
)} @@ -429,7 +446,16 @@ function GridLayoutObjectFieldTemplate( )} > {ungroupedLeftovers.map((item) => ( -
{item.content}
+
0 && + !isObjectLikeElement(item) && + "px-4", + )} + > + {item.content} +
))}
, ); diff --git a/web/src/components/config-form/theme/templates/BaseInputTemplate.tsx b/web/src/components/config-form/theme/templates/BaseInputTemplate.tsx index 957c9e0cf..f1636fa6b 100644 --- a/web/src/components/config-form/theme/templates/BaseInputTemplate.tsx +++ b/web/src/components/config-form/theme/templates/BaseInputTemplate.tsx @@ -1,6 +1,7 @@ // Base Input Template - default input wrapper import type { WidgetProps } from "@rjsf/utils"; import { Input } from "@/components/ui/input"; +import { getSizedFieldClassName } from "../utils"; export function BaseInputTemplate(props: WidgetProps) { const { @@ -14,9 +15,11 @@ export function BaseInputTemplate(props: WidgetProps) { onFocus, placeholder, schema, + options, } = props; const inputType = type || "text"; + const fieldClassName = getSizedFieldClassName(options, "xs"); const handleChange = (e: React.ChangeEvent) => { const val = e.target.value; @@ -32,6 +35,7 @@ export function BaseInputTemplate(props: WidgetProps) { type !== "null"); + const isScalarValueField = + nonNullSchemaTypes.length === 1 && + ["string", "number", "integer"].includes(nonNullSchemaTypes[0]); // Only suppress labels/descriptions if this is a multi-schema field (anyOf/oneOf) with suppressMultiSchema flag // This prevents duplicate labels while still showing the inner field's label const isMultiSchemaWrapper = (schema.anyOf || schema.oneOf) && (suppressMultiSchema || isNullableUnion); + const useSplitBooleanLayout = + uiOptionsFromSchema.splitLayout !== false && + isBoolean && + !isMultiSchemaWrapper && + !isObjectField && + !isAdditionalProperty; + const useSplitLayout = + uiOptionsFromSchema.splitLayout !== false && + isScalarValueField && + !isBoolean && + !isMultiSchemaWrapper && + !isObjectField && + !isAdditionalProperty; // Get translation path for this field const pathSegments = fieldPathId.path.filter( @@ -379,17 +400,11 @@ export function FieldTemplate(props: FieldTemplateProps) { >
{beforeContent} -
+
{displayLabel && finalLabel && !isBoolean && + !useSplitLayout && !isMultiSchemaWrapper && !isObjectField && !isAdditionalProperty && ( @@ -409,29 +424,180 @@ export function FieldTemplate(props: FieldTemplateProps) { )} {isBoolean ? ( -
-
- {displayLabel && finalLabel && ( -