@object-ui/plugin-form 0.3.1 → 2.0.0

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 (34) hide show
  1. package/.turbo/turbo-build.log +21 -0
  2. package/CHANGELOG.md +15 -0
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1400 -280
  5. package/dist/index.umd.cjs +2 -2
  6. package/dist/packages/plugin-form/src/DrawerForm.d.ts +61 -0
  7. package/dist/packages/plugin-form/src/FormSection.d.ts +49 -0
  8. package/dist/packages/plugin-form/src/FormVariants.test.d.ts +0 -0
  9. package/dist/packages/plugin-form/src/ModalForm.d.ts +60 -0
  10. package/dist/packages/plugin-form/src/ObjectForm.msw.test.d.ts +0 -0
  11. package/dist/packages/plugin-form/src/ObjectForm.test.d.ts +1 -0
  12. package/dist/packages/plugin-form/src/SplitForm.d.ts +50 -0
  13. package/dist/packages/plugin-form/src/TabbedForm.d.ts +123 -0
  14. package/dist/packages/plugin-form/src/WizardForm.d.ts +112 -0
  15. package/dist/packages/plugin-form/src/__tests__/NewVariants.test.d.ts +8 -0
  16. package/dist/packages/plugin-form/src/index.d.ts +15 -0
  17. package/package.json +10 -8
  18. package/src/DrawerForm.tsx +385 -0
  19. package/src/FormSection.tsx +144 -0
  20. package/src/FormVariants.test.tsx +219 -0
  21. package/src/ModalForm.tsx +379 -0
  22. package/src/ObjectForm.msw.test.tsx +156 -0
  23. package/src/ObjectForm.test.tsx +61 -0
  24. package/src/ObjectForm.tsx +267 -15
  25. package/src/SplitForm.tsx +299 -0
  26. package/src/TabbedForm.tsx +394 -0
  27. package/src/WizardForm.tsx +501 -0
  28. package/src/__tests__/NewVariants.test.tsx +488 -0
  29. package/src/index.tsx +62 -2
  30. package/vite.config.ts +18 -0
  31. package/vitest.config.ts +12 -0
  32. package/vitest.setup.ts +1 -0
  33. package/dist/plugin-form/src/index.d.ts +0 -3
  34. /package/dist/{plugin-form → packages/plugin-form}/src/ObjectForm.d.ts +0 -0
