@object-ui/plugin-form 3.3.0 → 3.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.
- package/CHANGELOG.md +11 -0
- package/README.md +21 -1
- package/dist/index.js +109 -66
- package/dist/index.umd.cjs +2 -2
- package/dist/packages/plugin-form/src/DrawerForm.d.ts +2 -0
- package/dist/packages/plugin-form/src/autoLayout.d.ts +11 -4
- package/package.json +42 -10
- package/.turbo/turbo-build.log +0 -32
- package/src/DrawerForm.tsx +0 -410
- package/src/EmbeddableForm.tsx +0 -240
- package/src/FormAnalytics.tsx +0 -209
- package/src/FormSection.tsx +0 -152
- package/src/FormVariants.test.tsx +0 -219
- package/src/ModalForm.tsx +0 -485
- package/src/ObjectForm.msw.test.tsx +0 -156
- package/src/ObjectForm.stories.tsx +0 -85
- package/src/ObjectForm.test.tsx +0 -61
- package/src/ObjectForm.tsx +0 -609
- package/src/SplitForm.tsx +0 -300
- package/src/TabbedForm.tsx +0 -395
- package/src/WizardForm.tsx +0 -502
- package/src/__tests__/EmbeddableFormPrefill.test.tsx +0 -186
- package/src/__tests__/MobileUX.test.tsx +0 -433
- package/src/__tests__/NewVariants.test.tsx +0 -684
- package/src/__tests__/autoLayout.test.ts +0 -339
- package/src/__tests__/form-validation-submit.test.tsx +0 -286
- package/src/autoLayout.ts +0 -166
- package/src/index.tsx +0 -134
- package/tsconfig.json +0 -9
- package/vite.config.ts +0 -58
- package/vitest.config.ts +0 -12
- package/vitest.setup.ts +0 -1
package/src/ObjectForm.tsx
DELETED
|
@@ -1,609 +0,0 @@
|
|
|
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
|
-
|
|
9
|
-
/**
|
|
10
|
-
* ObjectForm Component
|
|
11
|
-
*
|
|
12
|
-
* A smart form component that generates forms from ObjectQL object schemas.
|
|
13
|
-
* It automatically creates form fields based on object metadata.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import React, { useEffect, useState, useCallback } from 'react';
|
|
17
|
-
import type { ObjectFormSchema, FormField, FormSchema, DataSource } from '@object-ui/types';
|
|
18
|
-
import { SchemaRenderer, useSafeFieldLabel } from '@object-ui/react';
|
|
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';
|
|
26
|
-
import { applyAutoLayout } from './autoLayout';
|
|
27
|
-
|
|
28
|
-
export interface ObjectFormProps {
|
|
29
|
-
/**
|
|
30
|
-
* The schema configuration for the form
|
|
31
|
-
*/
|
|
32
|
-
schema: ObjectFormSchema;
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Data source (ObjectQL or ObjectStack adapter)
|
|
36
|
-
* Optional when using inline field definitions (customFields or fields array with field objects)
|
|
37
|
-
*/
|
|
38
|
-
dataSource?: DataSource;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Additional CSS class
|
|
42
|
-
*/
|
|
43
|
-
className?: string;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* ObjectForm Component
|
|
48
|
-
*
|
|
49
|
-
* Renders a form for an ObjectQL object with automatic schema integration.
|
|
50
|
-
*
|
|
51
|
-
* @example
|
|
52
|
-
* ```tsx
|
|
53
|
-
* <ObjectForm
|
|
54
|
-
* schema={{
|
|
55
|
-
* type: 'object-form',
|
|
56
|
-
* objectName: 'users',
|
|
57
|
-
* mode: 'create',
|
|
58
|
-
* fields: ['name', 'email', 'status']
|
|
59
|
-
* }}
|
|
60
|
-
* dataSource={dataSource}
|
|
61
|
-
* />
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
export const ObjectForm: React.FC<ObjectFormProps> = ({
|
|
65
|
-
schema,
|
|
66
|
-
dataSource,
|
|
67
|
-
}) => {
|
|
68
|
-
|
|
69
|
-
// Route to specialized form variant based on formType
|
|
70
|
-
if (schema.formType === 'tabbed' && schema.sections?.length) {
|
|
71
|
-
return (
|
|
72
|
-
<TabbedForm
|
|
73
|
-
schema={{
|
|
74
|
-
...schema,
|
|
75
|
-
formType: 'tabbed',
|
|
76
|
-
sections: schema.sections.map(s => ({
|
|
77
|
-
name: s.name,
|
|
78
|
-
label: s.label,
|
|
79
|
-
description: s.description,
|
|
80
|
-
columns: s.columns,
|
|
81
|
-
fields: s.fields,
|
|
82
|
-
})),
|
|
83
|
-
defaultTab: schema.defaultTab,
|
|
84
|
-
tabPosition: schema.tabPosition,
|
|
85
|
-
}}
|
|
86
|
-
dataSource={dataSource}
|
|
87
|
-
className={schema.className}
|
|
88
|
-
/>
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (schema.formType === 'wizard' && schema.sections?.length) {
|
|
93
|
-
return (
|
|
94
|
-
<WizardForm
|
|
95
|
-
schema={{
|
|
96
|
-
...schema,
|
|
97
|
-
formType: 'wizard',
|
|
98
|
-
sections: schema.sections.map(s => ({
|
|
99
|
-
name: s.name,
|
|
100
|
-
label: s.label,
|
|
101
|
-
description: s.description,
|
|
102
|
-
columns: s.columns,
|
|
103
|
-
fields: s.fields,
|
|
104
|
-
})),
|
|
105
|
-
allowSkip: schema.allowSkip,
|
|
106
|
-
showStepIndicator: schema.showStepIndicator,
|
|
107
|
-
nextText: schema.nextText,
|
|
108
|
-
prevText: schema.prevText,
|
|
109
|
-
onStepChange: schema.onStepChange,
|
|
110
|
-
}}
|
|
111
|
-
dataSource={dataSource}
|
|
112
|
-
className={schema.className}
|
|
113
|
-
/>
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (schema.formType === 'split' && schema.sections?.length) {
|
|
118
|
-
return (
|
|
119
|
-
<SplitForm
|
|
120
|
-
schema={{
|
|
121
|
-
...schema,
|
|
122
|
-
formType: 'split',
|
|
123
|
-
sections: schema.sections.map(s => ({
|
|
124
|
-
name: s.name,
|
|
125
|
-
label: s.label,
|
|
126
|
-
description: s.description,
|
|
127
|
-
columns: s.columns,
|
|
128
|
-
fields: s.fields,
|
|
129
|
-
})),
|
|
130
|
-
splitDirection: schema.splitDirection,
|
|
131
|
-
splitSize: schema.splitSize,
|
|
132
|
-
splitResizable: schema.splitResizable,
|
|
133
|
-
}}
|
|
134
|
-
dataSource={dataSource}
|
|
135
|
-
className={schema.className}
|
|
136
|
-
/>
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (schema.formType === 'drawer') {
|
|
141
|
-
const { layout: _layout, ...drawerRest } = schema;
|
|
142
|
-
const drawerLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined;
|
|
143
|
-
return (
|
|
144
|
-
<DrawerForm
|
|
145
|
-
schema={{
|
|
146
|
-
...drawerRest,
|
|
147
|
-
layout: drawerLayout,
|
|
148
|
-
formType: 'drawer',
|
|
149
|
-
sections: schema.sections?.map(s => ({
|
|
150
|
-
name: s.name,
|
|
151
|
-
label: s.label,
|
|
152
|
-
description: s.description,
|
|
153
|
-
columns: s.columns,
|
|
154
|
-
fields: s.fields,
|
|
155
|
-
})),
|
|
156
|
-
open: schema.open,
|
|
157
|
-
onOpenChange: schema.onOpenChange,
|
|
158
|
-
drawerSide: schema.drawerSide,
|
|
159
|
-
drawerWidth: schema.drawerWidth,
|
|
160
|
-
}}
|
|
161
|
-
dataSource={dataSource}
|
|
162
|
-
className={schema.className}
|
|
163
|
-
/>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (schema.formType === 'modal') {
|
|
168
|
-
const { layout: _layout2, ...modalRest } = schema;
|
|
169
|
-
const modalLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal') ? schema.layout : undefined;
|
|
170
|
-
return (
|
|
171
|
-
<ModalForm
|
|
172
|
-
schema={{
|
|
173
|
-
...modalRest,
|
|
174
|
-
layout: modalLayout,
|
|
175
|
-
formType: 'modal',
|
|
176
|
-
sections: schema.sections?.map(s => ({
|
|
177
|
-
name: s.name,
|
|
178
|
-
label: s.label,
|
|
179
|
-
description: s.description,
|
|
180
|
-
columns: s.columns,
|
|
181
|
-
fields: s.fields,
|
|
182
|
-
})),
|
|
183
|
-
open: schema.open,
|
|
184
|
-
onOpenChange: schema.onOpenChange,
|
|
185
|
-
modalSize: schema.modalSize,
|
|
186
|
-
modalCloseButton: schema.modalCloseButton,
|
|
187
|
-
}}
|
|
188
|
-
dataSource={dataSource}
|
|
189
|
-
className={schema.className}
|
|
190
|
-
/>
|
|
191
|
-
);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// Default: simple form
|
|
195
|
-
return <SimpleObjectForm schema={schema} dataSource={dataSource} />;
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
/**
|
|
199
|
-
* SimpleObjectForm — default form variant with auto-generated fields from ObjectQL schema.
|
|
200
|
-
*/
|
|
201
|
-
const SimpleObjectForm: React.FC<ObjectFormProps> = ({
|
|
202
|
-
schema,
|
|
203
|
-
dataSource,
|
|
204
|
-
}) => {
|
|
205
|
-
const { fieldLabel } = useSafeFieldLabel();
|
|
206
|
-
|
|
207
|
-
const [objectSchema, setObjectSchema] = useState<any>(null);
|
|
208
|
-
const [formFields, setFormFields] = useState<FormField[]>([]);
|
|
209
|
-
const [initialData, setInitialData] = useState<any>(null);
|
|
210
|
-
const [loading, setLoading] = useState(true);
|
|
211
|
-
const [error, setError] = useState<Error | null>(null);
|
|
212
|
-
|
|
213
|
-
// Check if using inline fields (fields defined as objects, not just names)
|
|
214
|
-
const hasInlineFields = schema.customFields && schema.customFields.length > 0;
|
|
215
|
-
|
|
216
|
-
// Initialize with inline data if provided
|
|
217
|
-
useEffect(() => {
|
|
218
|
-
if (hasInlineFields) {
|
|
219
|
-
setInitialData(schema.initialData || schema.initialValues || {});
|
|
220
|
-
setLoading(false);
|
|
221
|
-
}
|
|
222
|
-
}, [hasInlineFields, schema.initialData, schema.initialValues]);
|
|
223
|
-
|
|
224
|
-
// Fetch object schema from ObjectQL/ObjectStack (skip if using inline fields)
|
|
225
|
-
useEffect(() => {
|
|
226
|
-
const fetchObjectSchema = async () => {
|
|
227
|
-
try {
|
|
228
|
-
if (!dataSource) {
|
|
229
|
-
throw new Error('DataSource is required when using ObjectQL schema fetching (inline fields not provided)');
|
|
230
|
-
}
|
|
231
|
-
const schemaData = await dataSource.getObjectSchema(schema.objectName);
|
|
232
|
-
if (!schemaData) {
|
|
233
|
-
throw new Error(`No schema found for object "${schema.objectName}"`);
|
|
234
|
-
}
|
|
235
|
-
setObjectSchema(schemaData);
|
|
236
|
-
} catch (err) {
|
|
237
|
-
setError(err as Error);
|
|
238
|
-
setLoading(false);
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
// Skip fetching if we have inline fields
|
|
243
|
-
if (hasInlineFields) {
|
|
244
|
-
// Use a minimal schema for inline fields
|
|
245
|
-
setObjectSchema({
|
|
246
|
-
name: schema.objectName,
|
|
247
|
-
fields: {} as Record<string, any>,
|
|
248
|
-
});
|
|
249
|
-
} else if (schema.objectName && dataSource) {
|
|
250
|
-
fetchObjectSchema();
|
|
251
|
-
} else if (!hasInlineFields) {
|
|
252
|
-
// No objectName or dataSource and no inline fields — cannot proceed
|
|
253
|
-
setLoading(false);
|
|
254
|
-
}
|
|
255
|
-
}, [schema.objectName, dataSource, hasInlineFields]);
|
|
256
|
-
|
|
257
|
-
// Fetch initial data for edit/view modes (skip if using inline data)
|
|
258
|
-
useEffect(() => {
|
|
259
|
-
const fetchInitialData = async () => {
|
|
260
|
-
if (!schema.recordId || schema.mode === 'create') {
|
|
261
|
-
setInitialData(schema.initialData || schema.initialValues || {});
|
|
262
|
-
setLoading(false);
|
|
263
|
-
return;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
// Skip fetching if using inline data
|
|
267
|
-
if (hasInlineFields) {
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (!dataSource) {
|
|
272
|
-
setError(new Error('DataSource is required for fetching record data (inline data not provided)'));
|
|
273
|
-
setLoading(false);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
setLoading(true);
|
|
278
|
-
try {
|
|
279
|
-
const data = await dataSource.findOne(schema.objectName, schema.recordId);
|
|
280
|
-
setInitialData(data);
|
|
281
|
-
} catch (err) {
|
|
282
|
-
console.error('Failed to fetch record:', err);
|
|
283
|
-
setError(err as Error);
|
|
284
|
-
} finally {
|
|
285
|
-
setLoading(false);
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
if (objectSchema && !hasInlineFields) {
|
|
290
|
-
fetchInitialData();
|
|
291
|
-
}
|
|
292
|
-
}, [schema.objectName, schema.recordId, schema.mode, schema.initialValues, schema.initialData, dataSource, objectSchema, hasInlineFields]);
|
|
293
|
-
|
|
294
|
-
// Generate form fields from object schema or inline fields
|
|
295
|
-
useEffect(() => {
|
|
296
|
-
// For inline fields, use them directly
|
|
297
|
-
if (hasInlineFields && schema.customFields) {
|
|
298
|
-
setFormFields(schema.customFields);
|
|
299
|
-
setLoading(false);
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
if (!objectSchema) return;
|
|
304
|
-
|
|
305
|
-
const generatedFields: FormField[] = [];
|
|
306
|
-
|
|
307
|
-
// Determine which fields to include
|
|
308
|
-
const fieldsToShow = schema.fields || Object.keys(objectSchema.fields || {});
|
|
309
|
-
|
|
310
|
-
// Support object format for fields in schema (legacy/compat)
|
|
311
|
-
const fieldNames = Array.isArray(fieldsToShow)
|
|
312
|
-
? fieldsToShow
|
|
313
|
-
: Object.keys(fieldsToShow);
|
|
314
|
-
|
|
315
|
-
fieldNames.forEach((fieldName) => {
|
|
316
|
-
// If fieldsToShow is an array of strings, fieldName is the string
|
|
317
|
-
// If fieldsToShow is array of objects (unlikely but possible in some formats), we need to extract name
|
|
318
|
-
const name = typeof fieldName === 'string' ? fieldName : (fieldName as any).name;
|
|
319
|
-
if (!name) return;
|
|
320
|
-
|
|
321
|
-
const field = objectSchema.fields?.[name];
|
|
322
|
-
if (!field && !hasInlineFields) return; // Skip if not found in object definition unless inline
|
|
323
|
-
|
|
324
|
-
// Check field-level permissions for create/edit modes
|
|
325
|
-
const hasWritePermission = !field?.permissions || field?.permissions.write !== false;
|
|
326
|
-
if (schema.mode !== 'view' && !hasWritePermission) return; // Skip fields without write permission
|
|
327
|
-
|
|
328
|
-
// Check if there's a custom field configuration
|
|
329
|
-
const customField = schema.customFields?.find(f => f.name === name);
|
|
330
|
-
|
|
331
|
-
if (customField) {
|
|
332
|
-
generatedFields.push(customField);
|
|
333
|
-
} else if (field) {
|
|
334
|
-
// Auto-generate field from schema
|
|
335
|
-
const formField: FormField = {
|
|
336
|
-
name: name,
|
|
337
|
-
label: fieldLabel(schema.objectName, name, field.label || fieldName),
|
|
338
|
-
type: mapFieldTypeToFormType(field.type),
|
|
339
|
-
required: field.required || false,
|
|
340
|
-
disabled: schema.readOnly || schema.mode === 'view' || field.readonly,
|
|
341
|
-
placeholder: field.placeholder,
|
|
342
|
-
description: field.help || field.description,
|
|
343
|
-
validation: buildValidationRules(field),
|
|
344
|
-
// Important: Pass the original field metadata so widgets can access properties like precision, currency, etc.
|
|
345
|
-
field: field,
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
// Add field-specific properties
|
|
349
|
-
if (field.type === 'select' || field.type === 'lookup' || field.type === 'master_detail') {
|
|
350
|
-
formField.options = field.options || [];
|
|
351
|
-
formField.multiple = field.multiple;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
if (field.type === 'number' || field.type === 'currency' || field.type === 'percent') {
|
|
355
|
-
formField.inputType = 'number';
|
|
356
|
-
formField.min = field.min;
|
|
357
|
-
formField.max = field.max;
|
|
358
|
-
formField.step = field.precision ? Math.pow(10, -field.precision) : undefined;
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
if (field.type === 'date') {
|
|
362
|
-
formField.inputType = 'date';
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
if (field.type === 'datetime') {
|
|
366
|
-
formField.inputType = 'datetime-local';
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (field.type === 'text' || field.type === 'textarea' || field.type === 'markdown' || field.type === 'html') {
|
|
370
|
-
formField.maxLength = field.max_length;
|
|
371
|
-
formField.minLength = field.min_length;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (field.type === 'file' || field.type === 'image') {
|
|
375
|
-
formField.inputType = 'file';
|
|
376
|
-
formField.multiple = field.multiple;
|
|
377
|
-
formField.accept = field.accept ? field.accept.join(',') : undefined;
|
|
378
|
-
// Add validation hints for file size and dimensions
|
|
379
|
-
if (field.max_size) {
|
|
380
|
-
const sizeHint = `Max size: ${formatFileSize(field.max_size)}`;
|
|
381
|
-
formField.description = formField.description
|
|
382
|
-
? `${formField.description} (${sizeHint})`
|
|
383
|
-
: sizeHint;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (field.type === 'email') {
|
|
388
|
-
formField.inputType = 'email';
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (field.type === 'phone') {
|
|
392
|
-
formField.inputType = 'tel';
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (field.type === 'url') {
|
|
396
|
-
formField.inputType = 'url';
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
if (field.type === 'password') {
|
|
400
|
-
formField.inputType = 'password';
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (field.type === 'time') {
|
|
404
|
-
formField.inputType = 'time';
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Read-only fields for computed types
|
|
408
|
-
if (field.type === 'formula' || field.type === 'summary' || field.type === 'auto_number') {
|
|
409
|
-
formField.disabled = true;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Add conditional visibility based on field dependencies
|
|
413
|
-
if (field.visible_on) {
|
|
414
|
-
formField.visible = (formData: any) => {
|
|
415
|
-
return evaluateCondition(field.visible_on, formData);
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
generatedFields.push(formField);
|
|
420
|
-
}
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
setFormFields(generatedFields);
|
|
424
|
-
|
|
425
|
-
// Only set loading to false if we are not going to fetch data
|
|
426
|
-
// This prevents a flash of empty form before data is loaded in edit mode
|
|
427
|
-
const willFetchData = !hasInlineFields && (schema.recordId && schema.mode !== 'create' && dataSource);
|
|
428
|
-
if (!willFetchData) {
|
|
429
|
-
setLoading(false);
|
|
430
|
-
}
|
|
431
|
-
}, [objectSchema, schema.fields, schema.customFields, schema.readOnly, schema.mode, hasInlineFields, schema.recordId, dataSource]);
|
|
432
|
-
|
|
433
|
-
// Handle form submission
|
|
434
|
-
const handleSubmit = useCallback(async (formData: any, e?: any) => {
|
|
435
|
-
// If we receive an event as the first argument, it means the Form renderer passed the event instead of data
|
|
436
|
-
// This happens when react-hook-form's handleSubmit is bypassed or configured incorrectly
|
|
437
|
-
if (formData && (formData.nativeEvent || formData._reactName === 'onSubmit')) {
|
|
438
|
-
console.warn('ObjectForm: Received Event instead of data in handleSubmit! This suggests a Form renderer issue.');
|
|
439
|
-
// Proceed defensively - we can't do much if we don't have data, but let's try to not crash
|
|
440
|
-
// If we are here, formData is actually the event
|
|
441
|
-
if (e === undefined) {
|
|
442
|
-
e = formData;
|
|
443
|
-
formData = {}; // Reset to empty object or we try to submit the Event object
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
// For inline fields without a dataSource, just call the success callback
|
|
448
|
-
if (hasInlineFields && !dataSource) {
|
|
449
|
-
if (schema.onSuccess) {
|
|
450
|
-
await schema.onSuccess(formData);
|
|
451
|
-
}
|
|
452
|
-
return formData;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
if (!dataSource) {
|
|
456
|
-
throw new Error('DataSource is required for form submission (inline mode not configured)');
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
try {
|
|
460
|
-
let result;
|
|
461
|
-
|
|
462
|
-
if (schema.mode === 'create') {
|
|
463
|
-
result = await dataSource.create(schema.objectName, formData);
|
|
464
|
-
} else if (schema.mode === 'edit' && schema.recordId) {
|
|
465
|
-
result = await dataSource.update(schema.objectName, schema.recordId, formData);
|
|
466
|
-
} else {
|
|
467
|
-
throw new Error('Invalid form mode or missing record ID');
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// Call success callback if provided
|
|
471
|
-
if (schema.onSuccess) {
|
|
472
|
-
await schema.onSuccess(result);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
return result;
|
|
476
|
-
} catch (err) {
|
|
477
|
-
console.error('Failed to submit form:', err);
|
|
478
|
-
|
|
479
|
-
// Call error callback if provided
|
|
480
|
-
if (schema.onError) {
|
|
481
|
-
schema.onError(err as Error);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
throw err;
|
|
485
|
-
}
|
|
486
|
-
}, [schema, dataSource, hasInlineFields]);
|
|
487
|
-
|
|
488
|
-
// Handle form cancellation
|
|
489
|
-
const handleCancel = useCallback(() => {
|
|
490
|
-
if (schema.onCancel) {
|
|
491
|
-
schema.onCancel();
|
|
492
|
-
}
|
|
493
|
-
}, [schema]);
|
|
494
|
-
|
|
495
|
-
// Calculate default values from schema fields
|
|
496
|
-
const schemaDefaultValues = React.useMemo(() => {
|
|
497
|
-
if (!objectSchema?.fields) return {};
|
|
498
|
-
const defaults: Record<string, any> = {};
|
|
499
|
-
Object.keys(objectSchema.fields).forEach(key => {
|
|
500
|
-
const field = objectSchema.fields[key];
|
|
501
|
-
if (field.defaultValue !== undefined) {
|
|
502
|
-
defaults[key] = field.defaultValue;
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
return defaults;
|
|
506
|
-
}, [objectSchema]);
|
|
507
|
-
|
|
508
|
-
const finalDefaultValues = {
|
|
509
|
-
...schemaDefaultValues,
|
|
510
|
-
...initialData
|
|
511
|
-
};
|
|
512
|
-
|
|
513
|
-
// Render error state
|
|
514
|
-
if (error) {
|
|
515
|
-
return (
|
|
516
|
-
<div className="p-3 sm:p-4 border border-red-300 bg-red-50 rounded-md">
|
|
517
|
-
<h3 className="text-red-800 font-semibold">Error loading form</h3>
|
|
518
|
-
<p className="text-red-600 text-sm mt-1">{error.message}</p>
|
|
519
|
-
</div>
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Render loading state
|
|
524
|
-
if (loading) {
|
|
525
|
-
return (
|
|
526
|
-
<div className="p-4 sm:p-8 text-center">
|
|
527
|
-
<div className="inline-block animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
|
|
528
|
-
<p className="mt-2 text-sm text-gray-600">Loading form...</p>
|
|
529
|
-
</div>
|
|
530
|
-
);
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Convert to FormSchema
|
|
534
|
-
// Note: FormSchema currently only supports 'vertical' and 'horizontal' layouts
|
|
535
|
-
// Map 'grid' and 'inline' to 'vertical' as fallback
|
|
536
|
-
const formLayout = (schema.layout === 'vertical' || schema.layout === 'horizontal')
|
|
537
|
-
? schema.layout
|
|
538
|
-
: 'vertical';
|
|
539
|
-
|
|
540
|
-
// If sections are provided for the simple form, render with FormSection grouping
|
|
541
|
-
if (schema.sections?.length && (!schema.formType || schema.formType === 'simple')) {
|
|
542
|
-
return (
|
|
543
|
-
<div className="w-full space-y-6">
|
|
544
|
-
{schema.sections.map((section, index) => {
|
|
545
|
-
// Filter formFields to only include fields in this section
|
|
546
|
-
const sectionFieldNames = section.fields.map(f => typeof f === 'string' ? f : f.name);
|
|
547
|
-
const sectionFields = formFields.filter(f => sectionFieldNames.includes(f.name));
|
|
548
|
-
|
|
549
|
-
return (
|
|
550
|
-
<FormSection
|
|
551
|
-
key={section.name || section.label || index}
|
|
552
|
-
label={section.label}
|
|
553
|
-
description={section.description}
|
|
554
|
-
collapsible={section.collapsible}
|
|
555
|
-
collapsed={section.collapsed}
|
|
556
|
-
columns={section.columns}
|
|
557
|
-
>
|
|
558
|
-
<SchemaRenderer
|
|
559
|
-
schema={{
|
|
560
|
-
type: 'form',
|
|
561
|
-
fields: sectionFields,
|
|
562
|
-
layout: formLayout,
|
|
563
|
-
defaultValues: finalDefaultValues,
|
|
564
|
-
// Only show action buttons after the last section
|
|
565
|
-
showSubmit: index === schema.sections!.length - 1 && schema.showSubmit !== false && schema.mode !== 'view',
|
|
566
|
-
showCancel: index === schema.sections!.length - 1 && schema.showCancel !== false,
|
|
567
|
-
submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
|
|
568
|
-
cancelLabel: schema.cancelText,
|
|
569
|
-
onSubmit: handleSubmit,
|
|
570
|
-
onCancel: handleCancel,
|
|
571
|
-
} as FormSchema}
|
|
572
|
-
/>
|
|
573
|
-
</FormSection>
|
|
574
|
-
);
|
|
575
|
-
})}
|
|
576
|
-
</div>
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Apply auto-layout: infer columns and colSpan when not explicitly configured
|
|
581
|
-
const hasSections = schema.sections?.length;
|
|
582
|
-
const autoLayoutResult = !hasSections
|
|
583
|
-
? applyAutoLayout(formFields, objectSchema, schema.columns, schema.mode)
|
|
584
|
-
: { fields: formFields, columns: schema.columns };
|
|
585
|
-
|
|
586
|
-
// Default flat form (no sections)
|
|
587
|
-
const formSchema: FormSchema = {
|
|
588
|
-
type: 'form',
|
|
589
|
-
fields: autoLayoutResult.fields,
|
|
590
|
-
layout: formLayout,
|
|
591
|
-
columns: autoLayoutResult.columns,
|
|
592
|
-
submitLabel: schema.submitText || (schema.mode === 'create' ? 'Create' : 'Update'),
|
|
593
|
-
cancelLabel: schema.cancelText,
|
|
594
|
-
showSubmit: schema.showSubmit !== false && schema.mode !== 'view',
|
|
595
|
-
showCancel: schema.showCancel !== false,
|
|
596
|
-
resetOnSubmit: schema.showReset,
|
|
597
|
-
defaultValues: finalDefaultValues,
|
|
598
|
-
onSubmit: handleSubmit,
|
|
599
|
-
onCancel: handleCancel,
|
|
600
|
-
className: schema.className,
|
|
601
|
-
};
|
|
602
|
-
|
|
603
|
-
return (
|
|
604
|
-
<div className="w-full">
|
|
605
|
-
<SchemaRenderer schema={formSchema} />
|
|
606
|
-
</div>
|
|
607
|
-
);
|
|
608
|
-
};
|
|
609
|
-
|