diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx index d25c6413e..f5feb4d0a 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx @@ -1,60 +1,114 @@ import React from 'react'; import { Button, Row, Col } from 'antd'; -import { ArrayFieldTemplateProps } from '@rjsf/utils'; +import { ArrayFieldTemplateProps, getUiOptions, RJSFSchema } from '@rjsf/utils'; import { ArrowDownOutlined, ArrowUpOutlined, DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import ObjectFieldTemplate from './ObjectFieldTemplate'; // Ensure this is correctly imported + +const DEFAULT_RESPONSIVE_COL_SPAN = { + xs: 24, + sm: 24, + md: 12, + lg: 8, + xl: 6, +}; + +type UiProps = { + rowGutter?: number; + colSpan?: number | Record; +}; const ArrayFieldTemplate = (props: ArrayFieldTemplateProps) => { - const { items, canAdd, onAddClick, title } = props; + const { items, canAdd, onAddClick, title, uiSchema, registry } = props; - return ( -
- {title && {title}} - - {items.map((element: any) => ( - - {/* Content container for the array item */} -
- {element.children} -
+ // Get UI schema configuration + const { rowGutter = 8, colSpan = DEFAULT_RESPONSIVE_COL_SPAN } = getUiOptions(uiSchema)?.["ui:props"] as UiProps || {}; - {/* Container for the control buttons with vertical alignment */} -
- {/* Move down button */} - {element.hasMoveDown && ( -
- - ))} - {/* Add button for the array */} + return ( + + {/* Use ObjectFieldTemplate to render each array item */} + void { + throw new Error('Function not implemented.'); + } } + /> + + {/* Control buttons */} +
+ {element.hasMoveDown && ( +
+ + ); + }); + }; + + return ( +
+ + + {renderItems()} {/* Render items */} {canAdd && ( - + diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx index 542f92ff9..f9ff6bc8b 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx @@ -1,7 +1,9 @@ -import React from 'react'; +import React, { useEffect, useRef, useState } from "react"; import { Row, Col } from 'antd'; import { ObjectFieldTemplateProps, getTemplate, getUiOptions, descriptionId, titleId, canExpand } from '@rjsf/utils'; import { ConfigConsumer } from 'antd/es/config-provider/context'; +import { useContainerWidth } from "./jsonSchemaFormComp"; +import styled from "styled-components"; const DESCRIPTION_COL_STYLE = { paddingBottom: '8px', @@ -21,7 +23,7 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { readonly, registry, } = props; - + const containerWidth = useContainerWidth(); const uiOptions = getUiOptions(uiSchema); const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, uiOptions); const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions); @@ -38,58 +40,233 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { xl: 8, // Extra large devices }; - const { rowGutter = 4, colSpan = defaultResponsiveColSpan } = uiSchema?.['ui:props'] || {}; + const { rowGutter = 4 } = uiSchema?.['ui:props'] || {}; + + const getLegendStyle = (level: number): React.CSSProperties => { + switch (level) { + case 0: + return { fontSize: "16px", fontWeight: "bold", marginBottom: "8px" }; // Form Title + case 1: + return { fontSize: "14px", fontWeight: "600", marginBottom: "6px" }; // Section Title + default: + return { fontSize: "12px", fontWeight: "normal", marginBottom: "4px" }; // Field Title + } + }; + + const calculateResponsiveColSpan = (uiSchema: any = {}): { span: number } => { + const colSpan = uiSchema?.["ui:colSpan"] || { + xs: 24, + sm: 24, + md: 12, + lg: 12, + xl: 8, + }; + + if (typeof colSpan === "number") { + return { span: colSpan }; + } else if (typeof colSpan === "object") { + if (containerWidth > 1200 && colSpan.xl !== undefined) { + return { span: colSpan.xl }; + } else if (containerWidth > 992 && colSpan.lg !== undefined) { + return { span: colSpan.lg }; + } else if (containerWidth > 768 && colSpan.md !== undefined) { + return { span: colSpan.md }; + } else if (containerWidth > 576 && colSpan.sm !== undefined) { + return { span: colSpan.sm }; + } else if (colSpan.xs !== undefined) { + return { span: colSpan.xs }; + } + } + return { span: 24 }; // Default span + }; + + const getFieldRenderer = (type: string) => { + const typeMap: Record = { + string: "StringField", // Handles strings + number: "NumberField", // Handles floating-point numbers + integer: "NumberField", // Handles integers (mapped to NumberField) + boolean: "BooleanField", // Handles true/false values + object: "ObjectField", // Handles nested objects + array: "ArrayField", // Handles arrays + null: "NullField", // Handles null values + anyOf: "AnyOfField", // Handles anyOf schemas + oneOf: "OneOfField", // Handles oneOf schemas + schema: "SchemaField", + }; + + const fieldName = typeMap[type]; + return fieldName ? registry.fields[fieldName] : undefined; + }; + + const renderFieldsFromSection = (section: any, level: number = 0) => { + const { formData, schema, uiSchema } = section.content.props; + + if (schema.type === "object" && schema.properties) { + // Render fields for objects + const fieldKeys = Object.keys(schema.properties); + + return ( + + {fieldKeys.map((fieldKey) => { + const fieldSchema = schema.properties[fieldKey]; + const fieldUiSchema = uiSchema?.[fieldKey] || {}; + const fieldFormData = formData ? formData[fieldKey] : undefined; + const span = calculateResponsiveColSpan(fieldUiSchema); + + const FieldRenderer = getFieldRenderer(fieldSchema.type); + + if (!FieldRenderer) { + console.error(`No renderer found for field type: ${fieldSchema.type}`); + return ( + +
Unsupported field type: {fieldSchema.type}
+ + ); + } + + return ( + +
+ {fieldSchema.title || fieldKey} + { + section.content.props.onChange({ + ...formData, + [fieldKey]: value, + }); + }} + onBlur={section.content.props.onBlur} + onFocus={section.content.props.onFocus} + /> +
+ + ); + })} +
+ ); + } else if (schema.type === "array" && schema.items) { + // Render fields for arrays + const FieldRenderer = getFieldRenderer(schema.type); + + if (!FieldRenderer) { + console.error(`No renderer found for field type: ${schema.type}`); + return ( +
+

Unsupported field type: {schema.type}

+
+ ); + } + + return ( +
+ +
+ ); + } + + // Log error for unsupported or missing schema types + console.error("Unsupported or missing schema type in section:", section); + return null; + }; + + const renderSections = (properties: any[], level: number) => { + return properties.map((section) => { + const schema = section.content.props.schema; + const isArray = typeof section.content.props.index === 'number'; + const sectionTitle = schema.title || section.name; - // Generate responsive colSpan props for each element - const calculateResponsiveColSpan = (element: any) => { - const { type } = element.content.props.schema; - const widget = getUiOptions(element.content.props.uiSchema).widget; + console.log("Section", sectionTitle, isArray, section); - const defaultSpan = widget === 'textarea' || type === 'object' || type === 'array' ? 24 : colSpan; + return ( + + +
+ {/* Always render the legend for the section itself */} + {level === 0 && !isArray ? ( + {sectionTitle} + ) : null} - // Ensure the returned object is properly formatted for AntD responsive properties - return typeof defaultSpan === 'object' ? defaultSpan : { span: defaultSpan }; + {/* Render the section content */} + {renderFieldsFromSection(section, level + 1)} +
+ +
+ ); + }); }; return ( - {(configProps) => ( + {() => (
- - {schema.type === 'object' && title && ( - - - - )} - {description && ( + {/* Render Title */} + {schema.type === "object" && title && ( + + + + )} + + {/* Render Description */} + {description && ( + - + - )} - {uiSchema?.['ui:grid'] && Array.isArray(uiSchema['ui:grid']) ? ( - uiSchema['ui:grid'].map((ui_row: Record) => { - return Object.keys(ui_row).map((row_item) => { - const element = properties.find((p) => p.name === row_item); - return element ? ( - // Pass responsive colSpan props using the calculated values - - {element.content} - - ) : null; - }); - }) - ) : ( - properties.map((element) => ( - - {element.content} - - )) - )} - + + )} + + {/* Render Sections */} + {renderSections(properties,0)} + + {/* Expand Button */} {canExpand(schema, uiSchema, formData) && ( - + - + )} diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx index da537681a..c1e58eda8 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/jsonSchemaFormComp.tsx @@ -22,19 +22,26 @@ import ErrorBoundary from "./errorBoundary"; import { Theme } from "@rjsf/antd"; import { hiddenPropertyView } from "comps/utils/propertyUtils"; import { AutoHeightControl } from "../../controls/autoHeightControl"; -import { useContext, useEffect } from "react"; +import { useContext, useEffect, useRef, useState, createContext } from "react"; import { EditorContext } from "comps/editorState"; import ObjectFieldTemplate from './ObjectFieldTemplate'; import ArrayFieldTemplate from './ArrayFieldTemplate'; import { Select } from 'antd'; import Title from 'antd/es/typography/Title'; + Theme.widgets.DateWidget = DateWidget(false); Theme.widgets.DateTimeWidget = DateWidget(true); const Form = withTheme(Theme); const EventOptions = [submitEvent] as const; +const ContainerWidthContext = createContext(0); + +const useContainerWidth = () => { + return useContext(ContainerWidthContext); +}; + const Container = styled.div<{ $style: JsonSchemaFormStyleType; $animationStyle: AnimationStyleType; @@ -216,6 +223,7 @@ function onSubmit(props: { }); } + let FormBasicComp = (function () { const childrenMap = { resetAfterSubmit: BoolControl, @@ -228,6 +236,7 @@ let FormBasicComp = (function () { style: styleControl(JsonSchemaFormStyle , 'style'), animationStyle: styleControl(AnimationStyle , 'animationStyle'), }; + return new UICompBuilder(childrenMap, (props) => { // rjsf 4.20 supports ui:submitButtonOptions, but if the button is customized, it will not take effect. Here we implement it ourselves const buttonOptions = props?.uiSchema?.[ @@ -236,51 +245,83 @@ let FormBasicComp = (function () { const schema = props.schema; + const containerRef = useRef(null); + const [containerWidth, setContainerWidth] = useState(0); + + // Monitor the container's width + useEffect(() => { + const updateWidth = () => { + if (containerRef.current) { + setContainerWidth(containerRef.current.offsetWidth); + } + }; + + const resizeObserver = new ResizeObserver(() => { + updateWidth(); + }); + + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // Initial update + updateWidth(); + + // Cleanup observer on unmount + return () => { + resizeObserver.disconnect(); + }; + }, []); + return ( - - - - - {schema.title as string | number} - -
onSubmit(props)} - onChange={(e) => props.data.onChange(e.formData)} - transformErrors={(errors) => transformErrors(errors)} - templates={{ - ObjectFieldTemplate: ObjectFieldTemplate, - ArrayFieldTemplate: ArrayFieldTemplate, - }} - widgets={{ searchableSelect: SearchableSelectWidget }} - // ErrorList={ErrorList} - children={ - - } - /> - - - + + + + + + {schema.title as string | number} + + onSubmit(props)} + onChange={(e) => props.data.onChange(e.formData)} + transformErrors={(errors) => transformErrors(errors)} + templates={{ + ObjectFieldTemplate: ObjectFieldTemplate, + ArrayFieldTemplate: ArrayFieldTemplate, + // FieldTemplate: LayoutFieldTemplate, + }} + widgets={{ searchableSelect: SearchableSelectWidget }} + // ErrorList={ErrorList} + children={ + + } + /> + + + + + ); }) .setPropertyViewFn((children) => { @@ -439,5 +480,5 @@ FormTmpComp = withMethodExposing(FormTmpComp, [ }), }, ]); - -export const JsonSchemaFormComp = FormTmpComp; \ No newline at end of file +export const JsonSchemaFormComp = FormTmpComp; +export { FormTmpComp, useContainerWidth };