@@ -0,0 +1,156 @@
1
+ import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { ObjectForm } from './ObjectForm';
5
+ import { ObjectStackAdapter } from '@object-ui/data-objectstack';
6
+ import { setupServer } from 'msw/node';
7
+ import { http, HttpResponse } from 'msw';
8
+ import { registerAllFields } from '@object-ui/fields';
9
+ import React from 'react';
10
+ import { ContactObject } from '../../../examples/crm/src/objects/contact.object';
11
+
12
+ // Register widget renderers
13
+ registerAllFields();
14
+
15
+ const BASE_URL = process.env.OBJECTSTACK_API_URL || 'http://localhost';
16
+
17
+ // --- Mock Data ---
18
+
19
+ const mockSchema = ContactObject;
20
+
21
+ const mockRecord = {
22
+ _id: '1',
23
+ name: 'Alice Johnson',
24
+ email: 'alice@example.com',
25
+ status: 'Active'
26
+ };
27
+
28
+ // --- MSW Setup ---
29
+
30
+ const handlers = [
31
+ // .well-known discovery endpoint (used by client.connect())
32
+ http.get(`${BASE_URL}/.well-known/objectstack`, () => {
33
+ return HttpResponse.json({
34
+ name: 'ObjectStack API',
35
+ version: '1.0',
36
+ endpoints: {
37
+ data: '/api/v1/data',
38
+ metadata: '/api/v1/meta'
39
+ },
40
+ capabilities: {
41
+ graphql: false,
42
+ search: false,
43
+ websockets: false,
44
+ files: true,
45
+ analytics: false,
46
+ hub: false
47
+ }
48
+ });
49
+ }),
50
+
51
+ // OPTIONS handler for CORS preflight
52
+ http.options(`${BASE_URL}/*`, () => {
53
+ return new HttpResponse(null, {
54
+ status: 200,
55
+ headers: {
56
+ 'Access-Control-Allow-Origin': '*',
57
+ 'Access-Control-Allow-Methods': 'GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH',
58
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
59
+ },
60
+ });
61
+ }),
62
+
63
+ // Health check / Connection check (ObjectStackClient often pings root or /api/v1)
64
+ http.get(`${BASE_URL}/api/v1`, () => {
65
+ return HttpResponse.json({ status: 'ok', version: '1.0.0' });
66
+ }),
67
+
68
+ // Mock Schema Fetch: GET /api/v1/metadata/object/:name and /api/v1/meta/object/:name (client uses /meta)
69
+ http.get(`${BASE_URL}/api/v1/metadata/object/:name`, ({ params }) => {
70
+ const { name } = params;
71
+ if (name === 'contact') {
72
+ return HttpResponse.json(mockSchema);
73
+ }
74
+ return new HttpResponse(null, { status: 404 });
75
+ }),
76
+ http.get(`${BASE_URL}/api/v1/meta/object/:name`, ({ params }) => {
77
+ const { name } = params;
78
+ if (name === 'contact') {
79
+ return HttpResponse.json(mockSchema);
80
+ }
81
+ return new HttpResponse(null, { status: 404 });
82
+ }),
83
+
84
+ // Mock Record Fetch: GET /api/v1/data/:object/:id
85
+ http.get(`${BASE_URL}/api/v1/data/:object/:id`, ({ params }) => {
86
+ const { object, id } = params;
87
+ if (object === 'contact' && id === '1') {
88
+ return HttpResponse.json({ record: mockRecord });
89
+ }
90
+ return new HttpResponse(null, { status: 404 });
91
+ })
92
+ ];
93
+
94
+ const server = setupServer(...handlers);
95
+
96
+ // --- Test Suite ---
97
+
98
+ describe('ObjectForm with ObjectStack/MSW', () => {
99
+ // Only start MSW if we are NOT using a real server
100
+ if (!process.env.OBJECTSTACK_API_URL) {
101
+ beforeAll(() => server.listen());
102
+ afterEach(() => server.resetHandlers());
103
+ afterAll(() => server.close());
104
+ }
105
+
106
+ // Create real adapter instance pointing to MSW or Real Server
107
+ const dataSource = new ObjectStackAdapter({
108
+ baseUrl: BASE_URL,
109
+ // Add custom fetch for environment that might need it, or rely on global fetch
110
+ // fetch: global.fetch
111
+ });
112
+
113
+ it('loads schema and renders form fields', async () => {
114
+ render(
115
+ <ObjectForm
116
+ schema={{
117
+ type: 'object-form',
118
+ objectName: 'contact', // Triggers schema fetch
119
+ mode: 'create'
120
+ }}
121
+ dataSource={dataSource} // Logic moves from mock fn to real adapter + MSW
122
+ />
123
+ );
124
+
125
+ // Verify fields appear (async as schema loads via HTTP)
126
+ await waitFor(() => {
127
+ // Changed from 'Full Name' to 'Name' based on CRM example schema
128
+ expect(screen.getByText('Name')).toBeInTheDocument();
129
+ }, { timeout: 2000 }); // Give slight buffer for network mock
130
+ expect(screen.getByText('Email')).toBeInTheDocument();
131
+ expect(screen.getByText('Status')).toBeInTheDocument();
132
+ });
133
+
134
+ it('loads record data in edit mode', async () => {
135
+ render(
136
+ <ObjectForm
137
+ schema={{
138
+ type: 'object-form',
139
+ objectName: 'contact',
140
+ mode: 'edit',
141
+ recordId: '1'
142
+ }}
143
+ dataSource={dataSource}
144
+ />
145
+ );
146
+
147
+ // Initial load of schema logic + data fetch
148
+ await waitFor(() => {
149
+ // Changed from 'Full Name' to 'Name'
150
+ expect(screen.getByRole('textbox', { name: /Name/i })).toHaveValue('Alice Johnson');
151
+ }, { timeout: 2000 }); // Give slight buffer for network mock
152
+
153
+ // Changed from 'Email Address' to 'Email'
154
+ expect(screen.getByRole('textbox', { name: /Email/i })).toHaveValue('alice@example.com');
155
+ });
156
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen, waitFor } from '@testing-library/react';
3
+ import { ObjectForm } from './ObjectForm';
4
+ import { registerAllFields } from '@object-ui/fields';
5
+ import React from 'react';
6
+
7
+ // Ensure fields are registered
8
+ registerAllFields();
9
+
10
+ describe('ObjectForm Integration', () => {
11
+ const objectSchema = {
12
+ name: 'test_object',
13
+ fields: {
14
+ name: {
15
+ type: 'text',
16
+ label: 'Name'
17
+ },
18
+ price: {
19
+ type: 'currency',
20
+ label: 'Price',
21
+ scale: 2
22
+ }
23
+ }
24
+ };
25
+
26
+ const mockDataSource: any = {
27
+ getObjectSchema: vi.fn().mockResolvedValue(objectSchema),
28
+ createRecord: vi.fn(),
29
+ updateRecord: vi.fn(),
30
+ getRecord: vi.fn(),
31
+ query: vi.fn()
32
+ };
33
+
34
+ it('renders fields using specialized components', async () => {
35
+ render(
36
+ <ObjectForm
37
+ schema={{
38
+ type: 'object-form',
39
+ objectName: 'test_object',
40
+ mode: 'create'
41
+ }}
42
+ dataSource={mockDataSource}
43
+ />
44
+ );
45
+
46
+ // Wait for schema to load (useEffect)
47
+ await waitFor(() => {
48
+ expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('test_object');
49
+ });
50
+
51
+ // Check if labels are present
52
+ await waitFor(() => {
53
+ expect(screen.queryByText('Name')).toBeTruthy();
54
+ });
55
+ expect(screen.getByText('Price')).toBeTruthy();
56
+
57
+ // Assert input exists
58
+ // Since we don't have getByLabelText working reliably without full accessibility tree in happy-dom sometimes,
59
+ // we can try looking for inputs.
60
+ });
61
+ });
@@ -17,6 +17,12 @@ import React, { useEffect, useState, useCallback } from 'react';
17
17
  import type { ObjectFormSchema, FormField, FormSchema, DataSource } from '@object-ui/types';
18
18
  import { SchemaRenderer } from '@object-ui/react';
19
19
  import { mapFieldTypeToFormType, buildValidationRules, evaluateCondition, formatFileSize } from '@object-ui/fields';
20
+ import { TabbedForm } from './TabbedForm';
21
+ import { WizardForm } from './WizardForm';
22
+ import { SplitForm } from './SplitForm';
23
+ import { DrawerForm } from './DrawerForm';
24
+ import { ModalForm } from './ModalForm';
25
+ import { FormSection } from './FormSection';
20
26
 
21
27
  export interface ObjectFormProps {
22
28
  /**
@@ -58,6 +64,144 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
58
64
  schema,
59
65
  dataSource,
60
66
  }) => {
67
+
68
+ // Route to specialized form variant based on formType
69
+ if (schema.formType === 'tabbed' && schema.sections?.length) {
70
+ return (
71
+ <TabbedForm
72
+ schema={{
73
+ ...schema,
74
+ formType: 'tabbed',
75
+ sections: schema.sections.map(s => ({
76
+ name: s.name,
77
+ label: s.label,
78
+ description: s.description,
79
+ columns: s.columns,
80
+ fields: s.fields,
81
+ })),
82
+ defaultTab: schema.defaultTab,
83
+ tabPosition: schema.tabPosition,
84
+ }}
85
+ dataSource={dataSource}
86
+ className={schema.className}
87
+ />
88
+ );
89
+ }
90
+
91
+ if (schema.formType === 'wizard' && schema.sections?.length) {
92
+ return (
93
+ <WizardForm
94
+ schema={{
95
+ ...schema,
96
+ formType: 'wizard',
97
+ sections: schema.sections.map(s => ({
98
+ name: s.name,
99
+ label: s.label,
100
+ description: s.description,
101
+ columns: s.columns,
102
+ fields: s.fields,
103
+ })),
104
+ allowSkip: schema.allowSkip,
105
+ showStepIndicator: schema.showStepIndicator,
106
+ nextText: schema.nextText,
107
+ prevText: schema.prevText,
108
+ onStepChange: schema.onStepChange,
109
+ }}
110
+ dataSource={dataSource}
111
+ className={schema.className}
112
+ />
113
+ );
114
+ }
115
+
116
+ if (schema.formType === 'split' && schema.sections?.length) {
117
+ return (
118
+ <SplitForm
119
+ schema={{
120
+ ...schema,
121
+ formType: 'split',
122
+ sections: schema.sections.map(s => ({
123
+ name: s.name,
124
+ label: s.label,
125
+ description: s.description,
126
+ columns: s.columns,
127
+ fields: s.fields,
128
+ })),
129
+ splitDirection: schema.splitDirection,
130
+ splitSize: schema.splitSize,
131
+ splitResizable: schema.splitResizable,
132
+ }}
133
+ dataSource={dataSource}
134
+ className={schema.className}
135
+ />
136
+ );
137
+ }
138
+
139
+ if (schema.formType === 'drawer') {
140
+ const { layout: _layout, ...drawerRest } = schema;
141
+ const drawerLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined;
142
+ return (
143
+ <DrawerForm
144
+ schema={{
145
+ ...drawerRest,
146
+ layout: drawerLayout,
147
+ formType: 'drawer',
148
+ sections: schema.sections?.map(s => ({
149
+ name: s.name,
150
+ label: s.label,
151
+ description: s.description,
152
+ columns: s.columns,
153
+ fields: s.fields,
154
+ })),
155
+ open: schema.open,
156
+ onOpenChange: schema.onOpenChange,
157
+ drawerSide: schema.drawerSide,
158
+ drawerWidth: schema.drawerWidth,
159
+ }}
160
+ dataSource={dataSource}
161
+ className={schema.className}
162
+ />
163
+ );
164
+ }
165
+
166
+ if (schema.formType === 'modal') {
167
+ const { layout: _layout2, ...modalRest } = schema;
168
+ const modalLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined;
169
+ return (
170
+ <ModalForm
171
+ schema={{
172
+ ...modalRest,
173
+ layout: modalLayout,
174
+ formType: 'modal',
175
+ sections: schema.sections?.map(s => ({
176
+ name: s.name,
177
+ label: s.label,
178
+ description: s.description,
179
+ columns: s.columns,
180
+ fields: s.fields,
181
+ })),
182
+ open: schema.open,
183
+ onOpenChange: schema.onOpenChange,
184
+ modalSize: schema.modalSize,
185
+ modalCloseButton: schema.modalCloseButton,
186
+ }}
187
+ dataSource={dataSource}
188
+ className={schema.className}
189
+ />
190
+ );
191
+ }
192
+
193
+ // Default: simple form
194
+ return <SimpleObjectForm schema={schema} dataSource={dataSource} />;
195
+ };
196
+
197
+ /**
198
+ * SimpleObjectForm — default form variant with auto-generated fields from ObjectQL schema.
199
+ */
200
+ const SimpleObjectForm: React.FC<ObjectFormProps> = ({
201
+ schema,
202
+ dataSource,
203
+ }) => {
204
+
61
205
  const [objectSchema, setObjectSchema] = useState<any>(null);
62
206
  const [formFields, setFormFields] = useState<FormField[]>([]);
63
207
  const [initialData, setInitialData] = useState<any>(null);
@@ -83,10 +227,13 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
83
227
  throw new Error('DataSource is required when using ObjectQL schema fetching (inline fields not provided)');
84
228
  }
85
229
  const schemaData = await dataSource.getObjectSchema(schema.objectName);
230
+ if (!schemaData) {
231
+ throw new Error(`No schema found for object "${schema.objectName}"`);
232
+ }
86
233
  setObjectSchema(schemaData);
87
234
  } catch (err) {
88
- console.error('Failed to fetch object schema:', err);
89
235
  setError(err as Error);
236
+ setLoading(false);
90
237
  }
91
238
  };
