@object-ui/plugin-form 3.1.5 → 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.
Files changed (42) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +21 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +116 -73
  5. package/dist/index.umd.cjs +2 -2
  6. package/dist/{plugin-form → packages/plugin-form}/src/DrawerForm.d.ts +2 -0
  7. package/dist/{plugin-form → packages/plugin-form}/src/autoLayout.d.ts +11 -4
  8. package/package.json +43 -11
  9. package/.turbo/turbo-build.log +0 -32
  10. package/src/DrawerForm.tsx +0 -410
  11. package/src/EmbeddableForm.tsx +0 -240
  12. package/src/FormAnalytics.tsx +0 -209
  13. package/src/FormSection.tsx +0 -152
  14. package/src/FormVariants.test.tsx +0 -219
  15. package/src/ModalForm.tsx +0 -485
  16. package/src/ObjectForm.msw.test.tsx +0 -156
  17. package/src/ObjectForm.stories.tsx +0 -85
  18. package/src/ObjectForm.test.tsx +0 -61
  19. package/src/ObjectForm.tsx +0 -609
  20. package/src/SplitForm.tsx +0 -300
  21. package/src/TabbedForm.tsx +0 -395
  22. package/src/WizardForm.tsx +0 -502
  23. package/src/__tests__/EmbeddableFormPrefill.test.tsx +0 -186
  24. package/src/__tests__/MobileUX.test.tsx +0 -433
  25. package/src/__tests__/NewVariants.test.tsx +0 -684
  26. package/src/__tests__/autoLayout.test.ts +0 -339
  27. package/src/__tests__/form-validation-submit.test.tsx +0 -286
  28. package/src/autoLayout.ts +0 -166
  29. package/src/index.tsx +0 -134
  30. package/tsconfig.json +0 -9
  31. package/vite.config.ts +0 -57
  32. package/vitest.config.ts +0 -12
  33. package/vitest.setup.ts +0 -1
  34. /package/dist/{plugin-form → packages/plugin-form}/src/EmbeddableForm.d.ts +0 -0
  35. /package/dist/{plugin-form → packages/plugin-form}/src/FormAnalytics.d.ts +0 -0
  36. /package/dist/{plugin-form → packages/plugin-form}/src/FormSection.d.ts +0 -0
  37. /package/dist/{plugin-form → packages/plugin-form}/src/ModalForm.d.ts +0 -0
  38. /package/dist/{plugin-form → packages/plugin-form}/src/ObjectForm.d.ts +0 -0
  39. /package/dist/{plugin-form → packages/plugin-form}/src/SplitForm.d.ts +0 -0
  40. /package/dist/{plugin-form → packages/plugin-form}/src/TabbedForm.d.ts +0 -0
  41. /package/dist/{plugin-form → packages/plugin-form}/src/WizardForm.d.ts +0 -0
  42. /package/dist/{plugin-form → packages/plugin-form}/src/index.d.ts +0 -0
