@object-ui/react 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/dist/SchemaRenderer.d.ts +7 -0
  3. package/dist/SchemaRenderer.js +7 -0
  4. package/dist/components/form/FieldFactory.d.ts +43 -0
  5. package/dist/components/form/FieldFactory.js +157 -0
  6. package/dist/components/form/FormRenderer.d.ts +40 -0
  7. package/dist/components/form/FormRenderer.js +87 -0
  8. package/dist/components/form/FormRenderer.test.d.ts +8 -0
  9. package/dist/components/form/FormRenderer.test.js +534 -0
  10. package/dist/components/form/index.d.ts +11 -0
  11. package/dist/components/form/index.js +9 -0
  12. package/dist/context/SchemaRendererContext.d.ts +13 -0
  13. package/dist/context/SchemaRendererContext.js +21 -0
  14. package/dist/context/index.d.ts +8 -1
  15. package/dist/context/index.js +8 -1
  16. package/dist/hooks/index.d.ts +9 -1
  17. package/dist/hooks/index.js +9 -1
  18. package/dist/hooks/useActionRunner.d.ts +32 -0
  19. package/dist/hooks/useActionRunner.js +65 -0
  20. package/dist/hooks/useExpression.d.ts +27 -0
  21. package/dist/hooks/useExpression.js +41 -0
  22. package/dist/index.d.ts +8 -0
  23. package/dist/index.js +8 -0
  24. package/dist/index.test.d.ts +7 -0
  25. package/dist/index.test.js +7 -0
  26. package/package.json +16 -4
  27. package/src/SchemaRenderer.tsx +8 -0
  28. package/src/components/form/FieldFactory.tsx +504 -0
  29. package/src/components/form/FormRenderer.test.tsx +599 -0
  30. package/src/components/form/FormRenderer.tsx +209 -0
  31. package/src/components/form/README.md +352 -0
  32. package/src/components/form/index.ts +12 -0
  33. package/src/context/SchemaRendererContext.tsx +40 -0
  34. package/src/context/index.ts +9 -1
  35. package/src/hooks/index.ts +11 -1
  36. package/src/hooks/useActionRunner.ts +81 -0
  37. package/src/hooks/useExpression.ts +56 -0
  38. package/src/index.test.ts +8 -0
  39. package/src/index.ts +9 -0
  40. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # @object-ui/react
2
2
 
3
+ ## 0.3.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Maintenance release - Documentation and build improvements
8
+ - Updated dependencies
9
+ - @object-ui/core@0.3.1
10
+
3
11
  ## 0.3.0
4
12
 
5
13
  ### Minor Changes
@@ -1,3 +1,10 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
1
8
  import React from 'react';
2
9
  import { SchemaNode } from '@object-ui/core';