92
239
 
@@ -99,6 +246,9 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
99
246
  });
100
247
  } else if (schema.objectName && dataSource) {
101
248
  fetchObjectSchema();
249
+ } else if (!hasInlineFields) {
250
+ // No objectName or dataSource and no inline fields — cannot proceed
251
+ setLoading(false);
102
252
  }
103
253
  }, [schema.objectName, dataSource, hasInlineFields]);
104
254
 
@@ -107,6 +257,7 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
107
257
  const fetchInitialData = async () => {
108
258
  if (!schema.recordId || schema.mode === 'create') {
109
259
  setInitialData(schema.initialData || schema.initialValues || {});
260
+ setLoading(false);
110
261
  return;
111
262
  }
112
263
 
@@ -154,23 +305,33 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
154
305
  // Determine which fields to include
155
306
  const fieldsToShow = schema.fields || Object.keys(objectSchema.fields || {});
156
307
 
157
- fieldsToShow.forEach((fieldName) => {
158
- const field = objectSchema.fields?.[fieldName];
159
- if (!field) return;
308
+ // Support object format for fields in schema (legacy/compat)
309
+ const fieldNames = Array.isArray(fieldsToShow)
310
+ ? fieldsToShow
311
+ : Object.keys(fieldsToShow);
312
+
313
+ fieldNames.forEach((fieldName) => {
314
+ // If fieldsToShow is an array of strings, fieldName is the string
315
+ // If fieldsToShow is array of objects (unlikely but possible in some formats), we need to extract name
316
+ const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name;
317
+ if (!name) return;
318
+
319
+ const field = objectSchema.fields?.[name];
320
+ if (!field && !hasInlineFields) return; // Skip if not found in object definition unless inline
160
321
 
161
322
  // Check field-level permissions for create/edit modes
162
- const hasWritePermission = !field.permissions || field.permissions.write !== false;
323
+ const hasWritePermission = !field?.permissions || field?.permissions.write !== false;
163
324
  if (schema.mode !== 'view' && !hasWritePermission) return; // Skip fields without write permission
164
325
 
165
326
  // Check if there's a custom field configuration
166
- const customField = schema.customFields?.find(f => f.name === fieldName);
327
+ const customField = schema.customFields?.find(f => f.name === name);
167
328
 
168
329
  if (customField) {
169
330
  generatedFields.push(customField);
170
- } else {
331
+ } else if (field) {
171
332
  // Auto-generate field from schema
172
333
  const formField: FormField = {
173
- name: fieldName,
334
+ name: name,
174
335
  label: field.label || fieldName,
175
336
  type: mapFieldTypeToFormType(field.type),
176
337
  required: field.required || false,
@@ -178,6 +339,8 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
178
339
  placeholder: field.placeholder,
179
340
  description: field.help || field.description,
180
341
  validation: buildValidationRules(field),
342
+ // Important: Pass the original field metadata so widgets can access properties like precision, currency, etc.
343
+ field: field,
181
344
  };
182
345
 
183
346
  // Add field-specific properties
@@ -187,17 +350,27 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
187
350
  }
188
351
 
189
352
  if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {
353
+ formField.inputType = 'number';
190
354
  formField.min = field.min;
191
355
  formField.max = field.max;
192
356
  formField.step = field.precision ? Math.pow(10, -field.precision) : undefined;
193
357
  }
194
358
 
359
+ if (field.type === 'date') {
360
+ formField.inputType = 'date';
361
+ }
362
+
363
+ if (field.type === 'datetime') {
364
+ formField.inputType = 'datetime-local';
365
+ }
366
+
195
367
  if (field.type === 'text' || field.type === 'textarea' || field.type === 'markdown' || field.type === 'html') {
196
368
  formField.maxLength = field.max_length;
197
369
  formField.minLength = field.min_length;
198
370
  }
199
371
 
200
372
  if (field.type === 'file' || field.type === 'image') {
373
+ formField.inputType = 'file';
201
374
  formField.multiple = field.multiple;
202
375
  formField.accept = field.accept ? field.accept.join(',') : undefined;
203
376
  // Add validation hints for file size and dimensions
@@ -246,11 +419,29 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
246
419
  });
247
420
 
248
421
  setFormFields(generatedFields);
249
- setLoading(false);
250
- }, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode, hasInlineFields]);
422
+
423
+ // Only set loading to false if we are not going to fetch data
424
+ // This prevents a flash of empty form before data is loaded in edit mode
425
+ const willFetchData = !hasInlineFields && (schema.recordId && schema.mode !== 'create' && dataSource);
426
+ if (!willFetchData) {
427
+ setLoading(false);
428
+ }
429
+ }, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode, hasInlineFields, schema.recordId, dataSource]);
251
430
 
