@object-ui/plugin-form 3.0.3 → 3.1.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.
- package/.turbo/turbo-build.log +6 -6
- package/dist/index.js +1217 -861
- package/dist/index.umd.cjs +2 -2
- package/dist/plugin-form/src/EmbeddableForm.d.ts +49 -0
- package/dist/plugin-form/src/FormAnalytics.d.ts +38 -0
- package/dist/plugin-form/src/FormSection.d.ts +6 -0
- package/dist/plugin-form/src/autoLayout.d.ts +60 -0
- package/dist/plugin-form/src/index.d.ts +5 -0
- package/package.json +8 -8
- package/src/DrawerForm.tsx +49 -24
- package/src/EmbeddableForm.tsx +240 -0
- package/src/FormAnalytics.tsx +209 -0
- package/src/FormSection.tsx +9 -1
- package/src/ModalForm.tsx +145 -45
- package/src/ObjectForm.tsx +12 -4
- package/src/SplitForm.tsx +3 -2
- package/src/TabbedForm.tsx +3 -2
- package/src/WizardForm.tsx +3 -2
- package/src/__tests__/EmbeddableFormPrefill.test.tsx +186 -0
- package/src/__tests__/MobileUX.test.tsx +433 -0
- package/src/__tests__/NewVariants.test.tsx +196 -0
- package/src/__tests__/autoLayout.test.ts +342 -0
- package/src/autoLayout.ts +168 -0
- package/src/index.tsx +52 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
isWideFieldType,
|
|
4
|
+
isAutoGeneratedFieldType,
|
|
5
|
+
inferColumns,
|
|
6
|
+
inferModalSize,
|
|
7
|
+
applyAutoColSpan,
|
|
8
|
+
filterCreateModeFields,
|
|
9
|
+
applyAutoLayout,
|
|
10
|
+
} from '../autoLayout';
|
|
11
|
+
import type { FormField } from '@object-ui/types';
|
|
12
|
+
|
|
13
|
+
describe('autoLayout', () => {
|
|
14
|
+
describe('isWideFieldType', () => {
|
|
15
|
+
it('returns true for wide form field types', () => {
|
|
16
|
+
expect(isWideFieldType('field:textarea')).toBe(true);
|
|
17
|
+
expect(isWideFieldType('field:markdown')).toBe(true);
|
|
18
|
+
expect(isWideFieldType('field:html')).toBe(true);
|
|
19
|
+
expect(isWideFieldType('field:grid')).toBe(true);
|
|
20
|
+
expect(isWideFieldType('field:rich-text')).toBe(true);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('returns true for raw wide field types', () => {
|
|
24
|
+
expect(isWideFieldType('textarea')).toBe(true);
|
|
25
|
+
expect(isWideFieldType('markdown')).toBe(true);
|
|
26
|
+
expect(isWideFieldType('html')).toBe(true);
|
|
27
|
+
expect(isWideFieldType('grid')).toBe(true);
|
|
28
|
+
expect(isWideFieldType('rich-text')).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns false for narrow field types', () => {
|
|
32
|
+
expect(isWideFieldType('field:text')).toBe(false);
|
|
33
|
+
expect(isWideFieldType('field:number')).toBe(false);
|
|
34
|
+
expect(isWideFieldType('field:select')).toBe(false);
|
|
35
|
+
expect(isWideFieldType('text')).toBe(false);
|
|
36
|
+
expect(isWideFieldType('boolean')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('isAutoGeneratedFieldType', () => {
|
|
41
|
+
it('returns true for auto-generated types', () => {
|
|
42
|
+
expect(isAutoGeneratedFieldType('formula')).toBe(true);
|
|
43
|
+
expect(isAutoGeneratedFieldType('summary')).toBe(true);
|
|
44
|
+
expect(isAutoGeneratedFieldType('auto_number')).toBe(true);
|
|
45
|
+
expect(isAutoGeneratedFieldType('autonumber')).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('returns false for user-editable types', () => {
|
|
49
|
+
expect(isAutoGeneratedFieldType('text')).toBe(false);
|
|
50
|
+
expect(isAutoGeneratedFieldType('number')).toBe(false);
|
|
51
|
+
expect(isAutoGeneratedFieldType('select')).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('inferColumns', () => {
|
|
56
|
+
it('returns 1 column for 0 fields', () => {
|
|
57
|
+
expect(inferColumns(0)).toBe(1);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns 1 column for 1-3 fields', () => {
|
|
61
|
+
expect(inferColumns(1)).toBe(1);
|
|
62
|
+
expect(inferColumns(2)).toBe(1);
|
|
63
|
+
expect(inferColumns(3)).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('returns 2 columns for 4+ fields', () => {
|
|
67
|
+
expect(inferColumns(4)).toBe(2);
|
|
68
|
+
expect(inferColumns(8)).toBe(2);
|
|
69
|
+
expect(inferColumns(20)).toBe(2);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('inferModalSize', () => {
|
|
74
|
+
it('returns default for 0 or 1 column', () => {
|
|
75
|
+
expect(inferModalSize(0)).toBe('default');
|
|
76
|
+
expect(inferModalSize(1)).toBe('default');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('returns lg for 2 columns', () => {
|
|
80
|
+
expect(inferModalSize(2)).toBe('lg');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('returns xl for 3 columns', () => {
|
|
84
|
+
expect(inferModalSize(3)).toBe('xl');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('returns full for 4+ columns', () => {
|
|
88
|
+
expect(inferModalSize(4)).toBe('full');
|
|
89
|
+
expect(inferModalSize(5)).toBe('full');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('applyAutoColSpan', () => {
|
|
94
|
+
it('returns fields unchanged when columns is 1', () => {
|
|
95
|
+
const fields: FormField[] = [
|
|
96
|
+
{ name: 'a', label: 'A', type: 'field:textarea' },
|
|
97
|
+
];
|
|
98
|
+
const result = applyAutoColSpan(fields, 1);
|
|
99
|
+
expect(result).toEqual(fields);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('sets colSpan for wide fields in multi-column layout', () => {
|
|
103
|
+
const fields: FormField[] = [
|
|
104
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
105
|
+
{ name: 'desc', label: 'Description', type: 'field:textarea' },
|
|
106
|
+
{ name: 'notes', label: 'Notes', type: 'field:markdown' },
|
|
107
|
+
];
|
|
108
|
+
const result = applyAutoColSpan(fields, 2);
|
|
109
|
+
|
|
110
|
+
expect(result[0].colSpan).toBeUndefined();
|
|
111
|
+
expect(result[1].colSpan).toBe(2);
|
|
112
|
+
expect(result[2].colSpan).toBe(2);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('does not override user-defined colSpan', () => {
|
|
116
|
+
const fields: FormField[] = [
|
|
117
|
+
{ name: 'desc', label: 'Description', type: 'field:textarea', colSpan: 1 },
|
|
118
|
+
];
|
|
119
|
+
const result = applyAutoColSpan(fields, 2);
|
|
120
|
+
expect(result[0].colSpan).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('does not mutate original fields', () => {
|
|
124
|
+
const fields: FormField[] = [
|
|
125
|
+
{ name: 'desc', label: 'Description', type: 'field:textarea' },
|
|
126
|
+
];
|
|
127
|
+
const result = applyAutoColSpan(fields, 2);
|
|
128
|
+
expect(fields[0].colSpan).toBeUndefined();
|
|
129
|
+
expect(result[0].colSpan).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('filterCreateModeFields', () => {
|
|
134
|
+
const objectSchema = {
|
|
135
|
+
name: 'test',
|
|
136
|
+
fields: {
|
|
137
|
+
name: { type: 'text', label: 'Name' },
|
|
138
|
+
total: { type: 'formula', label: 'Total' },
|
|
139
|
+
count: { type: 'summary', label: 'Count' },
|
|
140
|
+
record_no: { type: 'auto_number', label: 'Record #' },
|
|
141
|
+
email: { type: 'email', label: 'Email' },
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
it('filters out formula, summary, and auto_number fields', () => {
|
|
146
|
+
const fields: FormField[] = [
|
|
147
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
148
|
+
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
149
|
+
{ name: 'count', label: 'Count', type: 'field:text' },
|
|
150
|
+
{ name: 'record_no', label: 'Record #', type: 'field:text' },
|
|
151
|
+
{ name: 'email', label: 'Email', type: 'field:text' },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
const result = filterCreateModeFields(fields, objectSchema);
|
|
155
|
+
|
|
156
|
+
expect(result).toHaveLength(2);
|
|
157
|
+
expect(result.map(f => f.name)).toEqual(['name', 'email']);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('keeps all fields when objectSchema has no fields metadata', () => {
|
|
161
|
+
const fields: FormField[] = [
|
|
162
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
163
|
+
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
164
|
+
];
|
|
165
|
+
|
|
166
|
+
const result = filterCreateModeFields(fields, { name: 'test' });
|
|
167
|
+
expect(result).toHaveLength(2);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('keeps custom fields not in object schema', () => {
|
|
171
|
+
const fields: FormField[] = [
|
|
172
|
+
{ name: 'custom_field', label: 'Custom', type: 'field:text' },
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const result = filterCreateModeFields(fields, objectSchema);
|
|
176
|
+
expect(result).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('applyAutoLayout', () => {
|
|
181
|
+
it('infers 1 column for 3 fields', () => {
|
|
182
|
+
const fields: FormField[] = [
|
|
183
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
184
|
+
{ name: 'b', label: 'B', type: 'field:number' },
|
|
185
|
+
{ name: 'c', label: 'C', type: 'field:select' },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const result = applyAutoLayout(fields, null, undefined, 'create');
|
|
189
|
+
expect(result.columns).toBe(1);
|
|
190
|
+
expect(result.fields).toHaveLength(3);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('infers 2 columns for 5 fields', () => {
|
|
194
|
+
const fields: FormField[] = [
|
|
195
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
196
|
+
{ name: 'b', label: 'B', type: 'field:text' },
|
|
197
|
+
{ name: 'c', label: 'C', type: 'field:text' },
|
|
198
|
+
{ name: 'd', label: 'D', type: 'field:text' },
|
|
199
|
+
{ name: 'e', label: 'E', type: 'field:text' },
|
|
200
|
+
];
|
|
201
|
+
|
|
202
|
+
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
203
|
+
expect(result.columns).toBe(2);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('applies colSpan to wide fields when columns > 1', () => {
|
|
207
|
+
const fields: FormField[] = [
|
|
208
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
209
|
+
{ name: 'b', label: 'B', type: 'field:text' },
|
|
210
|
+
{ name: 'c', label: 'C', type: 'field:text' },
|
|
211
|
+
{ name: 'd', label: 'D', type: 'field:textarea' },
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
215
|
+
expect(result.columns).toBe(2);
|
|
216
|
+
expect(result.fields[3].colSpan).toBe(2);
|
|
217
|
+
// Regular fields should not have colSpan
|
|
218
|
+
expect(result.fields[0].colSpan).toBeUndefined();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('filters auto-generated fields in create mode', () => {
|
|
222
|
+
const fields: FormField[] = [
|
|
223
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
224
|
+
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
225
|
+
{ name: 'email', label: 'Email', type: 'field:text' },
|
|
226
|
+
{ name: 'count', label: 'Count', type: 'field:text' },
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
const objectSchema = {
|
|
230
|
+
name: 'test',
|
|
231
|
+
fields: {
|
|
232
|
+
name: { type: 'text' },
|
|
233
|
+
total: { type: 'formula' },
|
|
234
|
+
email: { type: 'email' },
|
|
235
|
+
count: { type: 'summary' },
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const result = applyAutoLayout(fields, objectSchema, undefined, 'create');
|
|
240
|
+
expect(result.fields.map(f => f.name)).toEqual(['name', 'email']);
|
|
241
|
+
expect(result.columns).toBe(1); // Only 2 fields after filtering → 1 column
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('does not filter auto-generated fields in edit mode', () => {
|
|
245
|
+
const fields: FormField[] = [
|
|
246
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
247
|
+
{ name: 'total', label: 'Total', type: 'field:text' },
|
|
248
|
+
];
|
|
249
|
+
|
|
250
|
+
const objectSchema = {
|
|
251
|
+
name: 'test',
|
|
252
|
+
fields: {
|
|
253
|
+
name: { type: 'text' },
|
|
254
|
+
total: { type: 'formula' },
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const result = applyAutoLayout(fields, objectSchema, undefined, 'edit');
|
|
259
|
+
expect(result.fields).toHaveLength(2);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('respects user-provided columns', () => {
|
|
263
|
+
const fields: FormField[] = [
|
|
264
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
265
|
+
{ name: 'b', label: 'B', type: 'field:text' },
|
|
266
|
+
];
|
|
267
|
+
|
|
268
|
+
const result = applyAutoLayout(fields, null, 3, 'edit');
|
|
269
|
+
expect(result.columns).toBe(3);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('applies auto colSpan even with user-provided columns', () => {
|
|
273
|
+
const fields: FormField[] = [
|
|
274
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
275
|
+
{ name: 'b', label: 'B', type: 'field:textarea' },
|
|
276
|
+
];
|
|
277
|
+
|
|
278
|
+
const result = applyAutoLayout(fields, null, 3, 'edit');
|
|
279
|
+
expect(result.columns).toBe(3);
|
|
280
|
+
expect(result.fields[1].colSpan).toBe(3);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('does not override user-defined colSpan on fields', () => {
|
|
284
|
+
const fields: FormField[] = [
|
|
285
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
286
|
+
{ name: 'b', label: 'B', type: 'field:textarea', colSpan: 1 },
|
|
287
|
+
{ name: 'c', label: 'C', type: 'field:text' },
|
|
288
|
+
{ name: 'd', label: 'D', type: 'field:text' },
|
|
289
|
+
];
|
|
290
|
+
|
|
291
|
+
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
292
|
+
expect(result.columns).toBe(2);
|
|
293
|
+
expect(result.fields[1].colSpan).toBe(1); // User override preserved
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('does not mutate original fields array', () => {
|
|
297
|
+
const fields: FormField[] = [
|
|
298
|
+
{ name: 'a', label: 'A', type: 'field:text' },
|
|
299
|
+
{ name: 'b', label: 'B', type: 'field:textarea' },
|
|
300
|
+
{ name: 'c', label: 'C', type: 'field:text' },
|
|
301
|
+
{ name: 'd', label: 'D', type: 'field:text' },
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
const result = applyAutoLayout(fields, null, undefined, 'edit');
|
|
305
|
+
expect(fields[1].colSpan).toBeUndefined();
|
|
306
|
+
expect(result.fields[1].colSpan).toBe(2);
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('handles empty fields array', () => {
|
|
310
|
+
const result = applyAutoLayout([], null, undefined, 'create');
|
|
311
|
+
expect(result.fields).toEqual([]);
|
|
312
|
+
expect(result.columns).toBe(1);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('infers columns based on field count after create-mode filtering', () => {
|
|
316
|
+
// Start with 5 fields, but 3 are auto-generated → 2 remain → 1 column
|
|
317
|
+
const fields: FormField[] = [
|
|
318
|
+
{ name: 'name', label: 'Name', type: 'field:text' },
|
|
319
|
+
{ name: 'f1', label: 'F1', type: 'field:text' },
|
|
320
|
+
{ name: 'f2', label: 'F2', type: 'field:text' },
|
|
321
|
+
{ name: 'f3', label: 'F3', type: 'field:text' },
|
|
322
|
+
{ name: 'f4', label: 'F4', type: 'field:text' },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
const objectSchema = {
|
|
326
|
+
name: 'test',
|
|
327
|
+
fields: {
|
|
328
|
+
name: { type: 'text' },
|
|
329
|
+
f1: { type: 'formula' },
|
|
330
|
+
f2: { type: 'summary' },
|
|
331
|
+
f3: { type: 'auto_number' },
|
|
332
|
+
f4: { type: 'text' },
|
|
333
|
+
},
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
const result = applyAutoLayout(fields, objectSchema, undefined, 'create');
|
|
337
|
+
// Only 'name' and 'f4' remain after filtering
|
|
338
|
+
expect(result.fields).toHaveLength(2);
|
|
339
|
+
expect(result.columns).toBe(1);
|
|
340
|
+
});
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
* Auto-Layout for ObjectForm
|
|
11
|
+
*
|
|
12
|
+
* Provides intelligent, zero-configuration default layout for metadata-driven forms.
|
|
13
|
+
* When the user has not explicitly set columns/colSpan/sections, this module
|
|
14
|
+
* infers optimal layout based on field count and field types.
|
|
15
|
+
*
|
|
16
|
+
* Priority: User configuration > Auto-layout inference
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { FormField } from '@object-ui/types';
|
|
20
|
+
|
|
21
|
+
/** Field types that should span full width in multi-column layouts */
|
|
22
|
+
const WIDE_FIELD_TYPES = new Set([
|
|
23
|
+
'field:textarea',
|
|
24
|
+
'field:markdown',
|
|
25
|
+
'field:html',
|
|
26
|
+
'field:grid',
|
|
27
|
+
'field:rich-text',
|
|
28
|
+
'textarea',
|
|
29
|
+
'markdown',
|
|
30
|
+
'html',
|
|
31
|
+
'grid',
|
|
32
|
+
'rich-text',
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/** Object field types that are auto-generated and should be hidden in create mode */
|
|
36
|
+
const AUTO_GENERATED_FIELD_TYPES = new Set([
|
|
37
|
+
'formula',
|
|
38
|
+
'summary',
|
|
39
|
+
'auto_number',
|
|
40
|
+
'autonumber',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Check if a field type is "wide" (should span full row in multi-column layout).
|
|
45
|
+
*/
|
|
46
|
+
export function isWideFieldType(type: string): boolean {
|
|
47
|
+
return WIDE_FIELD_TYPES.has(type);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Check if an object field type is auto-generated (formula, summary, auto_number).
|
|
52
|
+
*/
|
|
53
|
+
export function isAutoGeneratedFieldType(type: string): boolean {
|
|
54
|
+
return AUTO_GENERATED_FIELD_TYPES.has(type);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Infer optimal number of columns based on the number of visible fields
|
|
59
|
+
* and whether any wide fields are present.
|
|
60
|
+
*
|
|
61
|
+
* Rules:
|
|
62
|
+
* - 0-3 fields → 1 column
|
|
63
|
+
* - 4+ fields → 2 columns
|
|
64
|
+
*/
|
|
65
|
+
export function inferColumns(fieldCount: number): number {
|
|
66
|
+
if (fieldCount <= 3) return 1;
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply colSpan to wide fields so they span the full row.
|
|
72
|
+
* Only sets colSpan if the field does not already have one explicitly set.
|
|
73
|
+
*
|
|
74
|
+
* @returns A new array of fields with colSpan applied where needed.
|
|
75
|
+
*/
|
|
76
|
+
export function applyAutoColSpan(fields: FormField[], columns: number): FormField[] {
|
|
77
|
+
if (columns <= 1) return fields;
|
|
78
|
+
|
|
79
|
+
return fields.map(field => {
|
|
80
|
+
// User-defined colSpan takes priority
|
|
81
|
+
if (field.colSpan !== undefined) return field;
|
|
82
|
+
|
|
83
|
+
// Wide field types should span full row
|
|
84
|
+
if (field.type && isWideFieldType(field.type)) {
|
|
85
|
+
return { ...field, colSpan: columns };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return field;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Filter out auto-generated/readonly fields for create mode.
|
|
94
|
+
* These fields (formula, summary, auto_number) are computed server-side
|
|
95
|
+
* and should not appear in create forms.
|
|
96
|
+
*
|
|
97
|
+
* @param fields - The form fields array
|
|
98
|
+
* @param objectSchema - The object schema with original field metadata
|
|
99
|
+
* @returns Filtered fields array
|
|
100
|
+
*/
|
|
101
|
+
export function filterCreateModeFields(
|
|
102
|
+
fields: FormField[],
|
|
103
|
+
objectSchema: any
|
|
104
|
+
): FormField[] {
|
|
105
|
+
if (!objectSchema?.fields) return fields;
|
|
106
|
+
|
|
107
|
+
return fields.filter(field => {
|
|
108
|
+
const objField = objectSchema.fields[field.name];
|
|
109
|
+
if (!objField) return true; // keep fields not in schema (custom fields)
|
|
110
|
+
|
|
111
|
+
return !isAutoGeneratedFieldType(objField.type);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Infer an appropriate modal size based on the number of layout columns.
|
|
117
|
+
* Used to auto-upgrade the modal width when auto-layout detects multi-column forms.
|
|
118
|
+
*
|
|
119
|
+
* Mapping:
|
|
120
|
+
* - 1 column → 'default' (max-w-lg)
|
|
121
|
+
* - 2 columns → 'lg' (max-w-2xl)
|
|
122
|
+
* - 3 columns → 'xl' (max-w-4xl)
|
|
123
|
+
* - 4+ columns → 'full' (max-w-[95vw])
|
|
124
|
+
*/
|
|
125
|
+
export function inferModalSize(columns: number): 'sm' | 'default' | 'lg' | 'xl' | 'full' {
|
|
126
|
+
if (columns <= 1) return 'default';
|
|
127
|
+
if (columns === 2) return 'lg';
|
|
128
|
+
if (columns === 3) return 'xl';
|
|
129
|
+
return 'full';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Main auto-layout orchestrator.
|
|
134
|
+
* Applies intelligent defaults only when the user has not explicitly configured layout.
|
|
135
|
+
*
|
|
136
|
+
* @param formFields - Generated form fields
|
|
137
|
+
* @param objectSchema - The original object schema (with field types)
|
|
138
|
+
* @param schemaColumns - User-provided columns (from ObjectFormSchema)
|
|
139
|
+
* @param mode - Form mode ('create' | 'edit' | 'view')
|
|
140
|
+
* @returns Object with processed fields and inferred columns
|
|
141
|
+
*/
|
|
142
|
+
export function applyAutoLayout(
|
|
143
|
+
formFields: FormField[],
|
|
144
|
+
objectSchema: any,
|
|
145
|
+
schemaColumns: number | undefined,
|
|
146
|
+
mode: string | undefined
|
|
147
|
+
): { fields: FormField[]; columns: number | undefined } {
|
|
148
|
+
let fields = [...formFields];
|
|
149
|
+
|
|
150
|
+
// Step 1: Filter auto-generated fields in create mode
|
|
151
|
+
if (mode === 'create') {
|
|
152
|
+
fields = filterCreateModeFields(fields, objectSchema);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Step 2: If user explicitly set columns, respect it but still apply auto colSpan
|
|
156
|
+
if (schemaColumns !== undefined) {
|
|
157
|
+
fields = applyAutoColSpan(fields, schemaColumns);
|
|
158
|
+
return { fields, columns: schemaColumns };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Step 3: Infer columns from field count
|
|
162
|
+
const columns = inferColumns(fields.length);
|
|
163
|
+
|
|
164
|
+
// Step 4: Apply auto colSpan for wide fields
|
|
165
|
+
fields = applyAutoColSpan(fields, columns);
|
|
166
|
+
|
|
167
|
+
return { fields, columns };
|
|
168
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -14,6 +14,15 @@ export { ObjectForm };
|
|
|
14
14
|
export type { ObjectFormProps } from './ObjectForm';
|
|
15
15
|
export { FormSection } from './FormSection';
|
|
16
16
|
export type { FormSectionProps } from './FormSection';
|
|
17
|
+
export {
|
|
18
|
+
applyAutoLayout,
|
|
19
|
+
inferColumns,
|
|
20
|
+
inferModalSize,
|
|
21
|
+
isWideFieldType,
|
|
22
|
+
isAutoGeneratedFieldType,
|
|
23
|
+
applyAutoColSpan,
|
|
24
|
+
filterCreateModeFields,
|
|
25
|
+
} from './autoLayout';
|
|
17
26
|
export { TabbedForm } from './TabbedForm';
|
|
18
27
|
export type { TabbedFormProps, TabbedFormSchema, FormSectionConfig } from './TabbedForm';
|
|
19
28
|
export { WizardForm } from './WizardForm';
|
|
@@ -24,6 +33,10 @@ export { DrawerForm } from './DrawerForm';
|
|
|
24
33
|
export type { DrawerFormProps, DrawerFormSchema } from './DrawerForm';
|
|
25
34
|
export { ModalForm } from './ModalForm';
|
|
26
35
|
export type { ModalFormProps, ModalFormSchema } from './ModalForm';
|
|
36
|
+
export { EmbeddableForm } from './EmbeddableForm';
|
|
37
|
+
export type { EmbeddableFormProps, EmbeddableFormConfig } from './EmbeddableForm';
|
|
38
|
+
export { FormAnalytics } from './FormAnalytics';
|
|
39
|
+
export type { FormAnalyticsProps, FormSubmissionMetric } from './FormAnalytics';
|
|
27
40
|
|
|
28
41
|
// Register object-form component
|
|
29
42
|
const ObjectFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
@@ -80,3 +93,42 @@ ComponentRegistry.register('form', ObjectFormRenderer, {
|
|
|
80
93
|
// Note: 'form' type (without namespace) is handled by @object-ui/components Form component
|
|
81
94
|
// This plugin registers as 'view:form' (with view namespace) for the view protocol
|
|
82
95
|
// ObjectForm internally uses { type: 'form' } to render the basic Form component
|
|
96
|
+
|
|
97
|
+
// Register embeddable-form component for standalone public forms
|
|
98
|
+
import { EmbeddableForm } from './EmbeddableForm';
|
|
99
|
+
|
|
100
|
+
const EmbeddableFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
101
|
+
return <EmbeddableForm config={schema} />;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
ComponentRegistry.register('embeddable-form', EmbeddableFormRenderer, {
|
|
105
|
+
namespace: 'plugin-form',
|
|
106
|
+
label: 'Embeddable Form',
|
|
107
|
+
category: 'plugin',
|
|
108
|
+
inputs: [
|
|
109
|
+
{ name: 'formId', type: 'string', label: 'Form ID', required: true },
|
|
110
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
111
|
+
{ name: 'title', type: 'string', label: 'Form Title' },
|
|
112
|
+
{ name: 'description', type: 'string', label: 'Description' },
|
|
113
|
+
{ name: 'fields', type: 'array', label: 'Fields' },
|
|
114
|
+
{ name: 'allowMultiple', type: 'boolean', label: 'Allow Multiple Submissions' },
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Register form-analytics component for submission dashboards
|
|
119
|
+
import { FormAnalytics } from './FormAnalytics';
|
|
120
|
+
|
|
121
|
+
const FormAnalyticsRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
122
|
+
return <FormAnalytics formId={schema.formId} formTitle={schema.formTitle} metrics={schema.metrics || { totalSubmissions: 0 }} />;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
ComponentRegistry.register('form-analytics', FormAnalyticsRenderer, {
|
|
126
|
+
namespace: 'plugin-form',
|
|
127
|
+
label: 'Form Analytics',
|
|
128
|
+
category: 'plugin',
|
|
129
|
+
inputs: [
|
|
130
|
+
{ name: 'formId', type: 'string', label: 'Form ID', required: true },
|
|
131
|
+
{ name: 'formTitle', type: 'string', label: 'Form Title' },
|
|
132
|
+
{ name: 'metrics', type: 'object', label: 'Submission Metrics' },
|
|
133
|
+
]
|
|
134
|
+
});
|