@@ -1,433 +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
- * Mobile UX Tests for ModalForm
11
- *
12
- * Validates mobile-specific optimizations:
13
- * - Skeleton loading state (replaces spinner)
14
- * - Flex layout structure with sticky header/footer
15
- * - Full-screen modal on mobile (h-[100dvh])
16
- * - Close button touch target (min 44×44px)
17
- * - Sticky footer with action buttons outside scroll area
18
- */
19
-
20
- import { describe, it, expect, vi } from 'vitest';
21
- import { render, screen, waitFor } from '@testing-library/react';
22
- import React from 'react';
23
- import { ModalForm } from '../ModalForm';
24
-
25
- const mockObjectSchema = {
26
- name: 'events',
27
- fields: {
28
- subject: { label: 'Subject', type: 'text', required: true },
29
- start: { label: 'Start', type: 'datetime', required: true },
30
- end: { label: 'End', type: 'datetime', required: true },
31
- location: { label: 'Location', type: 'text', required: false },
32
- description: { label: 'Description', type: 'textarea', required: false },
33
- participants: { label: 'Participants', type: 'lookup', required: false },
34
- type: { label: 'Type', type: 'select', required: false, options: [{ value: 'meeting', label: 'Meeting' }] },
35
- },
36
- };
37
-
38
- const createMockDataSource = () => ({
39
- getObjectSchema: vi.fn().mockResolvedValue(mockObjectSchema),
40
- findOne: vi.fn().mockResolvedValue({ id: '1', subject: 'Test Event' }),
41
- find: vi.fn().mockResolvedValue([]),
42
- create: vi.fn().mockResolvedValue({ id: '1' }),
43
- update: vi.fn().mockResolvedValue({ id: '1' }),
44
- delete: vi.fn().mockResolvedValue(true),
45
- });
46
-
47
- describe('ModalForm Mobile UX', () => {
48
- it('renders skeleton loading instead of spinner', () => {
49
- const mockDataSource = createMockDataSource();
50
- // Make getObjectSchema hang (never resolve) to keep loading state
51
- mockDataSource.getObjectSchema.mockReturnValue(new Promise(() => {}));
52
-
53
- render(
54
- <ModalForm
55
- schema={{
56
- type: 'object-form',
57
- formType: 'modal',
58
- objectName: 'events',
59
- mode: 'create',
60
- title: 'Create Event',
61
- open: true,
62
- }}
63
- dataSource={mockDataSource as any}
64
- />
65
- );
66
-
67
- // Should show skeleton loading, not the old spinner text
68
- expect(screen.queryByText('Loading form...')).not.toBeInTheDocument();
69
- expect(screen.getByTestId('modal-form-skeleton')).toBeInTheDocument();
70
- });
71
-
72
- it('renders with full-screen mobile classes and flex layout on MobileDialogContent', async () => {
73
- const mockDataSource = createMockDataSource();
74
-
75
- render(
76
- <ModalForm
77
- schema={{
78
- type: 'object-form',
79
- formType: 'modal',
80
- objectName: 'events',
81
- mode: 'create',
82
- title: 'Create Event',
83
- description: 'Add a new Event to your database.',
84
- open: true,
85
- }}
86
- dataSource={mockDataSource as any}
87
- />
88
- );
89
-
90
- await waitFor(() => {
91
- expect(screen.getByText('Create Event')).toBeInTheDocument();
92
- });
93
- expect(screen.getByText('Add a new Event to your database.')).toBeInTheDocument();
94
-
95
- // MobileDialogContent should be rendered via portal
96
- const dialogContent = document.querySelector('[role="dialog"]');
97
- expect(dialogContent).not.toBeNull();
98
- const cls = dialogContent!.className;
99
- // Mobile full-screen
100
- expect(cls).toContain('h-[100dvh]');
101
- // Flex column layout for sticky header/footer
102
- expect(cls).toContain('flex');
103
- expect(cls).toContain('flex-col');
104
- // Overflow hidden (scroll is on the body area)
105
- expect(cls).toContain('overflow-hidden');
106
- });
107
-
108
- it('close button has accessible touch target (≥44×44px on mobile)', async () => {
109
- const mockDataSource = createMockDataSource();
110
-
111
- render(
112
- <ModalForm
113
- schema={{
114
- type: 'object-form',
115
- formType: 'modal',
116
- objectName: 'events',
117
- mode: 'create',
118
- title: 'Create Event',
119
- open: true,
120
- }}
121
- dataSource={mockDataSource as any}
122
- />
123
- );
124
-
125
- await waitFor(() => {
126
- expect(screen.getByText('Create Event')).toBeInTheDocument();
127
- });
128
-
129
- // Close button should have WCAG-compliant touch target classes
130
- const closeButton = screen.getByRole('button', { name: /close/i });
131
- expect(closeButton).toBeInTheDocument();
132
- const cls = closeButton.className;
133
- expect(cls).toContain('min-h-[44px]');
134
- expect(cls).toContain('min-w-[44px]');
135
- });
136
-
137
- it('renders sticky footer with action buttons outside scroll area', async () => {
138
- const mockDataSource = createMockDataSource();
139
-
140
- render(
141
- <ModalForm
142
- schema={{
143
- type: 'object-form',
144
- formType: 'modal',
145
- objectName: 'events',
146
- mode: 'create',
147
- title: 'Create Event',
148
- open: true,
149
- showSubmit: true,
150
- showCancel: true,
151
- submitText: 'Save Record',
152
- cancelText: 'Cancel',
153
- }}
154
- dataSource={mockDataSource as any}
155
- />
156
- );
157
-
158
- // Footer should exist as a sibling to the scroll area, not inside it
159
- const footer = await screen.findByTestId('modal-form-footer');
160
- expect(footer.className).toContain('border-t');
161
- expect(footer.className).toContain('shrink-0');
162
-
163
- // Action buttons should be in the footer
164
- const saveButton = screen.getByRole('button', { name: /save record/i });
165
- const cancelButton = screen.getByRole('button', { name: /cancel/i });
166
- expect(footer.contains(saveButton)).toBe(true);
167
- expect(footer.contains(cancelButton)).toBe(true);
168
- });
169
-
170
- it('does not show footer during loading or error states', () => {
171
- const mockDataSource = createMockDataSource();
172
- mockDataSource.getObjectSchema.mockReturnValue(new Promise(() => {}));
173
-
174
- render(
175
- <ModalForm
176
- schema={{
177
- type: 'object-form',
178
- formType: 'modal',
179
- objectName: 'events',
180
- mode: 'create',
181
- title: 'Create Event',
182
- open: true,
183
- showSubmit: true,
184
- showCancel: true,
185
- }}
186
- dataSource={mockDataSource as any}
187
- />
188
- );
189
-
190
- // While loading, footer should not be rendered
191
- expect(screen.queryByTestId('modal-form-footer')).not.toBeInTheDocument();
192
- });
193
- });
194
-
195
- describe('ModalForm Container Query Layout', () => {
196
- /** CSS selector for the @container query context element */
197
- const CONTAINER_SELECTOR = '.\\@container';
198
-
199
- it('applies @container class on scrollable content area', async () => {
200
- const mockDataSource = createMockDataSource();
201
-
202
- render(
203
- <ModalForm
204
- schema={{
205
- type: 'object-form',
206
- formType: 'modal',
207
- objectName: 'events',
208
- mode: 'create',
209
- title: 'Create Event',
210
- open: true,
211
- }}
212
- dataSource={mockDataSource as any}
213
- />
214
- );
215
-
216
- await waitFor(() => {
217
- expect(screen.getByText('Create Event')).toBeInTheDocument();
218
- });
219
-
220
- // The scrollable content wrapper should be a @container query context
221
- const dialogContent = document.querySelector('[role="dialog"]');
222
- expect(dialogContent).not.toBeNull();
223
- const scrollArea = dialogContent!.querySelector(CONTAINER_SELECTOR);
224
- expect(scrollArea).not.toBeNull();
225
- expect(scrollArea!.className).toContain('overflow-y-auto');
226
- });
227
-
228
- it('uses container-query grid classes for multi-column flat fields', async () => {
229
- // Mock schema with enough fields to trigger auto-layout 2-column
230
- const manyFieldsSchema = {
231
- name: 'contacts',
232
- fields: {
233
- name: { label: 'Name', type: 'text', required: true },
234
- email: { label: 'Email', type: 'email', required: false },
235
- phone: { label: 'Phone', type: 'phone', required: false },
236
- company: { label: 'Company', type: 'text', required: false },
237
- department: { label: 'Department', type: 'text', required: false },
238
- title: { label: 'Title', type: 'text', required: false },
239
- },
240
- };
241
- const mockDataSource = createMockDataSource();
242
- mockDataSource.getObjectSchema.mockResolvedValue(manyFieldsSchema);
243
-
244
- render(
245
- <ModalForm
246
- schema={{
247
- type: 'object-form',
248
- formType: 'modal',
249
- objectName: 'contacts',
250
- mode: 'create',
251
- title: 'Create Contact',
252
- open: true,
253
- }}
254
- dataSource={mockDataSource as any}
255
- />
256
- );
257
-
258
- await waitFor(() => {
259
- expect(screen.getByText('Create Contact')).toBeInTheDocument();
260
- });
261
-
262
- // Wait for fields to render
263
- await waitFor(() => {
264
- expect(screen.getByText('Name')).toBeInTheDocument();
265
- });
266
-
267
- // The form field container should use container-query classes (@md:grid-cols-2)
268
- // instead of viewport-based classes (md:grid-cols-2)
269
- const dialogContent = document.querySelector('[role="dialog"]');
270
- const containerEl = dialogContent!.querySelector(CONTAINER_SELECTOR);
271
- expect(containerEl).not.toBeNull();
272
-
273
- // Look for the grid container with @md:grid-cols-2
274
- const gridEl = containerEl!.querySelector('[class*="@md:grid-cols-2"]');
275
- expect(gridEl).not.toBeNull();
276
- expect(gridEl!.className).toContain('@md:grid-cols-2');
277
- // Should NOT use viewport-based md:grid-cols-2 (without @ prefix)
278
- expect(gridEl!.className).not.toContain(' md:grid-cols-2');
279
- });
280
-
281
- it('single-column forms do not get container grid override', async () => {
282
- // Only 3 fields → auto-layout stays at 1 column
283
- const fewFieldsSchema = {
284
- name: 'notes',
285
- fields: {
286
- title: { label: 'Title', type: 'text', required: true },
287
- body: { label: 'Body', type: 'textarea', required: false },
288
- status: { label: 'Status', type: 'select', required: false, options: [{ value: 'draft', label: 'Draft' }] },
289
- },
290
- };
291
- const mockDataSource = createMockDataSource();
292
- mockDataSource.getObjectSchema.mockResolvedValue(fewFieldsSchema);
293
-
294
- render(
295
- <ModalForm
296
- schema={{
297
- type: 'object-form',
298
- formType: 'modal',
299
- objectName: 'notes',
300
- mode: 'create',
301
- title: 'Create Note',
302
- open: true,
303
- }}
304
- dataSource={mockDataSource as any}
305
- />
306
- );
307
-
308
- await waitFor(() => {
309
- expect(screen.getByText('Create Note')).toBeInTheDocument();
310
- });
311
-
312
- await waitFor(() => {
313
- expect(screen.getByText('Title')).toBeInTheDocument();
314
- });
315
-
316
- // Single column form should not have @md:grid-cols-2
317
- const dialogContent = document.querySelector('[role="dialog"]');
318
- const containerEl = dialogContent!.querySelector(CONTAINER_SELECTOR);
319
- expect(containerEl).not.toBeNull();
320
- const gridEl = containerEl!.querySelector('[class*="@md:grid-cols"]');
321
- expect(gridEl).toBeNull();
322
- });
323
- });
324
-
325
- describe('ModalForm Sections — Modal Size Auto-Upgrade', () => {
326
- it('auto-upgrades modal to xl when sections use 2-column layout', async () => {
327
- const mockDataSource = createMockDataSource();
328
-
329
- render(
330
- <ModalForm
331
- schema={{
332
- type: 'object-form',
333
- formType: 'modal',
334
- objectName: 'events',
335
- mode: 'create',
336
- title: 'Create Task',
337
- open: true,
338
- sections: [
339
- {
340
- label: 'Task Information',
341
- columns: 2,
342
- fields: ['subject', 'start', 'end', 'location'],
343
- },
344
- {
345
- label: 'Details',
346
- columns: 1,
347
- fields: ['description'],
348
- },
349
- ],
350
- }}
351
- dataSource={mockDataSource as any}
352
- />
353
- );
354
-
355
- await waitFor(() => {
356
- expect(screen.getByText('Create Task')).toBeInTheDocument();
357
- });
358
-
359
- // Dialog should auto-upgrade to xl (max-w-5xl) because sections have columns: 2
360
- const dialogContent = document.querySelector('[role="dialog"]');
361
- expect(dialogContent).not.toBeNull();
362
- expect(dialogContent!.className).toContain('max-w-5xl');
363
- });
364
-
365
- it('keeps default size when all sections use 1-column layout', async () => {
366
- const mockDataSource = createMockDataSource();
367
-
368
- render(
369
- <ModalForm
370
- schema={{
371
- type: 'object-form',
372
- formType: 'modal',
373
- objectName: 'events',
374
- mode: 'create',
375
- title: 'Create Task',
376
- open: true,
377
- sections: [
378
- {
379
- label: 'Basic Info',
380
- columns: 1,
381
- fields: ['subject', 'start'],
382
- },
383
- ],
384
- }}
385
- dataSource={mockDataSource as any}
386
- />
387
- );
388
-
389
- await waitFor(() => {
390
- expect(screen.getByText('Create Task')).toBeInTheDocument();
391
- });
392
-
393
- // Dialog should remain at default size (max-w-lg)
394
- const dialogContent = document.querySelector('[role="dialog"]');
395
- expect(dialogContent).not.toBeNull();
396
- expect(dialogContent!.className).toContain('max-w-lg');
397
- });
398
-
399
- it('respects explicit modalSize over section auto-upgrade', async () => {
400
- const mockDataSource = createMockDataSource();
401
-
402
- render(
403
- <ModalForm
404
- schema={{
405
- type: 'object-form',
406
- formType: 'modal',
407
- objectName: 'events',
408
- mode: 'create',
409
- title: 'Create Task',
410
- open: true,
411
- modalSize: 'sm',
412
- sections: [
413
- {
414
- label: 'Task Information',
415
- columns: 2,
416
- fields: ['subject', 'start', 'end', 'location'],
417
- },
418
- ],
419
- }}
420
- dataSource={mockDataSource as any}
421
- />
422
- );
423
-
424
- await waitFor(() => {
425
- expect(screen.getByText('Create Task')).toBeInTheDocument();
426
- });
427
-
428
- // Explicit modalSize: 'sm' should override section auto-upgrade
429
- const dialogContent = document.querySelector('[role="dialog"]');
430
- expect(dialogContent).not.toBeNull();
431
- expect(dialogContent!.className).toContain('max-w-sm');
432
- });
433
- });