252
431
  // Handle form submission
253
- const handleSubmit = useCallback(async (formData: any) => {
432
+ const handleSubmit = useCallback(async (formData: any, e?: any) => {
433
+ // If we receive an event as the first argument, it means the Form renderer passed the event instead of data
434
+ // This happens when react-hook-form's handleSubmit is bypassed or configured incorrectly
435
+ if (formData && (formData.nativeEvent || formData._reactName === 'onSubmit')) {
436
+ console.warn('ObjectForm: Received Event instead of data in handleSubmit! This suggests a Form renderer issue.');
437
+ // Proceed defensively - we can't do much if we don't have data, but let's try to not crash
438
+ // If we are here, formData is actually the event
439
+ if (e === undefined) {
440
+ e = formData;
441
+ formData = {}; // Reset to empty object or we try to submit the Event object
442
+ }
443
+ }
444
+
254
445
  // For inline fields without a dataSource, just call the success callback
255
446
  if (hasInlineFields && !dataSource) {
256
447
  if (schema.onSuccess) {
@@ -299,6 +490,24 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
299
490
  }
300
491
  }, [schema]);
301
492
 
493
+ // Calculate default values from schema fields
494
+ const schemaDefaultValues = React.useMemo(() => {
495
+ if (!objectSchema?.fields) return {};
496
+ const defaults: Record<string, any> = {};
497
+ Object.keys(objectSchema.fields).forEach(key => {
498
+ const field = objectSchema.fields[key];
499
+ if (field.defaultValue !== undefined) {
500
+ defaults[key] = field.defaultValue;
501
+ }
502
+ });
503
+ return defaults;
504
+ }, [objectSchema]);
505
+
506
+ const finalDefaultValues = {
507
+ ...schemaDefaultValues,
508
+ ...initialData
509
+ };
510
+
302
511
  // Render error state
303
512
  if (error) {
304
513
  return (
@@ -322,19 +531,62 @@ export const ObjectForm: React.FC<ObjectFormProps> = ({
322
531
  // Convert to FormSchema
323
532
  // Note: FormSchema currently only supports 'vertical' and 'horizontal' layouts
324
533
  // Map 'grid' and 'inline' to 'vertical' as fallback
534
+ const formLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal')
535
+ ? schema.layout
536
+ : 'vertical';
537
+
538
+ // If sections are provided for the simple form, render with FormSection grouping
539
+ if (schema.sections?.length && (!schema.formType || schema.formType === 'simple')) {
540
+ return (
541
+ <div className="w-full space-y-6">
542
+ {schema.sections.map((section, index) => {
543
+ // Filter formFields to only include fields in this section
544
+ const sectionFieldNames = section.fields.map(f => typeof f === 'string' ? f : f.name);
545
+ const sectionFields = formFields.filter(f => sectionFieldNames.includes(f.name));
546
+
547
+ return (
548
+ <FormSection
549
+ key={section.name || section.label || index}
550
+ label={section.label}
551
+ description={section.description}
552
+ collapsible={section.collapsible}
553
+ collapsed={section.collapsed}
554
+ columns={section.columns}
555
+ >
556
+ <SchemaRenderer
557
+ schema={{
558
+ type: 'form',
559
+ fields: sectionFields,
560
+ layout: formLayout,
561
+ defaultValues: finalDefaultValues,
562
+ // Only show action buttons after the last section
563
+ showSubmit: index === schema.sections!.length - 1 && schema.showSubmit !== false && schema.mode !== 'view',
564
+ showCancel: index === schema.sections!.length - 1 && schema.showCancel !== false,
565
+ submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
566
+ cancelLabel: schema.cancelText,
567
+ onSubmit: handleSubmit,
568
+ onCancel: handleCancel,
569
+ } as FormSchema}
570
+ />
571
+ </FormSection>
572
+ );
573
+ })}
574
+ </div>
575
+ );
576
+ }
577
+
578
+ // Default flat form (no sections)
325
579
  const formSchema: FormSchema = {
326
580
  type: 'form',
327
581
  fields: formFields,
328
- layout: (schema.layout === 'vertical' || schema.layout === 'horizontal')
329
- ? schema.layout
330
- : 'vertical',
582
+ layout: formLayout,
331
583
  columns: schema.columns,
332
584
  submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
333
585
  cancelLabel: schema.cancelText,
334
586
  showSubmit: schema.showSubmit !== false && schema.mode !== 'view',
335
587
  showCancel: schema.showCancel !== false,
336
588
  resetOnSubmit: schema.showReset,
337
- defaultValues: initialData,
589
+ defaultValues: finalDefaultValues,
338
590
  onSubmit: handleSubmit,
339
591
  onCancel: handleCancel,
340
592
  className: schema.className,