From c7bddbf001e63a8138ea364b4e95f75e75688fc1 Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Sun, 17 Nov 2024 13:09:14 +0100 Subject: [PATCH 1/2] Work in Progress for Responsive Layout --- .../ObjectFieldTemplate.tsx | 311 +++++++----------- .../jsonSchemaFormComp/jsonSchemaFormComp.tsx | 132 +++++--- 2 files changed, 214 insertions(+), 229 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx index a852332ff..33751aca8 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx @@ -1,33 +1,14 @@ 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 { 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", + paddingBottom: '8px', }; -interface ColSpan { - xs: number; - sm: number; - md: number; - lg: number; - xl: number; -} - -interface UiOptions { - colSpan: ColSpan; - rowGutter: number; - // other properties... -} - const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { const { title, @@ -42,59 +23,62 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { readonly, registry, } = props; - - const containerRef = useRef(null); - const [containerWidth, setContainerWidth] = useState(0); - - // Monitor the container's width - useEffect(() => { - const updateWidth = () => { - if (containerRef.current) { - setContainerWidth(containerRef.current.offsetWidth); - } - }; - - // Create a ResizeObserver to watch for width changes - const resizeObserver = new ResizeObserver(() => { - updateWidth(); - }); - - if (containerRef.current) { - resizeObserver.observe(containerRef.current); - } - - // Initial update - updateWidth(); - - // Cleanup observer on unmount - return () => { - resizeObserver.disconnect(); - }; - }, []); + const containerWidth = useContainerWidth(); const uiOptions = getUiOptions(uiSchema); - const TitleFieldTemplate = getTemplate("TitleFieldTemplate", registry, uiOptions); - const DescriptionFieldTemplate = getTemplate("DescriptionFieldTemplate", registry, uiOptions); + const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, uiOptions); + const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions); const { ButtonTemplates: { AddButton }, } = registry.templates; - const defaultResponsiveColSpan = (width: number) => { - if (width > 1200) return 8; // Wide screens - if (width > 768) return 12; // Tablets - return 24; // Mobile + // Define responsive column spans based on the ui:props or fallback to defaults + const defaultResponsiveColSpan = { + xs: 24, // Extra small devices + sm: 24, // Small devices + md: 12, // Medium devices + lg: 12, // Large devices + xl: 8, // Extra large devices }; - const { rowGutter = 4 } = uiSchema?.["ui:props"] || {}; - - const calculateResponsiveColSpan = (element: any): { span: number } => { + const { rowGutter = 4 } = uiSchema?.['ui:props'] || {}; + + const calculateResponsiveColSpan = (element: any, level: number): { span: number } => { + + console.log("Calculating span for", element.name, "at level", level); + + // root level + if (level === 0) return { span: 24 }; + + // Check if the element has a layout definition in ui:grid + const gridColSpan = uiSchema?.['ui:grid'] + ?.find((row: Record) => row[element.name]) + ?. [element.name]; + + if (gridColSpan) { + if (typeof gridColSpan === "number") { + return { span: gridColSpan }; + } else if (typeof gridColSpan === "object") { + if (containerWidth > 1200 && gridColSpan.xl !== undefined) { + return { span: gridColSpan.xl }; + } else if (containerWidth > 992 && gridColSpan.lg !== undefined) { + return { span: gridColSpan.lg }; + } else if (containerWidth > 768 && gridColSpan.md !== undefined) { + return { span: gridColSpan.md }; + } else if (containerWidth > 576 && gridColSpan.sm !== undefined) { + return { span: gridColSpan.sm }; + } else if (gridColSpan.xs !== undefined) { + return { span: gridColSpan.xs }; + } + } + } + // Fallback to default colSpan or ui:props.colSpan const uiSchemaProps = getUiOptions(element.content.props.uiSchema)?.["ui:props"] as | { colSpan?: Record | number } | undefined; const uiSchemaColSpan = uiSchemaProps?.colSpan; - const defaultSpan = containerWidth > 1200 ? 8 : containerWidth > 768 ? 12 : 24; if (uiSchemaColSpan) { if (typeof uiSchemaColSpan === "number") { @@ -114,133 +98,94 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { } } + // Default responsive behavior + const defaultSpan = containerWidth > 1200 ? 8 : containerWidth > 768 ? 12 : 24; return { span: defaultSpan }; }; - const renderSectionLayout = (properties: any[], uiGrid: any, section: string) => { - - if (uiGrid && Array.isArray(uiGrid)) { - return ( - - {uiGrid.map((ui_row: Record) => - Object.keys(ui_row).map((row_item) => { - const element = properties.find((p) => p.name === row_item); - if (element) { - const span = calculateResponsiveColSpan(element).span; - return ( - - {element.content} - - ); - } - return null; - }) - )} - - ); - } - - // Default layout if no grid is provided + const renderProperties = (properties: any[], level: number) => { + console.log("Rendering level:", level); // Debugging level return ( - - {properties.map((element) => ( - - {element.content} - - ))} + + {properties.map((element) => { + const span = calculateResponsiveColSpan(element, level); + + // Check if the element is an object or array and has nested properties + if (element.content?.props?.schema?.type === "object" && element.content.props.properties) { + // Render nested objects with an incremented level + return ( + +
+ {element.content.props.title || element.name} + {renderProperties(element.content.props.properties, level + 1)} +
+ + ); + } + + // Render normal elements + return ( + + {element.content} + + ); + })}
); }; - - const renderCustomLayout = () => { - const schemaType = schema.type as string; - switch (schemaType) { - case "Group": - return ( -
-

{schema.label || "Group"}

- {renderSectionLayout(properties, uiSchema?.["ui:grid"], schema.label)} -
- ); - case "HorizontalLayout": - return ( - - {properties.map((element) => ( - - {element.content} - - ))} - - ); - case "VerticalLayout": - return ( -
- {properties.map((element) => ( -
{element.content}
- ))} -
- ); - default: - return null; // Fall back to default rendering if no match - } - }; - - // Check if the schema is a custom layout type - const schemaType = schema.type as string; // Extract schema type safely - const isCustomLayout = ["Group", "HorizontalLayout", "VerticalLayout"].includes(schemaType); - + return ( -
- - {(configProps) => ( -
- {!isCustomLayout && ( - <> - {schema.type === "object" && title && ( - - Codestin Search App -
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={ - - } - /> - - - + + + + + Codestin Search App + 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) => { @@ -445,3 +484,4 @@ FormTmpComp = withMethodExposing(FormTmpComp, [ ]); export const JsonSchemaFormComp = FormTmpComp; +export { FormTmpComp, useContainerWidth }; From fd94adc0f8f703cebf621784144edbecaa5f04ba Mon Sep 17 00:00:00 2001 From: FalkWolsky Date: Mon, 18 Nov 2024 15:21:07 +0100 Subject: [PATCH 2/2] Enable Responsiveness for JSON Schema Form --- .../jsonSchemaFormComp/ArrayFieldTemplate.tsx | 148 ++++++---- .../ObjectFieldTemplate.tsx | 258 ++++++++++++------ 2 files changed, 275 insertions(+), 131 deletions(-) diff --git a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ArrayFieldTemplate.tsx index 5af2ba32d..6a652d45e 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 33751aca8..fecde5c57 100644 --- a/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx +++ b/client/packages/lowcoder/src/comps/comps/jsonSchemaFormComp/ObjectFieldTemplate.tsx @@ -25,6 +25,8 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { } = props; const containerWidth = useContainerWidth(); + // console.log("ObjectFieldTemplate Props", props); + const uiOptions = getUiOptions(uiSchema); const TitleFieldTemplate = getTemplate('TitleFieldTemplate', registry, uiOptions); const DescriptionFieldTemplate = getTemplate('DescriptionFieldTemplate', registry, uiOptions); @@ -43,103 +45,188 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { const { rowGutter = 4 } = uiSchema?.['ui:props'] || {}; - const calculateResponsiveColSpan = (element: any, level: number): { span: number } => { - - console.log("Calculating span for", element.name, "at level", level); - - // root level - if (level === 0) return { span: 24 }; - - // Check if the element has a layout definition in ui:grid - const gridColSpan = uiSchema?.['ui:grid'] - ?.find((row: Record) => row[element.name]) - ?. [element.name]; - - if (gridColSpan) { - if (typeof gridColSpan === "number") { - return { span: gridColSpan }; - } else if (typeof gridColSpan === "object") { - if (containerWidth > 1200 && gridColSpan.xl !== undefined) { - return { span: gridColSpan.xl }; - } else if (containerWidth > 992 && gridColSpan.lg !== undefined) { - return { span: gridColSpan.lg }; - } else if (containerWidth > 768 && gridColSpan.md !== undefined) { - return { span: gridColSpan.md }; - } else if (containerWidth > 576 && gridColSpan.sm !== undefined) { - return { span: gridColSpan.sm }; - } else if (gridColSpan.xs !== undefined) { - return { span: gridColSpan.xs }; - } - } + 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 } + }; - // Fallback to default colSpan or ui:props.colSpan - const uiSchemaProps = getUiOptions(element.content.props.uiSchema)?.["ui:props"] as - | { colSpan?: Record | number } - | undefined; - - const uiSchemaColSpan = uiSchemaProps?.colSpan; - - if (uiSchemaColSpan) { - if (typeof uiSchemaColSpan === "number") { - return { span: uiSchemaColSpan }; - } else if (typeof uiSchemaColSpan === "object") { - if (containerWidth > 1200 && uiSchemaColSpan.xl !== undefined) { - return { span: uiSchemaColSpan.xl }; - } else if (containerWidth > 992 && uiSchemaColSpan.lg !== undefined) { - return { span: uiSchemaColSpan.lg }; - } else if (containerWidth > 768 && uiSchemaColSpan.md !== undefined) { - return { span: uiSchemaColSpan.md }; - } else if (containerWidth > 576 && uiSchemaColSpan.sm !== undefined) { - return { span: uiSchemaColSpan.sm }; - } else if (uiSchemaColSpan.xs !== undefined) { - return { span: uiSchemaColSpan.xs }; - } + 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 }; } } - - // Default responsive behavior - const defaultSpan = containerWidth > 1200 ? 8 : containerWidth > 768 ? 12 : 24; - return { span: defaultSpan }; + return { span: 24 }; // Default span }; - - const renderProperties = (properties: any[], level: number) => { - console.log("Rendering level:", level); // Debugging level - return ( - - {properties.map((element) => { - const span = calculateResponsiveColSpan(element, level); - // Check if the element is an object or array and has nested properties - if (element.content?.props?.schema?.type === "object" && element.content.props.properties) { - // Render nested objects with an incremented level + 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 ( - +
- {element.content.props.title || element.name} - {renderProperties(element.content.props.properties, level + 1)} + {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); - // Render normal elements - return ( - - {element.content} - - ); - })} -
- ); - }; + 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; + + console.log("Section", sectionTitle, isArray, section); + + return ( + + +
+ {/* Always render the legend for the section itself */} + {level === 0 && !isArray ? ( + {sectionTitle} + ) : null} + + {/* Render the section content */} + {renderFieldsFromSection(section, level + 1)} +
+ +
+ ); + }); + }; + return ( - {(configProps) => ( + {() => (
{/* Render Title */} {schema.type === "object" && title && ( @@ -154,6 +241,7 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { /> )} + {/* Render Description */} {description && ( @@ -168,8 +256,10 @@ const ObjectFieldTemplate = (props: ObjectFieldTemplateProps) => { )} - {/* Render Properties */} - {renderProperties(properties, 0)} + + {/* Render Sections */} + {renderSections(properties,0)} + {/* Expand Button */} {canExpand(schema, uiSchema, formData) && (