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 && ( -