@object-ui/plugin-form 3.3.0 → 3.3.2
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 +21 -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/FormAnalytics.tsx
DELETED
|
@@ -1,209 +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
|
-
* FormAnalytics Component
|
|
11
|
-
*
|
|
12
|
-
* Displays basic analytics for form submissions:
|
|
13
|
-
* - Total submissions count
|
|
14
|
-
* - Fill rate (completed vs started)
|
|
15
|
-
* - Average completion time
|
|
16
|
-
* - Field drop-off rates
|
|
17
|
-
*
|
|
18
|
-
* This is an L1 Foundation component for Phase 14.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import React, { useMemo } from 'react';
|
|
22
|
-
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '@object-ui/components';
|
|
23
|
-
|
|
24
|
-
export interface FormSubmissionMetric {
|
|
25
|
-
/** Total number of form submissions */
|
|
26
|
-
totalSubmissions: number;
|
|
27
|
-
/** Number of forms started but not completed */
|
|
28
|
-
abandonedSubmissions?: number;
|
|
29
|
-
/** Average time to complete form in seconds */
|
|
30
|
-
avgCompletionTime?: number;
|
|
31
|
-
/** Submissions per day over the last N days */
|
|
32
|
-
dailySubmissions?: Array<{
|
|
33
|
-
date: string;
|
|
34
|
-
count: number;
|
|
35
|
-
}>;
|
|
36
|
-
/** Field-level drop-off data */
|
|
37
|
-
fieldDropOff?: Array<{
|
|
38
|
-
fieldName: string;
|
|
39
|
-
label: string;
|
|
40
|
-
completionRate: number;
|
|
41
|
-
}>;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export interface FormAnalyticsProps {
|
|
45
|
-
/** Form identifier */
|
|
46
|
-
formId: string;
|
|
47
|
-
/** Form title */
|
|
48
|
-
formTitle?: string;
|
|
49
|
-
/** Submission metrics data */
|
|
50
|
-
metrics: FormSubmissionMetric;
|
|
51
|
-
/** Additional CSS class */
|
|
52
|
-
className?: string;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function formatDuration(seconds: number): string {
|
|
56
|
-
if (seconds < 60) return `${Math.round(seconds)}s`;
|
|
57
|
-
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${Math.round(seconds % 60)}s`;
|
|
58
|
-
return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* FormAnalytics — Basic form analytics dashboard showing submission metrics.
|
|
63
|
-
*
|
|
64
|
-
* Displays fill rate, completion time, and field-level drop-off data
|
|
65
|
-
* for embeddable forms and data collection workflows.
|
|
66
|
-
*/
|
|
67
|
-
export const FormAnalytics: React.FC<FormAnalyticsProps> = ({
|
|
68
|
-
formId,
|
|
69
|
-
formTitle,
|
|
70
|
-
metrics,
|
|
71
|
-
className,
|
|
72
|
-
}) => {
|
|
73
|
-
const fillRate = useMemo(() => {
|
|
74
|
-
if (!metrics.abandonedSubmissions) return 100;
|
|
75
|
-
const total = metrics.totalSubmissions + metrics.abandonedSubmissions;
|
|
76
|
-
if (total === 0) return 0;
|
|
77
|
-
return Math.round((metrics.totalSubmissions / total) * 100);
|
|
78
|
-
}, [metrics.totalSubmissions, metrics.abandonedSubmissions]);
|
|
79
|
-
|
|
80
|
-
// Find max count for bar chart scaling
|
|
81
|
-
const maxDaily = useMemo(() => {
|
|
82
|
-
if (!metrics.dailySubmissions?.length) return 1;
|
|
83
|
-
return Math.max(...metrics.dailySubmissions.map(d => d.count), 1);
|
|
84
|
-
}, [metrics.dailySubmissions]);
|
|
85
|
-
|
|
86
|
-
return (
|
|
87
|
-
<div className={`space-y-4 ${className || ''}`}>
|
|
88
|
-
{/* Header */}
|
|
89
|
-
<div>
|
|
90
|
-
<h2 className="text-lg font-semibold text-foreground">
|
|
91
|
-
{formTitle || `Form Analytics`}
|
|
92
|
-
</h2>
|
|
93
|
-
<p className="text-sm text-muted-foreground">
|
|
94
|
-
Form ID: {formId}
|
|
95
|
-
</p>
|
|
96
|
-
</div>
|
|
97
|
-
|
|
98
|
-
{/* Metric cards */}
|
|
99
|
-
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
100
|
-
{/* Total Submissions */}
|
|
101
|
-
<Card>
|
|
102
|
-
<CardHeader className="pb-2">
|
|
103
|
-
<CardDescription>Total Submissions</CardDescription>
|
|
104
|
-
<CardTitle className="text-2xl">
|
|
105
|
-
{metrics.totalSubmissions.toLocaleString()}
|
|
106
|
-
</CardTitle>
|
|
107
|
-
</CardHeader>
|
|
108
|
-
</Card>
|
|
109
|
-
|
|
110
|
-
{/* Fill Rate */}
|
|
111
|
-
<Card>
|
|
112
|
-
<CardHeader className="pb-2">
|
|
113
|
-
<CardDescription>Fill Rate</CardDescription>
|
|
114
|
-
<CardTitle className="text-2xl">
|
|
115
|
-
{fillRate}%
|
|
116
|
-
</CardTitle>
|
|
117
|
-
</CardHeader>
|
|
118
|
-
<CardContent>
|
|
119
|
-
<div className="w-full bg-muted rounded-full h-2">
|
|
120
|
-
<div
|
|
121
|
-
className="bg-primary rounded-full h-2 transition-all"
|
|
122
|
-
style={{ width: `${fillRate}%` }}
|
|
123
|
-
/>
|
|
124
|
-
</div>
|
|
125
|
-
</CardContent>
|
|
126
|
-
</Card>
|
|
127
|
-
|
|
128
|
-
{/* Avg Completion Time */}
|
|
129
|
-
<Card>
|
|
130
|
-
<CardHeader className="pb-2">
|
|
131
|
-
<CardDescription>Avg. Completion Time</CardDescription>
|
|
132
|
-
<CardTitle className="text-2xl">
|
|
133
|
-
{metrics.avgCompletionTime
|
|
134
|
-
? formatDuration(metrics.avgCompletionTime)
|
|
135
|
-
: '—'}
|
|
136
|
-
</CardTitle>
|
|
137
|
-
</CardHeader>
|
|
138
|
-
</Card>
|
|
139
|
-
</div>
|
|
140
|
-
|
|
141
|
-
{/* Daily Submissions Chart */}
|
|
142
|
-
{metrics.dailySubmissions && metrics.dailySubmissions.length > 0 && (
|
|
143
|
-
<Card>
|
|
144
|
-
<CardHeader>
|
|
145
|
-
<CardTitle className="text-sm font-medium">
|
|
146
|
-
Daily Submissions
|
|
147
|
-
</CardTitle>
|
|
148
|
-
</CardHeader>
|
|
149
|
-
<CardContent>
|
|
150
|
-
<div className="flex items-end gap-1 h-32">
|
|
151
|
-
{metrics.dailySubmissions.map((day, idx) => (
|
|
152
|
-
<div key={idx} className="flex-1 flex flex-col items-center gap-1">
|
|
153
|
-
<div
|
|
154
|
-
className="w-full bg-primary/80 rounded-t transition-all hover:bg-primary"
|
|
155
|
-
style={{ height: `${(day.count / maxDaily) * 100}%`, minHeight: day.count > 0 ? '4px' : '0' }}
|
|
156
|
-
title={`${day.date}: ${day.count} submissions`}
|
|
157
|
-
/>
|
|
158
|
-
<span className="text-[10px] text-muted-foreground truncate w-full text-center">
|
|
159
|
-
{day.date.slice(-5)}
|
|
160
|
-
</span>
|
|
161
|
-
</div>
|
|
162
|
-
))}
|
|
163
|
-
</div>
|
|
164
|
-
</CardContent>
|
|
165
|
-
</Card>
|
|
166
|
-
)}
|
|
167
|
-
|
|
168
|
-
{/* Field Drop-off */}
|
|
169
|
-
{metrics.fieldDropOff && metrics.fieldDropOff.length > 0 && (
|
|
170
|
-
<Card>
|
|
171
|
-
<CardHeader>
|
|
172
|
-
<CardTitle className="text-sm font-medium">
|
|
173
|
-
Field Completion Rates
|
|
174
|
-
</CardTitle>
|
|
175
|
-
<CardDescription>
|
|
176
|
-
Percentage of users who completed each field
|
|
177
|
-
</CardDescription>
|
|
178
|
-
</CardHeader>
|
|
179
|
-
<CardContent>
|
|
180
|
-
<div className="space-y-3">
|
|
181
|
-
{metrics.fieldDropOff.map((field, idx) => (
|
|
182
|
-
<div key={idx} className="space-y-1">
|
|
183
|
-
<div className="flex justify-between text-sm">
|
|
184
|
-
<span className="text-foreground">{field.label}</span>
|
|
185
|
-
<span className="text-muted-foreground">{field.completionRate}%</span>
|
|
186
|
-
</div>
|
|
187
|
-
<div className="w-full bg-muted rounded-full h-1.5">
|
|
188
|
-
<div
|
|
189
|
-
className={`rounded-full h-1.5 transition-all ${
|
|
190
|
-
field.completionRate >= 80
|
|
191
|
-
? 'bg-green-500'
|
|
192
|
-
: field.completionRate >= 50
|
|
193
|
-
? 'bg-yellow-500'
|
|
194
|
-
: 'bg-destructive'
|
|
195
|
-
}`}
|
|
196
|
-
style={{ width: `${field.completionRate}%` }}
|
|
197
|
-
/>
|
|
198
|
-
</div>
|
|
199
|
-
</div>
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
202
|
-
</CardContent>
|
|
203
|
-
</Card>
|
|
204
|
-
)}
|
|
205
|
-
</div>
|
|
206
|
-
);
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
export default FormAnalytics;
|
package/src/FormSection.tsx
DELETED
|
@@ -1,152 +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
|
-
* FormSection Component
|
|
11
|
-
*
|
|
12
|
-
* A form section component that groups fields together with optional
|
|
13
|
-
* collapsibility and multi-column layout. Aligns with @objectstack/spec FormSection.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import React, { useState } from 'react';
|
|
17
|
-
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
18
|
-
import { cn } from '@object-ui/components';
|
|
19
|
-
|
|
20
|
-
export interface FormSectionProps {
|
|
21
|
-
/**
|
|
22
|
-
* Section title/label
|
|
23
|
-
*/
|
|
24
|
-
label?: string;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Section description
|
|
28
|
-
*/
|
|
29
|
-
description?: string;
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Whether the section can be collapsed
|
|
33
|
-
* @default false
|
|
34
|
-
*/
|
|
35
|
-
collapsible?: boolean;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Whether the section is initially collapsed
|
|
39
|
-
* @default false
|
|
40
|
-
*/
|
|
41
|
-
collapsed?: boolean;
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Number of columns for field layout
|
|
45
|
-
* @default 1
|
|
46
|
-
*/
|
|
47
|
-
columns?: 1 | 2 | 3 | 4;
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Section children (form fields)
|
|
51
|
-
*/
|
|
52
|
-
children: React.ReactNode;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Additional CSS classes
|
|
56
|
-
*/
|
|
57
|
-
className?: string;
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Override the default responsive grid classes.
|
|
61
|
-
* When provided, replaces the viewport-based grid-cols classes
|
|
62
|
-
* (e.g. with container-query-based classes like `@md:grid-cols-2`).
|
|
63
|
-
*/
|
|
64
|
-
gridClassName?: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* FormSection Component
|
|
69
|
-
*
|
|
70
|
-
* Groups form fields with optional header, collapsibility, and multi-column layout.
|
|
71
|
-
*
|
|
72
|
-
* @example
|
|
73
|
-
* ```tsx
|
|
74
|
-
* <FormSection label="Contact Details" columns={2} collapsible>
|
|
75
|
-
* <FormField name="firstName" />
|
|
76
|
-
* <FormField name="lastName" />
|
|
77
|
-
* </FormSection>
|
|
78
|
-
* ```
|
|
79
|
-
*/
|
|
80
|
-
export const FormSection: React.FC<FormSectionProps> = ({
|
|
81
|
-
label,
|
|
82
|
-
description,
|
|
83
|
-
collapsible = false,
|
|
84
|
-
collapsed: initialCollapsed = false,
|
|
85
|
-
columns = 1,
|
|
86
|
-
children,
|
|
87
|
-
className,
|
|
88
|
-
gridClassName,
|
|
89
|
-
}) => {
|
|
90
|
-
const [isCollapsed, setIsCollapsed] = useState(initialCollapsed);
|
|
91
|
-
|
|
92
|
-
const gridCols: Record<number, string> = {
|
|
93
|
-
1: 'grid-cols-1',
|
|
94
|
-
2: 'grid-cols-1 md:grid-cols-2',
|
|
95
|
-
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
96
|
-
4: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-4',
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const handleToggle = () => {
|
|
100
|
-
if (collapsible) {
|
|
101
|
-
setIsCollapsed(!isCollapsed);
|
|
102
|
-
}
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
return (
|
|
106
|
-
<div className={cn('form-section', className)}>
|
|
107
|
-
{/* Section Header */}
|
|
108
|
-
{(label || description) && (
|
|
109
|
-
<div
|
|
110
|
-
className={cn(
|
|
111
|
-
'flex items-start gap-2 mb-4',
|
|
112
|
-
collapsible && 'cursor-pointer select-none'
|
|
113
|
-
)}
|
|
114
|
-
onClick={handleToggle}
|
|
115
|
-
role={collapsible ? 'button' : undefined}
|
|
116
|
-
aria-expanded={collapsible ? !isCollapsed : undefined}
|
|
117
|
-
>
|
|
118
|
-
{collapsible && (
|
|
119
|
-
<span className="mt-0.5 text-muted-foreground">
|
|
120
|
-
{isCollapsed ? (
|
|
121
|
-
<ChevronRight className="h-4 w-4" />
|
|
122
|
-
) : (
|
|
123
|
-
<ChevronDown className="h-4 w-4" />
|
|
124
|
-
)}
|
|
125
|
-
</span>
|
|
126
|
-
)}
|
|
127
|
-
<div className="flex-1">
|
|
128
|
-
{label && (
|
|
129
|
-
<h3 className="text-base font-semibold text-foreground">
|
|
130
|
-
{label}
|
|
131
|
-
</h3>
|
|
132
|
-
)}
|
|
133
|
-
{description && (
|
|
134
|
-
<p className="text-sm text-muted-foreground mt-0.5">
|
|
135
|
-
{description}
|
|
136
|
-
</p>
|
|
137
|
-
)}
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
)}
|
|
141
|
-
|
|
142
|
-
{/* Section Content */}
|
|
143
|
-
{!isCollapsed && (
|
|
144
|
-
<div className={cn('grid gap-4', gridClassName || gridCols[columns])}>
|
|
145
|
-
{children}
|
|
146
|
-
</div>
|
|
147
|
-
)}
|
|
148
|
-
</div>
|
|
149
|
-
);
|
|
150
|
-
};
|
|
151
|
-
|
|
152
|
-
export default FormSection;
|
|
@@ -1,219 +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
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import userEvent from '@testing-library/user-event';
|
|
12
|
-
import React from 'react';
|
|
13
|
-
import '@object-ui/components';
|
|
14
|
-
import '@object-ui/fields';
|
|
15
|
-
import { FormSection } from './FormSection';
|
|
16
|
-
import { ObjectForm } from './ObjectForm';
|
|
17
|
-
|
|
18
|
-
describe('FormSection', () => {
|
|
19
|
-
it('renders children without label', () => {
|
|
20
|
-
render(
|
|
21
|
-
<FormSection>
|
|
22
|
-
<div data-testid="child">Field content</div>
|
|
23
|
-
</FormSection>
|
|
24
|
-
);
|
|
25
|
-
|
|
26
|
-
expect(screen.getByTestId('child')).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('renders with label and description', () => {
|
|
30
|
-
render(
|
|
31
|
-
<FormSection label="Contact Info" description="Enter your contact details">
|
|
32
|
-
<div>Field</div>
|
|
33
|
-
</FormSection>
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText('Contact Info')).toBeInTheDocument();
|
|
37
|
-
expect(screen.getByText('Enter your contact details')).toBeInTheDocument();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('supports collapsible behavior', async () => {
|
|
41
|
-
const user = userEvent.setup();
|
|
42
|
-
|
|
43
|
-
render(
|
|
44
|
-
<FormSection label="Details" collapsible>
|
|
45
|
-
<div data-testid="content">Collapsible content</div>
|
|
46
|
-
</FormSection>
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
// Content should be visible initially
|
|
50
|
-
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
51
|
-
|
|
52
|
-
// Click to collapse
|
|
53
|
-
await user.click(screen.getByText('Details'));
|
|
54
|
-
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
|
|
55
|
-
|
|
56
|
-
// Click to expand
|
|
57
|
-
await user.click(screen.getByText('Details'));
|
|
58
|
-
expect(screen.getByTestId('content')).toBeInTheDocument();
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('starts collapsed when collapsed=true', () => {
|
|
62
|
-
render(
|
|
63
|
-
<FormSection label="Details" collapsible collapsed>
|
|
64
|
-
<div data-testid="content">Hidden content</div>
|
|
65
|
-
</FormSection>
|
|
66
|
-
);
|
|
67
|
-
|
|
68
|
-
expect(screen.queryByTestId('content')).not.toBeInTheDocument();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('applies multi-column grid classes', () => {
|
|
72
|
-
const { container } = render(
|
|
73
|
-
<FormSection columns={2}>
|
|
74
|
-
<div>Field 1</div>
|
|
75
|
-
<div>Field 2</div>
|
|
76
|
-
</FormSection>
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const grid = container.querySelector('.grid');
|
|
80
|
-
expect(grid).toBeInTheDocument();
|
|
81
|
-
expect(grid?.className).toContain('md:grid-cols-2');
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('ObjectForm with formType', () => {
|
|
86
|
-
const mockDataSource = {
|
|
87
|
-
getObjectSchema: vi.fn().mockResolvedValue({
|
|
88
|
-
name: 'contacts',
|
|
89
|
-
fields: {
|
|
90
|
-
firstName: { label: 'First Name', type: 'text', required: true },
|
|
91
|
-
lastName: { label: 'Last Name', type: 'text', required: false },
|
|
92
|
-
email: { label: 'Email', type: 'email', required: true },
|
|
93
|
-
phone: { label: 'Phone', type: 'phone', required: false },
|
|
94
|
-
street: { label: 'Street', type: 'text', required: false },
|
|
95
|
-
city: { label: 'City', type: 'text', required: false },
|
|
96
|
-
}
|
|
97
|
-
}),
|
|
98
|
-
findOne: vi.fn().mockResolvedValue({}),
|
|
99
|
-
find: vi.fn().mockResolvedValue([]),
|
|
100
|
-
create: vi.fn().mockResolvedValue({ id: '1' }),
|
|
101
|
-
update: vi.fn().mockResolvedValue({ id: '1' }),
|
|
102
|
-
delete: vi.fn().mockResolvedValue(true),
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
it('renders simple form by default (no formType)', async () => {
|
|
106
|
-
render(
|
|
107
|
-
<ObjectForm
|
|
108
|
-
schema={{
|
|
109
|
-
type: 'object-form',
|
|
110
|
-
objectName: 'contacts',
|
|
111
|
-
mode: 'create',
|
|
112
|
-
fields: ['firstName', 'lastName'],
|
|
113
|
-
}}
|
|
114
|
-
dataSource={mockDataSource as any}
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
117
|
-
|
|
118
|
-
// Wait for schema fetch
|
|
119
|
-
await screen.findByText(/first name/i, {}, { timeout: 5000 });
|
|
120
|
-
expect(screen.getByText(/last name/i)).toBeInTheDocument();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('renders simple form with sections', async () => {
|
|
124
|
-
render(
|
|
125
|
-
<ObjectForm
|
|
126
|
-
schema={{
|
|
127
|
-
type: 'object-form',
|
|
128
|
-
objectName: 'contacts',
|
|
129
|
-
mode: 'create',
|
|
130
|
-
formType: 'simple',
|
|
131
|
-
sections: [
|
|
132
|
-
{
|
|
133
|
-
label: 'Personal Info',
|
|
134
|
-
fields: ['firstName', 'lastName'],
|
|
135
|
-
columns: 2,
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
label: 'Contact Details',
|
|
139
|
-
fields: ['email', 'phone'],
|
|
140
|
-
columns: 2,
|
|
141
|
-
},
|
|
142
|
-
],
|
|
143
|
-
}}
|
|
144
|
-
dataSource={mockDataSource as any}
|
|
145
|
-
/>
|
|
146
|
-
);
|
|
147
|
-
|
|
148
|
-
// Wait for schema fetch and section rendering
|
|
149
|
-
await screen.findByText('Personal Info', {}, { timeout: 5000 });
|
|
150
|
-
expect(screen.getByText('Contact Details')).toBeInTheDocument();
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('renders tabbed form with sections as tabs', async () => {
|
|
154
|
-
render(
|
|
155
|
-
<ObjectForm
|
|
156
|
-
schema={{
|
|
157
|
-
type: 'object-form',
|
|
158
|
-
objectName: 'contacts',
|
|
159
|
-
mode: 'create',
|
|
160
|
-
formType: 'tabbed',
|
|
161
|
-
sections: [
|
|
162
|
-
{
|
|
163
|
-
name: 'personal',
|
|
164
|
-
label: 'Personal',
|
|
165
|
-
fields: ['firstName', 'lastName'],
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
name: 'contact',
|
|
169
|
-
label: 'Contact',
|
|
170
|
-
fields: ['email', 'phone'],
|
|
171
|
-
},
|
|
172
|
-
],
|
|
173
|
-
}}
|
|
174
|
-
dataSource={mockDataSource as any}
|
|
175
|
-
/>
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// Wait for tabs to render
|
|
179
|
-
await screen.findByRole('tab', { name: 'Personal' }, { timeout: 5000 });
|
|
180
|
-
expect(screen.getByRole('tab', { name: 'Contact' })).toBeInTheDocument();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it('renders wizard form with step indicator', async () => {
|
|
184
|
-
render(
|
|
185
|
-
<ObjectForm
|
|
186
|
-
schema={{
|
|
187
|
-
type: 'object-form',
|
|
188
|
-
objectName: 'contacts',
|
|
189
|
-
mode: 'create',
|
|
190
|
-
formType: 'wizard',
|
|
191
|
-
sections: [
|
|
192
|
-
{
|
|
193
|
-
label: 'Step 1: Basics',
|
|
194
|
-
fields: ['firstName', 'lastName'],
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
label: 'Step 2: Contact',
|
|
198
|
-
fields: ['email', 'phone'],
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
label: 'Step 3: Address',
|
|
202
|
-
fields: ['street', 'city'],
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
}}
|
|
206
|
-
dataSource={mockDataSource as any}
|
|
207
|
-
/>
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// Wait for loading to finish - wizard shows "Step X of Y" counter
|
|
211
|
-
await screen.findByText(/Step 1 of 3/, {}, { timeout: 5000 });
|
|
212
|
-
|
|
213
|
-
// Should show Next button (not Submit, since we're on step 1)
|
|
214
|
-
expect(screen.getByRole('button', { name: /next/i })).toBeInTheDocument();
|
|
215
|
-
|
|
216
|
-
// Step labels should be present (appears in both indicator and section header)
|
|
217
|
-
expect(screen.getAllByText('Step 1: Basics').length).toBeGreaterThanOrEqual(1);
|
|
218
|
-
});
|
|
219
|
-
});
|