@object-ui/plugin-form 0.5.0 → 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.
- package/.turbo/turbo-build.log +8 -6
- package/CHANGELOG.md +15 -0
- package/dist/index.js +1388 -283
- package/dist/index.umd.cjs +2 -2
- package/dist/packages/plugin-form/src/DrawerForm.d.ts +61 -0
- package/dist/packages/plugin-form/src/FormSection.d.ts +49 -0
- package/dist/packages/plugin-form/src/FormVariants.test.d.ts +0 -0
- package/dist/packages/plugin-form/src/ModalForm.d.ts +60 -0
- package/dist/packages/plugin-form/src/SplitForm.d.ts +50 -0
- package/dist/packages/plugin-form/src/TabbedForm.d.ts +123 -0
- package/dist/packages/plugin-form/src/WizardForm.d.ts +112 -0
- package/dist/packages/plugin-form/src/__tests__/NewVariants.test.d.ts +8 -0
- package/dist/packages/plugin-form/src/index.d.ts +12 -0
- package/package.json +8 -8
- package/src/DrawerForm.tsx +385 -0
- package/src/FormSection.tsx +144 -0
- package/src/FormVariants.test.tsx +219 -0
- package/src/ModalForm.tsx +379 -0
- package/src/ObjectForm.msw.test.tsx +29 -2
- package/src/ObjectForm.tsx +204 -6
- package/src/SplitForm.tsx +299 -0
- package/src/TabbedForm.tsx +394 -0
- package/src/WizardForm.tsx +501 -0
- package/src/__tests__/NewVariants.test.tsx +488 -0
- package/src/index.tsx +60 -3
- package/vitest.config.ts +12 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,488 @@
|
|
|
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
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
+
import { render, screen, waitFor } from '@testing-library/react';
|
|
11
|
+
import React from 'react';
|
|
12
|
+
import { ObjectForm } from '../ObjectForm';
|
|
13
|
+
import { SplitForm } from '../SplitForm';
|
|
14
|
+
import { DrawerForm } from '../DrawerForm';
|
|
15
|
+
import { ModalForm } from '../ModalForm';
|
|
16
|
+
|
|
17
|
+
// Mock dataSource used across tests
|
|
18
|
+
const mockObjectSchema = {
|
|
19
|
+
name: 'contacts',
|
|
20
|
+
fields: {
|
|
21
|
+
firstName: { label: 'First Name', type: 'text', required: true },
|
|
22
|
+
lastName: { label: 'Last Name', type: 'text', required: false },
|
|
23
|
+
email: { label: 'Email', type: 'email', required: true },
|
|
24
|
+
phone: { label: 'Phone', type: 'phone', required: false },
|
|
25
|
+
street: { label: 'Street', type: 'text', required: false },
|
|
26
|
+
city: { label: 'City', type: 'text', required: false },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const createMockDataSource = () => ({
|
|
31
|
+
getObjectSchema: vi.fn().mockResolvedValue(mockObjectSchema),
|
|
32
|
+
findOne: vi.fn().mockResolvedValue({ firstName: 'John', lastName: 'Doe' }),
|
|
33
|
+
find: vi.fn().mockResolvedValue([]),
|
|
34
|
+
create: vi.fn().mockResolvedValue({ id: '1' }),
|
|
35
|
+
update: vi.fn().mockResolvedValue({ id: '1' }),
|
|
36
|
+
delete: vi.fn().mockResolvedValue(true),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ─── SplitForm Tests ────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('SplitForm', () => {
|
|
42
|
+
it('renders with split panel layout', async () => {
|
|
43
|
+
const mockDataSource = createMockDataSource();
|
|
44
|
+
|
|
45
|
+
render(
|
|
46
|
+
<SplitForm
|
|
47
|
+
schema={{
|
|
48
|
+
type: 'object-form',
|
|
49
|
+
formType: 'split',
|
|
50
|
+
objectName: 'contacts',
|
|
51
|
+
mode: 'create',
|
|
52
|
+
sections: [
|
|
53
|
+
{
|
|
54
|
+
label: 'Personal',
|
|
55
|
+
fields: ['firstName', 'lastName'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
label: 'Contact',
|
|
59
|
+
fields: ['email', 'phone'],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
}}
|
|
63
|
+
dataSource={mockDataSource as any}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
// Wait for schema to load
|
|
68
|
+
await waitFor(() => {
|
|
69
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Should show section labels
|
|
73
|
+
await waitFor(() => {
|
|
74
|
+
expect(screen.getByText('Personal')).toBeInTheDocument();
|
|
75
|
+
});
|
|
76
|
+
expect(screen.getByText('Contact')).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders loading state initially', () => {
|
|
80
|
+
const mockDataSource = createMockDataSource();
|
|
81
|
+
// Make getObjectSchema hang (never resolve)
|
|
82
|
+
mockDataSource.getObjectSchema.mockReturnValue(new Promise(() => {}));
|
|
83
|
+
|
|
84
|
+
render(
|
|
85
|
+
<SplitForm
|
|
86
|
+
schema={{
|
|
87
|
+
type: 'object-form',
|
|
88
|
+
formType: 'split',
|
|
89
|
+
objectName: 'contacts',
|
|
90
|
+
mode: 'create',
|
|
91
|
+
sections: [
|
|
92
|
+
{ label: 'Section 1', fields: ['firstName'] },
|
|
93
|
+
{ label: 'Section 2', fields: ['email'] },
|
|
94
|
+
],
|
|
95
|
+
}}
|
|
96
|
+
dataSource={mockDataSource as any}
|
|
97
|
+
/>
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText('Loading form...')).toBeInTheDocument();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('shows error state on fetch failure', async () => {
|
|
104
|
+
const mockDataSource = createMockDataSource();
|
|
105
|
+
mockDataSource.getObjectSchema.mockRejectedValue(new Error('Network error'));
|
|
106
|
+
|
|
107
|
+
render(
|
|
108
|
+
<SplitForm
|
|
109
|
+
schema={{
|
|
110
|
+
type: 'object-form',
|
|
111
|
+
formType: 'split',
|
|
112
|
+
objectName: 'contacts',
|
|
113
|
+
mode: 'create',
|
|
114
|
+
sections: [
|
|
115
|
+
{ label: 'Section 1', fields: ['firstName'] },
|
|
116
|
+
{ label: 'Section 2', fields: ['email'] },
|
|
117
|
+
],
|
|
118
|
+
}}
|
|
119
|
+
dataSource={mockDataSource as any}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
await waitFor(() => {
|
|
124
|
+
expect(screen.getByText('Error loading form')).toBeInTheDocument();
|
|
125
|
+
});
|
|
126
|
+
expect(screen.getByText('Network error')).toBeInTheDocument();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ─── DrawerForm Tests ───────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
describe('DrawerForm', () => {
|
|
133
|
+
it('renders inside a sheet with title and description', async () => {
|
|
134
|
+
const mockDataSource = createMockDataSource();
|
|
135
|
+
|
|
136
|
+
render(
|
|
137
|
+
<DrawerForm
|
|
138
|
+
schema={{
|
|
139
|
+
type: 'object-form',
|
|
140
|
+
formType: 'drawer',
|
|
141
|
+
objectName: 'contacts',
|
|
142
|
+
mode: 'create',
|
|
143
|
+
title: 'Create Contact',
|
|
144
|
+
description: 'Fill in the contact details',
|
|
145
|
+
open: true,
|
|
146
|
+
fields: ['firstName', 'lastName', 'email'],
|
|
147
|
+
}}
|
|
148
|
+
dataSource={mockDataSource as any}
|
|
149
|
+
/>
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
// Wait for schema and rendering
|
|
153
|
+
await waitFor(() => {
|
|
154
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// Title and description should be visible
|
|
158
|
+
await waitFor(() => {
|
|
159
|
+
expect(screen.getByText('Create Contact')).toBeInTheDocument();
|
|
160
|
+
});
|
|
161
|
+
expect(screen.getByText('Fill in the contact details')).toBeInTheDocument();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('calls onOpenChange when closing', async () => {
|
|
165
|
+
const onOpenChange = vi.fn();
|
|
166
|
+
const mockDataSource = createMockDataSource();
|
|
167
|
+
|
|
168
|
+
render(
|
|
169
|
+
<DrawerForm
|
|
170
|
+
schema={{
|
|
171
|
+
type: 'object-form',
|
|
172
|
+
formType: 'drawer',
|
|
173
|
+
objectName: 'contacts',
|
|
174
|
+
mode: 'create',
|
|
175
|
+
title: 'Create Contact',
|
|
176
|
+
open: true,
|
|
177
|
+
onOpenChange,
|
|
178
|
+
fields: ['firstName'],
|
|
179
|
+
}}
|
|
180
|
+
dataSource={mockDataSource as any}
|
|
181
|
+
/>
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// The close button (X) in the Sheet should exist
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
const closeButton = screen.getByRole('button', { name: /close/i });
|
|
187
|
+
expect(closeButton).toBeInTheDocument();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('renders with sections layout inside drawer', async () => {
|
|
192
|
+
const mockDataSource = createMockDataSource();
|
|
193
|
+
|
|
194
|
+
render(
|
|
195
|
+
<DrawerForm
|
|
196
|
+
schema={{
|
|
197
|
+
type: 'object-form',
|
|
198
|
+
formType: 'drawer',
|
|
199
|
+
objectName: 'contacts',
|
|
200
|
+
mode: 'create',
|
|
201
|
+
title: 'New Contact',
|
|
202
|
+
open: true,
|
|
203
|
+
sections: [
|
|
204
|
+
{
|
|
205
|
+
label: 'Personal Info',
|
|
206
|
+
fields: ['firstName', 'lastName'],
|
|
207
|
+
columns: 2,
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: 'Contact Details',
|
|
211
|
+
fields: ['email', 'phone'],
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
}}
|
|
215
|
+
dataSource={mockDataSource as any}
|
|
216
|
+
/>
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
await waitFor(() => {
|
|
220
|
+
expect(screen.getByText('New Contact')).toBeInTheDocument();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
await waitFor(() => {
|
|
224
|
+
expect(screen.getByText('Personal Info')).toBeInTheDocument();
|
|
225
|
+
});
|
|
226
|
+
expect(screen.getByText('Contact Details')).toBeInTheDocument();
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('renders when open is false (hidden)', () => {
|
|
230
|
+
const mockDataSource = createMockDataSource();
|
|
231
|
+
|
|
232
|
+
const { container } = render(
|
|
233
|
+
<DrawerForm
|
|
234
|
+
schema={{
|
|
235
|
+
type: 'object-form',
|
|
236
|
+
formType: 'drawer',
|
|
237
|
+
objectName: 'contacts',
|
|
238
|
+
mode: 'create',
|
|
239
|
+
open: false,
|
|
240
|
+
fields: ['firstName'],
|
|
241
|
+
}}
|
|
242
|
+
dataSource={mockDataSource as any}
|
|
243
|
+
/>
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// When open=false, the sheet should not render content
|
|
247
|
+
expect(screen.queryByText('Create Contact')).not.toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ─── ModalForm Tests ────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
describe('ModalForm', () => {
|
|
254
|
+
it('renders inside a dialog with title and description', async () => {
|
|
255
|
+
const mockDataSource = createMockDataSource();
|
|
256
|
+
|
|
257
|
+
render(
|
|
258
|
+
<ModalForm
|
|
259
|
+
schema={{
|
|
260
|
+
type: 'object-form',
|
|
261
|
+
formType: 'modal',
|
|
262
|
+
objectName: 'contacts',
|
|
263
|
+
mode: 'create',
|
|
264
|
+
title: 'Create Contact',
|
|
265
|
+
description: 'Enter contact information',
|
|
266
|
+
open: true,
|
|
267
|
+
fields: ['firstName', 'lastName', 'email'],
|
|
268
|
+
}}
|
|
269
|
+
dataSource={mockDataSource as any}
|
|
270
|
+
/>
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
await waitFor(() => {
|
|
274
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
await waitFor(() => {
|
|
278
|
+
expect(screen.getByText('Create Contact')).toBeInTheDocument();
|
|
279
|
+
});
|
|
280
|
+
expect(screen.getByText('Enter contact information')).toBeInTheDocument();
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it('renders with sections layout inside modal', async () => {
|
|
284
|
+
const mockDataSource = createMockDataSource();
|
|
285
|
+
|
|
286
|
+
render(
|
|
287
|
+
<ModalForm
|
|
288
|
+
schema={{
|
|
289
|
+
type: 'object-form',
|
|
290
|
+
formType: 'modal',
|
|
291
|
+
objectName: 'contacts',
|
|
292
|
+
mode: 'create',
|
|
293
|
+
title: 'New Contact',
|
|
294
|
+
open: true,
|
|
295
|
+
sections: [
|
|
296
|
+
{
|
|
297
|
+
label: 'Basic Info',
|
|
298
|
+
fields: ['firstName', 'lastName'],
|
|
299
|
+
columns: 2,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
label: 'Contact Details',
|
|
303
|
+
fields: ['email', 'phone'],
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
}}
|
|
307
|
+
dataSource={mockDataSource as any}
|
|
308
|
+
/>
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
await waitFor(() => {
|
|
312
|
+
expect(screen.getByText('New Contact')).toBeInTheDocument();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
await waitFor(() => {
|
|
316
|
+
expect(screen.getByText('Basic Info')).toBeInTheDocument();
|
|
317
|
+
});
|
|
318
|
+
expect(screen.getByText('Contact Details')).toBeInTheDocument();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('does not render content when open is false', () => {
|
|
322
|
+
const mockDataSource = createMockDataSource();
|
|
323
|
+
|
|
324
|
+
render(
|
|
325
|
+
<ModalForm
|
|
326
|
+
schema={{
|
|
327
|
+
type: 'object-form',
|
|
328
|
+
formType: 'modal',
|
|
329
|
+
objectName: 'contacts',
|
|
330
|
+
mode: 'create',
|
|
331
|
+
title: 'Hidden Modal',
|
|
332
|
+
open: false,
|
|
333
|
+
fields: ['firstName'],
|
|
334
|
+
}}
|
|
335
|
+
dataSource={mockDataSource as any}
|
|
336
|
+
/>
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
expect(screen.queryByText('Hidden Modal')).not.toBeInTheDocument();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it('applies size class for large modal', async () => {
|
|
343
|
+
const mockDataSource = createMockDataSource();
|
|
344
|
+
|
|
345
|
+
render(
|
|
346
|
+
<ModalForm
|
|
347
|
+
schema={{
|
|
348
|
+
type: 'object-form',
|
|
349
|
+
formType: 'modal',
|
|
350
|
+
objectName: 'contacts',
|
|
351
|
+
mode: 'create',
|
|
352
|
+
title: 'Large Modal',
|
|
353
|
+
open: true,
|
|
354
|
+
modalSize: 'xl',
|
|
355
|
+
fields: ['firstName'],
|
|
356
|
+
}}
|
|
357
|
+
dataSource={mockDataSource as any}
|
|
358
|
+
/>
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
await waitFor(() => {
|
|
362
|
+
expect(screen.getByText('Large Modal')).toBeInTheDocument();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('shows error state on data fetch failure', async () => {
|
|
367
|
+
const mockDataSource = createMockDataSource();
|
|
368
|
+
mockDataSource.getObjectSchema.mockRejectedValue(new Error('Server error'));
|
|
369
|
+
|
|
370
|
+
render(
|
|
371
|
+
<ModalForm
|
|
372
|
+
schema={{
|
|
373
|
+
type: 'object-form',
|
|
374
|
+
formType: 'modal',
|
|
375
|
+
objectName: 'contacts',
|
|
376
|
+
mode: 'create',
|
|
377
|
+
open: true,
|
|
378
|
+
fields: ['firstName'],
|
|
379
|
+
}}
|
|
380
|
+
dataSource={mockDataSource as any}
|
|
381
|
+
/>
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
await waitFor(() => {
|
|
385
|
+
expect(screen.getByText('Error loading form')).toBeInTheDocument();
|
|
386
|
+
});
|
|
387
|
+
expect(screen.getByText('Server error')).toBeInTheDocument();
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
// ─── ObjectForm Routing Tests ───────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
describe('ObjectForm routing to new variants', () => {
|
|
394
|
+
it('routes formType=split to SplitForm', async () => {
|
|
395
|
+
const mockDataSource = createMockDataSource();
|
|
396
|
+
|
|
397
|
+
render(
|
|
398
|
+
<ObjectForm
|
|
399
|
+
schema={{
|
|
400
|
+
type: 'object-form',
|
|
401
|
+
objectName: 'contacts',
|
|
402
|
+
mode: 'create',
|
|
403
|
+
formType: 'split',
|
|
404
|
+
sections: [
|
|
405
|
+
{ label: 'Left Panel', fields: ['firstName', 'lastName'] },
|
|
406
|
+
{ label: 'Right Panel', fields: ['email', 'phone'] },
|
|
407
|
+
],
|
|
408
|
+
}}
|
|
409
|
+
dataSource={mockDataSource as any}
|
|
410
|
+
/>
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
// Should load via SplitForm
|
|
414
|
+
await waitFor(() => {
|
|
415
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('contacts');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
await waitFor(() => {
|
|
419
|
+
expect(screen.getByText('Left Panel')).toBeInTheDocument();
|
|
420
|
+
});
|
|
421
|
+
expect(screen.getByText('Right Panel')).toBeInTheDocument();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('routes formType=drawer to DrawerForm', async () => {
|
|
425
|
+
const mockDataSource = createMockDataSource();
|
|
426
|
+
|
|
427
|
+
render(
|
|
428
|
+
<ObjectForm
|
|
429
|
+
schema={{
|
|
430
|
+
type: 'object-form',
|
|
431
|
+
objectName: 'contacts',
|
|
432
|
+
mode: 'create',
|
|
433
|
+
formType: 'drawer',
|
|
434
|
+
title: 'Drawer Form Title',
|
|
435
|
+
open: true,
|
|
436
|
+
fields: ['firstName', 'lastName'],
|
|
437
|
+
}}
|
|
438
|
+
dataSource={mockDataSource as any}
|
|
439
|
+
/>
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
await waitFor(() => {
|
|
443
|
+
expect(screen.getByText('Drawer Form Title')).toBeInTheDocument();
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
it('routes formType=modal to ModalForm', async () => {
|
|
448
|
+
const mockDataSource = createMockDataSource();
|
|
449
|
+
|
|
450
|
+
render(
|
|
451
|
+
<ObjectForm
|
|
452
|
+
schema={{
|
|
453
|
+
type: 'object-form',
|
|
454
|
+
objectName: 'contacts',
|
|
455
|
+
mode: 'create',
|
|
456
|
+
formType: 'modal',
|
|
457
|
+
title: 'Modal Form Title',
|
|
458
|
+
open: true,
|
|
459
|
+
fields: ['firstName', 'lastName'],
|
|
460
|
+
}}
|
|
461
|
+
dataSource={mockDataSource as any}
|
|
462
|
+
/>
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
await waitFor(() => {
|
|
466
|
+
expect(screen.getByText('Modal Form Title')).toBeInTheDocument();
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
// ─── Export Tests ────────────────────────────────────────────────────────
|
|
472
|
+
|
|
473
|
+
describe('New variant exports', () => {
|
|
474
|
+
it('exports SplitForm', () => {
|
|
475
|
+
expect(SplitForm).toBeDefined();
|
|
476
|
+
expect(typeof SplitForm).toBe('function');
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
it('exports DrawerForm', () => {
|
|
480
|
+
expect(DrawerForm).toBeDefined();
|
|
481
|
+
expect(typeof DrawerForm).toBe('function');
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
it('exports ModalForm', () => {
|
|
485
|
+
expect(ModalForm).toBeDefined();
|
|
486
|
+
expect(typeof ModalForm).toBe('function');
|
|
487
|
+
});
|
|
488
|
+
});
|
package/src/index.tsx
CHANGED
|
@@ -12,6 +12,18 @@ import { ObjectForm } from './ObjectForm';
|
|
|
12
12
|
|
|
13
13
|
export { ObjectForm };
|
|
14
14
|
export type { ObjectFormProps } from './ObjectForm';
|
|
15
|
+
export { FormSection } from './FormSection';
|
|
16
|
+
export type { FormSectionProps } from './FormSection';
|
|
17
|
+
export { TabbedForm } from './TabbedForm';
|
|
18
|
+
export type { TabbedFormProps, TabbedFormSchema, FormSectionConfig } from './TabbedForm';
|
|
19
|
+
export { WizardForm } from './WizardForm';
|
|
20
|
+
export type { WizardFormProps, WizardFormSchema } from './WizardForm';
|
|
21
|
+
export { SplitForm } from './SplitForm';
|
|
22
|
+
export type { SplitFormProps, SplitFormSchema } from './SplitForm';
|
|
23
|
+
export { DrawerForm } from './DrawerForm';
|
|
24
|
+
export type { DrawerFormProps, DrawerFormSchema } from './DrawerForm';
|
|
25
|
+
export { ModalForm } from './ModalForm';
|
|
26
|
+
export type { ModalFormProps, ModalFormSchema } from './ModalForm';
|
|
15
27
|
|
|
16
28
|
// Register object-form component
|
|
17
29
|
const ObjectFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
@@ -19,7 +31,52 @@ const ObjectFormRenderer: React.FC<{ schema: any }> = ({ schema }) => {
|
|
|
19
31
|
};
|
|
20
32
|
|
|
21
33
|
ComponentRegistry.register('object-form', ObjectFormRenderer, {
|
|
22
|
-
namespace: 'plugin-form'
|
|
34
|
+
namespace: 'plugin-form',
|
|
35
|
+
label: 'Object Form',
|
|
36
|
+
category: 'plugin',
|
|
37
|
+
inputs: [
|
|
38
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
39
|
+
{ name: 'fields', type: 'array', label: 'Fields' },
|
|
40
|
+
{ name: 'mode', type: 'enum', label: 'Mode', enum: ['create', 'edit', 'view'] },
|
|
41
|
+
{ name: 'formType', type: 'enum', label: 'Form Type', enum: ['simple', 'tabbed', 'wizard', 'split', 'drawer', 'modal'] },
|
|
42
|
+
{ name: 'sections', type: 'array', label: 'Sections' },
|
|
43
|
+
{ name: 'title', type: 'string', label: 'Title' },
|
|
44
|
+
{ name: 'description', type: 'string', label: 'Description' },
|
|
45
|
+
{ name: 'layout', type: 'enum', label: 'Layout', enum: ['vertical', 'horizontal', 'inline', 'grid'] },
|
|
46
|
+
{ name: 'columns', type: 'number', label: 'Columns' },
|
|
47
|
+
// Tabbed
|
|
48
|
+
{ name: 'defaultTab', type: 'string', label: 'Default Tab' },
|
|
49
|
+
{ name: 'tabPosition', type: 'enum', label: 'Tab Position', enum: ['top', 'bottom', 'left', 'right'] },
|
|
50
|
+
// Wizard
|
|
51
|
+
{ name: 'allowSkip', type: 'boolean', label: 'Allow Skip Steps' },
|
|
52
|
+
{ name: 'showStepIndicator', type: 'boolean', label: 'Show Step Indicator' },
|
|
53
|
+
// Split
|
|
54
|
+
{ name: 'splitDirection', type: 'enum', label: 'Split Direction', enum: ['horizontal', 'vertical'] },
|
|
55
|
+
{ name: 'splitSize', type: 'number', label: 'Split Panel Size (%)' },
|
|
56
|
+
{ name: 'splitResizable', type: 'boolean', label: 'Split Resizable' },
|
|
57
|
+
// Drawer
|
|
58
|
+
{ name: 'drawerSide', type: 'enum', label: 'Drawer Side', enum: ['top', 'bottom', 'left', 'right'] },
|
|
59
|
+
{ name: 'drawerWidth', type: 'string', label: 'Drawer Width' },
|
|
60
|
+
// Modal
|
|
61
|
+
{ name: 'modalSize', type: 'enum', label: 'Modal Size', enum: ['sm', 'default', 'lg', 'xl', 'full'] },
|
|
62
|
+
]
|
|
23
63
|
});
|
|
24
|
-
|
|
25
|
-
//
|
|
64
|
+
|
|
65
|
+
// Register as view:form for the standard view protocol
|
|
66
|
+
// This allows using { type: 'view:form', objectName: '...' } in schemas
|
|
67
|
+
// skipFallback prevents overwriting the basic 'form' component from @object-ui/components
|
|
68
|
+
ComponentRegistry.register('form', ObjectFormRenderer, {
|
|
69
|
+
namespace: 'view',
|
|
70
|
+
skipFallback: true,
|
|
71
|
+
label: 'Data Form View',
|
|
72
|
+
category: 'view',
|
|
73
|
+
inputs: [
|
|
74
|
+
{ name: 'objectName', type: 'string', label: 'Object Name', required: true },
|
|
75
|
+
{ name: 'fields', type: 'array', label: 'Fields' },
|
|
76
|
+
{ name: 'mode', type: 'enum', label: 'Mode', enum: ['create', 'edit', 'view'] },
|
|
77
|
+
]
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Note: 'form' type (without namespace) is handled by @object-ui/components Form component
|
|
81
|
+
// This plugin registers as 'view:form' (with view namespace) for the view protocol
|
|
82
|
+
// ObjectForm internally uses { type: 'form' } to render the basic Form component
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/// <reference types="vitest" />
|
|
2
|
+
import { defineConfig } from 'vite';
|
|
3
|
+
import react from '@vitejs/plugin-react';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react()],
|
|
7
|
+
test: {
|
|
8
|
+
environment: 'happy-dom',
|
|
9
|
+
globals: true,
|
|
10
|
+
setupFiles: ['./vitest.setup.ts'],
|
|
11
|
+
},
|
|
12
|
+
});
|
package/vitest.setup.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import '@testing-library/jest-dom';
|