3
10
  export declare const SchemaRenderer: React.ForwardRefExoticComponent<Omit<{
@@ -1,4 +1,11 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
2
9
  import React, { forwardRef } from 'react';
3
10
  import { ComponentRegistry } from '@object-ui/core';
4
11
  export const SchemaRenderer = forwardRef(({ schema, ...props }, _ref) => {
@@ -0,0 +1,43 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React from 'react';
9
+ import { UseFormReturn } from 'react-hook-form';
10
+ import type { FormField } from '@objectstack/spec/ui';
11
+ /**
12
+ * Extended form field with additional properties for complex widgets
13
+ * These properties are not part of the standard FormField schema but may be
14
+ * provided by specific implementations
15
+ */
16
+ export interface ExtendedFormField extends FormField {
17
+ multiple?: boolean;
18
+ accept?: string[];
19
+ options?: Array<{
20
+ label: string;
21
+ value: string;
22
+ disabled?: boolean;
23
+ }>;
24
+ }
25
+ export interface FieldFactoryProps {
26
+ /**
27
+ * Field configuration from FormFieldSchema
28
+ */
29
+ field: FormField;
30
+ /**
31
+ * React Hook Form methods
32
+ */
33
+ methods: UseFormReturn<any>;
34
+ /**
35
+ * Whether the field is disabled
36
+ */
37
+ disabled?: boolean;
38
+ }
39
+ /**
40
+ * FieldFactory component that renders different input types based on
41
+ * the widget property or field type
42
+ */
43
+ export declare const FieldFactory: React.FC<FieldFactoryProps>;
@@ -0,0 +1,157 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * FieldFactory component that renders different input types based on
4
+ * the widget property or field type
5
+ */
6
+ export const FieldFactory = ({ field, methods, disabled = false, }) => {
7
+ const { register, formState: { errors } } = methods;
8
+ // Cast to extended field for properties not in base schema
9
+ const extendedField = field;
10
+ // Determine the widget type
11
+ const widgetType = field.widget || 'text';
12
+ const fieldName = field.field;
13
+ const error = errors[fieldName];
14
+ // Helper function to handle multiple select value conversion
15
+ const handleMultipleSelectValue = (value) => {
16
+ if (extendedField.multiple && value instanceof HTMLCollection) {
17
+ return Array.from(value).map((opt) => opt.value);
18
+ }
19
+ return value;
20
+ };
21
+ // Handle conditional visibility
22
+ // Note: visibleOn expression evaluation is not yet implemented
23
+ // Fields are always visible unless explicitly hidden
24
+ // Skip if explicitly hidden
25
+ if (field.hidden) {
26
+ return null;
27
+ }
28
+ // Common field wrapper
29
+ const renderField = (input) => (_jsxs("div", { className: "space-y-2", children: [field.label && (_jsxs("label", { htmlFor: fieldName, className: "block text-sm font-medium text-gray-700", children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), input, field.helpText && (_jsx("p", { className: "text-sm text-gray-500", children: field.helpText })), error && (_jsx("p", { className: "text-sm text-red-600", children: error.message }))] }));
30
+ // Render based on widget type
31
+ switch (widgetType.toLowerCase()) {
32
+ case 'text':
33
+ case 'string':
34
+ case 'email':
35
+ case 'password':
36
+ case 'url':
37
+ case 'tel':
38
+ return renderField(_jsx("input", { id: fieldName, type: widgetType === 'string' ? 'text' : widgetType, placeholder: field.placeholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
39
+ required: field.required ? `${field.label || fieldName} is required` : false,
40
+ }) }));
41
+ case 'number':
42
+ case 'integer':
43
+ case 'float':
44
+ return renderField(_jsx("input", { id: fieldName, type: "number", placeholder: field.placeholder, disabled: disabled, step: widgetType === 'integer' ? '1' : 'any', className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
45
+ required: field.required ? `${field.label || fieldName} is required` : false,
46
+ valueAsNumber: true,
47
+ }) }));
48
+ case 'checkbox':
49
+ case 'boolean':
50
+ return (_jsxs("div", { className: "flex items-start space-x-2", children: [_jsx("input", { id: fieldName, type: "checkbox", disabled: disabled, className: "mt-1 h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-2 focus:ring-blue-500 disabled:opacity-50", ...register(fieldName) }), _jsxs("div", { className: "flex-1", children: [field.label && (_jsxs("label", { htmlFor: fieldName, className: "text-sm font-medium text-gray-700", children: [field.label, field.required && _jsx("span", { className: "text-red-500 ml-1", children: "*" })] })), field.helpText && (_jsx("p", { className: "text-sm text-gray-500 mt-1", children: field.helpText })), error && (_jsx("p", { className: "text-sm text-red-600 mt-1", children: error.message }))] })] }));
51
+ case 'textarea':
52
+ return renderField(_jsx("textarea", { id: fieldName, placeholder: field.placeholder, disabled: disabled, rows: 4, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
53
+ required: field.required ? `${field.label || fieldName} is required` : false,
54
+ }) }));
55
+ case 'select':
56
+ case 'dropdown':
57
+ return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
58
+ required: field.required ? `${field.label || fieldName} is required` : false,
59
+ setValueAs: handleMultipleSelectValue,
60
+ }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
61
+ case 'date':
62
+ return renderField(_jsx("input", { id: fieldName, type: "date", placeholder: field.placeholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
63
+ required: field.required ? `${field.label || fieldName} is required` : false,
64
+ }) }));
65
+ case 'datetime':
66
+ case 'datetime-local':
67
+ return renderField(_jsx("input", { id: fieldName, type: "datetime-local", placeholder: field.placeholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
68
+ required: field.required ? `${field.label || fieldName} is required` : false,
69
+ }) }));
70
+ case 'time':
71
+ return renderField(_jsx("input", { id: fieldName, type: "time", placeholder: field.placeholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
72
+ required: field.required ? `${field.label || fieldName} is required` : false,
73
+ }) }));
74
+ case 'currency':
75
+ return renderField(_jsxs("div", { className: "relative", children: [_jsx("span", { className: "absolute left-3 top-2 text-gray-500", children: "$" }), _jsx("input", { id: fieldName, type: "number", placeholder: field.placeholder, disabled: disabled, step: "0.01", className: "w-full pl-8 pr-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
76
+ required: field.required ? `${field.label || fieldName} is required` : false,
77
+ valueAsNumber: true,
78
+ }) })] }));
79
+ case 'percent':
80
+ return renderField(_jsxs("div", { className: "relative", children: [_jsx("input", { id: fieldName, type: "number", placeholder: field.placeholder, disabled: disabled, step: "0.01", min: "0", max: "100", className: "w-full pr-8 pl-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
81
+ required: field.required ? `${field.label || fieldName} is required` : false,
82
+ valueAsNumber: true,
83
+ }) }), _jsx("span", { className: "absolute right-3 top-2 text-gray-500", children: "%" })] }));
84
+ case 'phone':
85
+ return renderField(_jsx("input", { id: fieldName, type: "tel", placeholder: field.placeholder || '+1 (555) 000-0000', disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
86
+ required: field.required ? `${field.label || fieldName} is required` : false,
87
+ }) }));
88
+ case 'markdown':
89
+ return renderField(_jsx("textarea", { id: fieldName, placeholder: field.placeholder || 'Enter markdown text...', disabled: disabled, rows: 8, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono", ...register(fieldName, {
90
+ required: field.required ? `${field.label || fieldName} is required` : false,
91
+ }) }));
92
+ case 'html':
93
+ return renderField(_jsx("textarea", { id: fieldName, placeholder: field.placeholder || 'Enter HTML...', disabled: disabled, rows: 8, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono", ...register(fieldName, {
94
+ required: field.required ? `${field.label || fieldName} is required` : false,
95
+ }) }));
96
+ case 'file':
97
+ return renderField(_jsx("input", { id: fieldName, type: "file", disabled: disabled, multiple: extendedField.multiple, accept: extendedField.accept?.join(','), className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100", ...register(fieldName, {
98
+ required: field.required ? `${field.label || fieldName} is required` : false,
99
+ }) }));
100
+ case 'image':
101
+ return renderField(_jsx("input", { id: fieldName, type: "file", disabled: disabled, multiple: extendedField.multiple, accept: extendedField.accept?.join(',') || 'image/*', className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-sm file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100", ...register(fieldName, {
102
+ required: field.required ? `${field.label || fieldName} is required` : false,
103
+ }) }));
104
+ case 'location':
105
+ return renderField(_jsxs("div", { className: "space-y-2", children: [_jsx("input", { id: `${fieldName}-lat`, type: "number", placeholder: "Latitude", disabled: disabled, step: "any", className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(`${fieldName}.lat`, {
106
+ required: field.required ? 'Latitude is required' : false,
107
+ valueAsNumber: true,
108
+ }) }), _jsx("input", { id: `${fieldName}-lng`, type: "number", placeholder: "Longitude", disabled: disabled, step: "any", className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(`${fieldName}.lng`, {
109
+ required: field.required ? 'Longitude is required' : false,
110
+ valueAsNumber: true,
111
+ }) })] }));
112
+ case 'lookup':
113
+ case 'master_detail':
114
+ return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
115
+ required: field.required ? `${field.label || fieldName} is required` : false,
116
+ setValueAs: handleMultipleSelectValue,
117
+ }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select an option' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
118
+ case 'user':
119
+ case 'owner':
120
+ return renderField(_jsxs("select", { id: fieldName, disabled: disabled, multiple: extendedField.multiple, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
121
+ required: field.required ? `${field.label || fieldName} is required` : false,
122
+ setValueAs: handleMultipleSelectValue,
123
+ }), children: [!extendedField.multiple && _jsx("option", { value: "", children: field.placeholder || 'Select user' }), extendedField.options?.map((option) => (_jsx("option", { value: option.value, disabled: option.disabled, children: option.label }, option.value)))] }));
124
+ case 'formula':
125
+ case 'summary':
126
+ case 'auto_number':
127
+ // Read-only computed fields - display as disabled text input
128
+ return renderField(_jsx("input", { id: fieldName, type: "text", disabled: true, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 cursor-not-allowed text-gray-600", ...register(fieldName) }));
129
+ case 'object':
130
+ return renderField(_jsx("textarea", { id: fieldName, placeholder: field.placeholder || 'Enter JSON object...', disabled: disabled, rows: 6, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono text-sm", ...register(fieldName, {
131
+ required: field.required ? `${field.label || fieldName} is required` : false,
132
+ validate: (value) => {
133
+ if (!value)
134
+ return true;
135
+ try {
136
+ JSON.parse(value);
137
+ return true;
138
+ }
139
+ catch (e) {
140
+ return 'Invalid JSON format';
141
+ }
142
+ },
143
+ }) }));
144
+ case 'vector':
145
+ // Vector fields are typically read-only or require specialized input
146
+ return renderField(_jsx("input", { id: fieldName, type: "text", placeholder: field.placeholder || 'Vector data (read-only)', disabled: true, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm bg-gray-50 cursor-not-allowed text-gray-600", ...register(fieldName) }));
147
+ case 'grid':
148
+ // Grid fields require complex table/grid editor - placeholder for now
149
+ return renderField(_jsx("div", { className: "w-full px-3 py-2 border border-gray-300 rounded-md bg-gray-50 text-gray-600", children: _jsx("p", { className: "text-sm", children: "Grid editor not yet implemented" }) }));
150
+ default:
151
+ // Default to text input
152
+ return renderField(_jsx("input", { id: fieldName, type: "text", placeholder: field.placeholder, disabled: disabled, className: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed", ...register(fieldName, {
153
+ required: field.required ? `${field.label || fieldName} is required` : false,
154
+ }) }));
155
+ }
156
+ };
157
+ FieldFactory.displayName = 'FieldFactory';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import React from 'react';
9
+ import type { FormView } from '@objectstack/spec/ui';
10
+ export interface FormRendererProps {
11
+ /**
12
+ * The FormView schema from @objectstack/spec
13
+ */
14
+ schema: FormView;
15
+ /**
16
+ * Initial form data
17
+ */
18
+ data?: Record<string, any>;
19
+ /**
20
+ * Form submission handler
21
+ */
22
+ onSubmit?: (data: Record<string, any>) => void | Promise<void>;
23
+ /**
24
+ * Form change handler
25
+ */
26
+ onChange?: (data: Record<string, any>) => void;
27
+ /**
28
+ * Custom class name for the form
29
+ */
30
+ className?: string;
31
+ /**
32
+ * Whether the form is disabled
33
+ */
34
+ disabled?: boolean;
35
+ }
36
+ /**
37
+ * FormRenderer component that renders forms based on FormViewSchema
38
+ * from @objectstack/spec
39
+ */
40
+ export declare const FormRenderer: React.FC<FormRendererProps>;
@@ -0,0 +1,87 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ObjectUI
4
+ * Copyright (c) 2024-present ObjectStack Inc.
5
+ *
6
+ * This source code is licensed under the MIT license found in the
7
+ * LICENSE file in the root directory of this source tree.
8
+ */
9
+ import React from 'react';
10
+ import { useForm, FormProvider } from 'react-hook-form';
11
+ import { FieldFactory } from './FieldFactory';
12
+ /**
13
+ * FormRenderer component that renders forms based on FormViewSchema
14
+ * from @objectstack/spec
15
+ */
16
+ export const FormRenderer = ({ schema, data = {}, onSubmit, onChange, className = '', disabled = false, }) => {
17
+ // Create form methods with react-hook-form
18
+ const methods = useForm({
19
+ defaultValues: data,
20
+ mode: 'onChange',
21
+ });
22
+ const { handleSubmit, watch } = methods;
23
+ // Watch for form changes
24
+ React.useEffect(() => {
25
+ if (onChange) {
26
+ const subscription = watch((value) => {
27
+ onChange(value);
28
+ });
29
+ return () => subscription.unsubscribe();
30
+ }
31
+ }, [watch, onChange]);
32
+ // Handle form submission
33
+ const onSubmitForm = handleSubmit(async (formData) => {
34
+ if (onSubmit) {
35
+ await onSubmit(formData);
36
+ }
37
+ });
38
+ // Render sections or fields
39
+ const renderContent = () => {
40
+ if (!schema.sections || schema.sections.length === 0) {
41
+ return null;
42
+ }
43
+ return (_jsx("div", { className: "space-y-6", children: schema.sections.map((section, index) => (_jsx(FormSectionRenderer, { section: section, methods: methods, disabled: disabled }, index))) }));
44
+ };
45
+ return (_jsx(FormProvider, { ...methods, children: _jsxs("form", { onSubmit: onSubmitForm, className: `space-y-6 ${className}`, children: [renderContent(), _jsx("div", { className: "flex justify-end gap-2", children: _jsx("button", { type: "submit", disabled: disabled, className: "px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed", children: "Submit" }) })] }) }));
46
+ };
47
+ // Grid column classes for different column counts
48
+ const GRID_COLS = {
49
+ 1: 'grid-cols-1',
50
+ 2: 'grid-cols-1 md:grid-cols-2',
51
+ 3: 'grid-cols-1 md:grid-cols-3',
52
+ 4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
53
+ };
54
+ // Column span classes
55
+ const COL_SPAN = {
56
+ 1: 'col-span-1',
57
+ 2: 'col-span-2',
58
+ 3: 'col-span-3',
59
+ 4: 'col-span-4',
60
+ };
61
+ /**
62
+ * Renders a form section with grid layout
63
+ */
64
+ const FormSectionRenderer = ({ section, methods, disabled = false, }) => {
65
+ const [isCollapsed, setIsCollapsed] = React.useState(section.collapsed || false);
66
+ // Determine grid columns based on section.columns
67
+ const gridCols = GRID_COLS[section.columns || 1];
68
+ const handleToggleCollapse = () => {
69
+ if (section.collapsible) {
70
+ setIsCollapsed(!isCollapsed);
71
+ }
72
+ };
73
+ return (_jsxs("div", { className: "border rounded-lg p-4", children: [section.label && (_jsxs("div", { className: `flex items-center justify-between mb-4 ${section.collapsible ? 'cursor-pointer' : ''}`, onClick: handleToggleCollapse, children: [_jsx("h3", { className: "text-lg font-semibold", children: section.label }), section.collapsible && (_jsx("span", { className: "text-sm text-gray-500", children: isCollapsed ? '▶' : '▼' }))] })), !isCollapsed && (_jsx("div", { className: `grid ${gridCols} gap-4`, children: section.fields.map((field, index) => {
74
+ // Handle both string fields (legacy) and FormFieldSchema objects
75
+ const fieldName = typeof field === 'string' ? field : field.field;
76
+ const fieldConfig = typeof field === 'string' ? { field: fieldName } : field;
77
+ // Skip hidden fields
78
+ if (fieldConfig.hidden) {
79
+ return null;
80
+ }
81
+ // Calculate colSpan for grid
82
+ const colSpan = fieldConfig.colSpan || 1;
83
+ const colSpanClass = COL_SPAN[Math.min(colSpan, section.columns || 1)];
84
+ return (_jsx("div", { className: colSpanClass, children: _jsx(FieldFactory, { field: fieldConfig, methods: methods, disabled: disabled || fieldConfig.readonly }) }, `${fieldName}-${index}`));
85
+ }) }))] }));
86
+ };
87
+ FormRenderer.displayName = 'FormRenderer';
@@ -0,0 +1,8 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+ import '@testing-library/jest-dom';