@k3-universe/react-kit 0.0.3 → 0.0.5
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/dist/kit/builder/form/components/FormBuilder.d.ts +20 -0
- package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
- package/dist/kit/builder/form/components/FormBuilderField.d.ts +5 -5
- package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/ArrayField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/ArrayField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/AutocompleteField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/CheckboxField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/CheckboxField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/DateField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/DateField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/FileField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/FileField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/NumberField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/NumberField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/ObjectField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/ObjectField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/RadioField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/RadioField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/SelectField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/SelectField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/SwitchField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/SwitchField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/TextField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/TextField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/TextareaField.d.ts +3 -0
- package/dist/kit/builder/form/components/fields/TextareaField.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/index.d.ts +14 -0
- package/dist/kit/builder/form/components/fields/index.d.ts.map +1 -0
- package/dist/kit/builder/form/components/fields/types.d.ts +14 -0
- package/dist/kit/builder/form/components/fields/types.d.ts.map +1 -0
- package/dist/kit/themes/clean-slate.css +1 -1
- package/dist/kit/themes/default.css +1 -1
- package/dist/kit/themes/minimal-modern.css +1 -1
- package/dist/kit/themes/spotify.css +1 -1
- package/package.json +1 -1
- package/src/kit/builder/form/components/FormBuilder.tsx +66 -16
- package/src/kit/builder/form/components/FormBuilderField.tsx +139 -387
- package/src/kit/builder/form/components/fields/ArrayField.tsx +222 -0
- package/src/kit/builder/form/components/fields/AutocompleteField.tsx +25 -0
- package/src/kit/builder/form/components/fields/CheckboxField.tsx +56 -0
- package/src/kit/builder/form/components/fields/DateField.tsx +15 -0
- package/src/kit/builder/form/components/fields/FileField.tsx +14 -0
- package/src/kit/builder/form/components/fields/NumberField.tsx +15 -0
- package/src/kit/builder/form/components/fields/ObjectField.tsx +30 -0
- package/src/kit/builder/form/components/fields/RadioField.tsx +29 -0
- package/src/kit/builder/form/components/fields/SelectField.tsx +31 -0
- package/src/kit/builder/form/components/fields/SwitchField.tsx +56 -0
- package/src/kit/builder/form/components/fields/TextField.tsx +18 -0
- package/src/kit/builder/form/components/fields/TextareaField.tsx +15 -0
- package/src/kit/builder/form/components/fields/index.ts +13 -0
- package/src/kit/builder/form/components/fields/types.ts +14 -0
- package/src/stories/kit/builder/Form.ArrayLayouts.stories.tsx +153 -0
|
@@ -1,31 +1,33 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import { Control,
|
|
2
|
+
import { Control, FieldValues, useController } from 'react-hook-form';
|
|
3
3
|
import { cn } from '../../../../shadcn/lib/utils';
|
|
4
|
-
import { Button } from '../../../../shadcn/ui/button';
|
|
5
|
-
import { Input } from '../../../../shadcn/ui/input';
|
|
6
|
-
import { Textarea } from '../../../../shadcn/ui/textarea';
|
|
7
|
-
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../../../shadcn/ui/select';
|
|
8
|
-
import { Checkbox } from '../../../../shadcn/ui/checkbox';
|
|
9
|
-
import { Switch } from '../../../../shadcn/ui/switch';
|
|
10
|
-
import { RadioGroup, RadioGroupItem } from '../../../../shadcn/ui/radio-group';
|
|
11
4
|
import { Label } from '../../../../shadcn/ui/label';
|
|
12
|
-
import { Card, CardContent, CardHeader, CardTitle } from '../../../../shadcn/ui/card';
|
|
13
|
-
import { Plus, Trash2, GripVertical } from 'lucide-react';
|
|
14
5
|
import { FormBuilderFieldConfig } from './FormBuilder';
|
|
15
|
-
import {
|
|
16
|
-
|
|
6
|
+
import {
|
|
7
|
+
AutocompleteField,
|
|
8
|
+
TextField,
|
|
9
|
+
NumberField,
|
|
10
|
+
TextareaField,
|
|
11
|
+
SelectField,
|
|
12
|
+
CheckboxField,
|
|
13
|
+
SwitchField,
|
|
14
|
+
RadioField,
|
|
15
|
+
DateField,
|
|
16
|
+
FileField,
|
|
17
|
+
ObjectField,
|
|
18
|
+
ArrayField,
|
|
19
|
+
} from './fields';
|
|
17
20
|
|
|
18
21
|
export interface FormBuilderFieldProps {
|
|
19
22
|
field: FormBuilderFieldConfig;
|
|
20
|
-
control: Control<
|
|
21
|
-
onChange?: (value:
|
|
22
|
-
onFieldChange?: (name: string, value:
|
|
23
|
+
control: Control<FieldValues>;
|
|
24
|
+
onChange?: (value: unknown) => void;
|
|
25
|
+
onFieldChange?: (name: string, value: unknown, allValues: Record<string, unknown>) => void;
|
|
23
26
|
parentPath?: string;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
|
-
export function FormBuilderField({ field, control, onChange,
|
|
29
|
+
export function FormBuilderField({ field, control, onChange, parentPath }: FormBuilderFieldProps) {
|
|
27
30
|
const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name;
|
|
28
|
-
const NULL_SENTINEL = '__NULL__';
|
|
29
31
|
|
|
30
32
|
const {
|
|
31
33
|
field: controllerField,
|
|
@@ -36,420 +38,170 @@ export function FormBuilderField({ field, control, onChange, onFieldChange, pare
|
|
|
36
38
|
disabled: field.disabled,
|
|
37
39
|
});
|
|
38
40
|
|
|
39
|
-
const handleChange = useCallback((value:
|
|
41
|
+
const handleChange = useCallback((value: unknown) => {
|
|
40
42
|
controllerField.onChange(value);
|
|
41
43
|
onChange?.(value);
|
|
42
44
|
}, [controllerField.onChange, onChange]);
|
|
45
|
+
const baseClassName = cn(
|
|
46
|
+
error && 'border-destructive focus-visible:ring-destructive',
|
|
47
|
+
field.className,
|
|
48
|
+
);
|
|
43
49
|
|
|
44
|
-
const
|
|
45
|
-
const baseProps = {
|
|
46
|
-
id: fieldPath,
|
|
47
|
-
disabled: field.disabled,
|
|
48
|
-
placeholder: field.placeholder,
|
|
49
|
-
className: cn(
|
|
50
|
-
error && 'border-destructive focus-visible:ring-destructive',
|
|
51
|
-
field.className,
|
|
52
|
-
),
|
|
53
|
-
};
|
|
54
|
-
|
|
50
|
+
const renderField = () => {
|
|
55
51
|
switch (field.type) {
|
|
56
|
-
case 'autocomplete':
|
|
57
|
-
const options: AutocompleteOption[] = (field.options ?? [])
|
|
58
|
-
.filter((o): o is { label: string; value: string | number } => o.value !== null && o.value !== undefined)
|
|
59
|
-
.map(o => ({ label: o.label, value: o.value }));
|
|
52
|
+
case 'autocomplete':
|
|
60
53
|
return (
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
placeholder={field.placeholder}
|
|
69
|
-
searchPlaceholder={field.searchPlaceholder}
|
|
70
|
-
renderOption={field.renderOption}
|
|
71
|
-
disabled={field.disabled}
|
|
72
|
-
className={baseProps.className}
|
|
54
|
+
<AutocompleteField
|
|
55
|
+
field={field}
|
|
56
|
+
control={control}
|
|
57
|
+
fieldPath={fieldPath}
|
|
58
|
+
value={controllerField.value}
|
|
59
|
+
onChange={handleChange}
|
|
60
|
+
className={baseClassName}
|
|
73
61
|
/>
|
|
74
62
|
);
|
|
75
|
-
}
|
|
76
63
|
case 'text':
|
|
77
64
|
case 'email':
|
|
78
65
|
case 'password':
|
|
79
66
|
return (
|
|
80
|
-
<
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
67
|
+
<TextField
|
|
68
|
+
field={field}
|
|
69
|
+
control={control}
|
|
70
|
+
fieldPath={fieldPath}
|
|
71
|
+
value={controllerField.value}
|
|
72
|
+
onChange={handleChange}
|
|
73
|
+
className={baseClassName}
|
|
85
74
|
/>
|
|
86
75
|
);
|
|
87
|
-
|
|
88
76
|
case 'number':
|
|
89
77
|
return (
|
|
90
|
-
<
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
78
|
+
<NumberField
|
|
79
|
+
field={field}
|
|
80
|
+
control={control}
|
|
81
|
+
fieldPath={fieldPath}
|
|
82
|
+
value={controllerField.value}
|
|
83
|
+
onChange={handleChange}
|
|
84
|
+
className={baseClassName}
|
|
95
85
|
/>
|
|
96
86
|
);
|
|
97
|
-
|
|
98
87
|
case 'textarea':
|
|
99
88
|
return (
|
|
100
|
-
<
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
89
|
+
<TextareaField
|
|
90
|
+
field={field}
|
|
91
|
+
control={control}
|
|
92
|
+
fieldPath={fieldPath}
|
|
93
|
+
value={controllerField.value}
|
|
94
|
+
onChange={handleChange}
|
|
95
|
+
className={baseClassName}
|
|
105
96
|
/>
|
|
106
97
|
);
|
|
107
|
-
|
|
108
|
-
case 'select': {
|
|
109
|
-
const toUiValue = (val: unknown) => (val === null || val === undefined ? NULL_SENTINEL : String(val));
|
|
110
|
-
const fromUiValue = (val: string) => {
|
|
111
|
-
const match = field.options?.find(opt => toUiValue(opt.value) === val);
|
|
112
|
-
return match ? match.value : null;
|
|
113
|
-
};
|
|
98
|
+
case 'select':
|
|
114
99
|
return (
|
|
115
|
-
<
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<SelectContent>
|
|
124
|
-
{field.options?.map(option => (
|
|
125
|
-
<SelectItem key={toUiValue(option.value)} value={toUiValue(option.value)}>
|
|
126
|
-
{option.label}
|
|
127
|
-
</SelectItem>
|
|
128
|
-
))}
|
|
129
|
-
</SelectContent>
|
|
130
|
-
</Select>
|
|
100
|
+
<SelectField
|
|
101
|
+
field={field}
|
|
102
|
+
control={control}
|
|
103
|
+
fieldPath={fieldPath}
|
|
104
|
+
value={controllerField.value}
|
|
105
|
+
onChange={handleChange}
|
|
106
|
+
className={baseClassName}
|
|
107
|
+
/>
|
|
131
108
|
);
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
case 'checkbox': {
|
|
135
|
-
const placement = field.labelPlacement ?? 'inline';
|
|
136
|
-
if (placement === 'stacked') {
|
|
137
|
-
const labelId = `${fieldPath}-label`;
|
|
138
|
-
return (
|
|
139
|
-
<div className="space-y-2">
|
|
140
|
-
<Label id={labelId} className="text-sm font-medium">
|
|
141
|
-
{field.label}
|
|
142
|
-
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
143
|
-
</Label>
|
|
144
|
-
<Checkbox
|
|
145
|
-
aria-labelledby={labelId}
|
|
146
|
-
id={fieldPath}
|
|
147
|
-
checked={controllerField.value || false}
|
|
148
|
-
onCheckedChange={handleChange}
|
|
149
|
-
disabled={field.disabled}
|
|
150
|
-
className={cn(error && 'border-destructive', field.className)}
|
|
151
|
-
/>
|
|
152
|
-
</div>
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
if (placement === 'hidden') {
|
|
156
|
-
return (
|
|
157
|
-
<Checkbox
|
|
158
|
-
id={fieldPath}
|
|
159
|
-
checked={controllerField.value || false}
|
|
160
|
-
onCheckedChange={handleChange}
|
|
161
|
-
disabled={field.disabled}
|
|
162
|
-
className={cn(error && 'border-destructive', field.className)}
|
|
163
|
-
/>
|
|
164
|
-
);
|
|
165
|
-
}
|
|
166
|
-
// inline (default)
|
|
109
|
+
case 'checkbox':
|
|
167
110
|
return (
|
|
168
|
-
<
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
<Label htmlFor={fieldPath} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
177
|
-
{field.label}
|
|
178
|
-
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
179
|
-
</Label>
|
|
180
|
-
</div>
|
|
111
|
+
<CheckboxField
|
|
112
|
+
field={field}
|
|
113
|
+
control={control}
|
|
114
|
+
fieldPath={fieldPath}
|
|
115
|
+
value={controllerField.value}
|
|
116
|
+
onChange={handleChange}
|
|
117
|
+
className={baseClassName}
|
|
118
|
+
/>
|
|
181
119
|
);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
case 'switch': {
|
|
185
|
-
const placement = field.labelPlacement ?? 'inline';
|
|
186
|
-
if (placement === 'stacked') {
|
|
187
|
-
const labelId = `${fieldPath}-label`;
|
|
188
|
-
return (
|
|
189
|
-
<div className="space-y-2">
|
|
190
|
-
<Label id={labelId} className="text-sm font-medium">
|
|
191
|
-
{field.label}
|
|
192
|
-
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
193
|
-
</Label>
|
|
194
|
-
<Switch
|
|
195
|
-
aria-labelledby={labelId}
|
|
196
|
-
id={fieldPath}
|
|
197
|
-
checked={controllerField.value || false}
|
|
198
|
-
onCheckedChange={handleChange}
|
|
199
|
-
disabled={field.disabled}
|
|
200
|
-
className={cn(error && 'border-destructive', field.className)}
|
|
201
|
-
/>
|
|
202
|
-
</div>
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
if (placement === 'hidden') {
|
|
206
|
-
return (
|
|
207
|
-
<Switch
|
|
208
|
-
id={fieldPath}
|
|
209
|
-
checked={controllerField.value || false}
|
|
210
|
-
onCheckedChange={handleChange}
|
|
211
|
-
disabled={field.disabled}
|
|
212
|
-
className={cn(error && 'border-destructive', field.className)}
|
|
213
|
-
/>
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
// inline (default)
|
|
120
|
+
case 'switch':
|
|
217
121
|
return (
|
|
218
|
-
<
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
<Label htmlFor={fieldPath} className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
|
227
|
-
{field.label}
|
|
228
|
-
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
229
|
-
</Label>
|
|
230
|
-
</div>
|
|
122
|
+
<SwitchField
|
|
123
|
+
field={field}
|
|
124
|
+
control={control}
|
|
125
|
+
fieldPath={fieldPath}
|
|
126
|
+
value={controllerField.value}
|
|
127
|
+
onChange={handleChange}
|
|
128
|
+
className={baseClassName}
|
|
129
|
+
/>
|
|
231
130
|
);
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
case 'radio': {
|
|
235
|
-
const toUiValue = (val: unknown) => (val === null || val === undefined ? NULL_SENTINEL : String(val));
|
|
236
|
-
const fromUiValue = (val: string) => {
|
|
237
|
-
const match = field.options?.find(opt => toUiValue(opt.value) === val);
|
|
238
|
-
return match ? match.value : null;
|
|
239
|
-
};
|
|
131
|
+
case 'radio':
|
|
240
132
|
return (
|
|
241
|
-
<
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
{
|
|
248
|
-
|
|
249
|
-
<RadioGroupItem value={toUiValue(option.value)} id={`${fieldPath}-${toUiValue(option.value)}`} />
|
|
250
|
-
<Label htmlFor={`${fieldPath}-${toUiValue(option.value)}`}>{option.label}</Label>
|
|
251
|
-
</div>
|
|
252
|
-
))}
|
|
253
|
-
</RadioGroup>
|
|
133
|
+
<RadioField
|
|
134
|
+
field={field}
|
|
135
|
+
control={control}
|
|
136
|
+
fieldPath={fieldPath}
|
|
137
|
+
value={controllerField.value}
|
|
138
|
+
onChange={handleChange}
|
|
139
|
+
className={baseClassName}
|
|
140
|
+
/>
|
|
254
141
|
);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
142
|
case 'date':
|
|
258
143
|
return (
|
|
259
|
-
<
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
144
|
+
<DateField
|
|
145
|
+
field={field}
|
|
146
|
+
control={control}
|
|
147
|
+
fieldPath={fieldPath}
|
|
148
|
+
value={controllerField.value}
|
|
149
|
+
onChange={handleChange}
|
|
150
|
+
className={baseClassName}
|
|
264
151
|
/>
|
|
265
152
|
);
|
|
266
|
-
|
|
267
153
|
case 'file':
|
|
268
154
|
return (
|
|
269
|
-
<
|
|
270
|
-
{
|
|
271
|
-
|
|
272
|
-
|
|
155
|
+
<FileField
|
|
156
|
+
field={field}
|
|
157
|
+
control={control}
|
|
158
|
+
fieldPath={fieldPath}
|
|
159
|
+
value={controllerField.value}
|
|
160
|
+
onChange={handleChange}
|
|
161
|
+
className={baseClassName}
|
|
162
|
+
/>
|
|
163
|
+
);
|
|
164
|
+
case 'object':
|
|
165
|
+
return (
|
|
166
|
+
<ObjectField
|
|
167
|
+
field={field}
|
|
168
|
+
control={control}
|
|
169
|
+
fieldPath={fieldPath}
|
|
170
|
+
value={controllerField.value}
|
|
171
|
+
onChange={handleChange}
|
|
172
|
+
className={baseClassName}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
case 'array':
|
|
176
|
+
return (
|
|
177
|
+
<ArrayField
|
|
178
|
+
field={field}
|
|
179
|
+
control={control}
|
|
180
|
+
fieldPath={fieldPath}
|
|
181
|
+
value={controllerField.value}
|
|
182
|
+
onChange={handleChange}
|
|
183
|
+
className={baseClassName}
|
|
273
184
|
/>
|
|
274
185
|
);
|
|
275
|
-
|
|
276
186
|
default:
|
|
277
187
|
return (
|
|
278
|
-
<
|
|
279
|
-
{
|
|
280
|
-
|
|
281
|
-
|
|
188
|
+
<TextField
|
|
189
|
+
field={field}
|
|
190
|
+
control={control}
|
|
191
|
+
fieldPath={fieldPath}
|
|
192
|
+
value={controllerField.value}
|
|
193
|
+
onChange={handleChange}
|
|
194
|
+
className={baseClassName}
|
|
282
195
|
/>
|
|
283
196
|
);
|
|
284
197
|
}
|
|
285
198
|
};
|
|
286
199
|
|
|
287
|
-
|
|
288
|
-
if (!field.fields) return null;
|
|
289
|
-
|
|
290
|
-
return (
|
|
291
|
-
<Card className={field.className}>
|
|
292
|
-
<CardHeader className="pb-3">
|
|
293
|
-
<CardTitle className="text-base">{field.label}</CardTitle>
|
|
294
|
-
{field.description && (
|
|
295
|
-
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
296
|
-
)}
|
|
297
|
-
</CardHeader>
|
|
298
|
-
<CardContent className="space-y-4">
|
|
299
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
300
|
-
{field.fields.map(subField => (
|
|
301
|
-
<FormBuilderField
|
|
302
|
-
key={subField.name}
|
|
303
|
-
field={subField}
|
|
304
|
-
control={control}
|
|
305
|
-
parentPath={fieldPath}
|
|
306
|
-
onChange={onChange}
|
|
307
|
-
onFieldChange={onFieldChange}
|
|
308
|
-
/>
|
|
309
|
-
))}
|
|
310
|
-
</div>
|
|
311
|
-
</CardContent>
|
|
312
|
-
</Card>
|
|
313
|
-
);
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
const renderArrayField = () => {
|
|
317
|
-
const { fields, append, remove } = useFieldArray({
|
|
318
|
-
control,
|
|
319
|
-
name: fieldPath,
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
const addItem = () => {
|
|
323
|
-
if (field.fields && field.fields.length === 1) {
|
|
324
|
-
// Single field array (e.g., array of strings)
|
|
325
|
-
const defaultValue = field.fields[0].defaultValue || '';
|
|
326
|
-
append(defaultValue);
|
|
327
|
-
}
|
|
328
|
-
else if (field.fields) {
|
|
329
|
-
// Object array
|
|
330
|
-
const defaultObject: Record<string, any> = {};
|
|
331
|
-
field.fields.forEach((subField) => {
|
|
332
|
-
defaultObject[subField.name] = subField.defaultValue || '';
|
|
333
|
-
});
|
|
334
|
-
append(defaultObject);
|
|
335
|
-
}
|
|
336
|
-
else {
|
|
337
|
-
append('');
|
|
338
|
-
}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
return (
|
|
342
|
-
<Card className={field.className}>
|
|
343
|
-
<CardHeader className="pb-3">
|
|
344
|
-
<div className="flex items-center justify-between">
|
|
345
|
-
<div>
|
|
346
|
-
<CardTitle className="text-base">{field.label}</CardTitle>
|
|
347
|
-
{field.description && (
|
|
348
|
-
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
349
|
-
)}
|
|
350
|
-
</div>
|
|
351
|
-
<Button
|
|
352
|
-
type="button"
|
|
353
|
-
variant="outline"
|
|
354
|
-
size="sm"
|
|
355
|
-
onClick={addItem}
|
|
356
|
-
disabled={field.disabled}
|
|
357
|
-
>
|
|
358
|
-
<Plus className="h-4 w-4 mr-1" />
|
|
359
|
-
Add Item
|
|
360
|
-
</Button>
|
|
361
|
-
</div>
|
|
362
|
-
</CardHeader>
|
|
363
|
-
<CardContent className="space-y-4">
|
|
364
|
-
{fields.length === 0 ? (
|
|
365
|
-
<p className="text-sm text-muted-foreground text-center py-4">
|
|
366
|
-
No items added yet. Click "Add Item" to get started.
|
|
367
|
-
</p>
|
|
368
|
-
) : (
|
|
369
|
-
fields.map((item, index) => (
|
|
370
|
-
<Card key={item.id} className="relative">
|
|
371
|
-
<CardHeader className="pb-3">
|
|
372
|
-
<div className="flex items-center justify-between">
|
|
373
|
-
<div className="flex items-center gap-2">
|
|
374
|
-
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
|
375
|
-
<span className="text-sm font-medium">
|
|
376
|
-
Item
|
|
377
|
-
{index + 1}
|
|
378
|
-
</span>
|
|
379
|
-
</div>
|
|
380
|
-
<Button
|
|
381
|
-
type="button"
|
|
382
|
-
variant="ghost"
|
|
383
|
-
size="sm"
|
|
384
|
-
onClick={() => remove(index)}
|
|
385
|
-
disabled={field.disabled}
|
|
386
|
-
>
|
|
387
|
-
<Trash2 className="h-4 w-4" />
|
|
388
|
-
</Button>
|
|
389
|
-
</div>
|
|
390
|
-
</CardHeader>
|
|
391
|
-
<CardContent>
|
|
392
|
-
{field.fields && field.fields.length === 1 ? (
|
|
393
|
-
// Single field array
|
|
394
|
-
<FormBuilderField
|
|
395
|
-
field={{
|
|
396
|
-
...field.fields[0],
|
|
397
|
-
name: field.fields[0].name,
|
|
398
|
-
label: field.fields[0].label || 'Value',
|
|
399
|
-
}}
|
|
400
|
-
control={control}
|
|
401
|
-
parentPath={`${fieldPath}.${index}`}
|
|
402
|
-
onChange={onChange}
|
|
403
|
-
/>
|
|
404
|
-
) : field.fields ? (
|
|
405
|
-
// Object array
|
|
406
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
407
|
-
{field.fields.map(subField => (
|
|
408
|
-
<FormBuilderField
|
|
409
|
-
key={subField.name}
|
|
410
|
-
field={subField}
|
|
411
|
-
control={control}
|
|
412
|
-
parentPath={`${fieldPath}.${index}`}
|
|
413
|
-
onChange={onChange}
|
|
414
|
-
onFieldChange={onFieldChange}
|
|
415
|
-
/>
|
|
416
|
-
))}
|
|
417
|
-
</div>
|
|
418
|
-
) : (
|
|
419
|
-
// Fallback for arrays without field definitions
|
|
420
|
-
<Input
|
|
421
|
-
value={controllerField.value?.[index] || ''}
|
|
422
|
-
onChange={(e) => {
|
|
423
|
-
const newArray = [...(controllerField.value || [])];
|
|
424
|
-
newArray[index] = e.target.value;
|
|
425
|
-
handleChange(newArray);
|
|
426
|
-
}}
|
|
427
|
-
placeholder={`Item ${index + 1}`}
|
|
428
|
-
disabled={field.disabled}
|
|
429
|
-
/>
|
|
430
|
-
)}
|
|
431
|
-
</CardContent>
|
|
432
|
-
</Card>
|
|
433
|
-
))
|
|
434
|
-
)}
|
|
435
|
-
</CardContent>
|
|
436
|
-
</Card>
|
|
437
|
-
);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
if (field.type === 'object') {
|
|
441
|
-
return renderObjectField();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
if (field.type === 'array') {
|
|
445
|
-
return renderArrayField();
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// For checkbox/switch, label may be inline/stacked/hidden handled inside renderBasicField
|
|
200
|
+
// For checkbox/switch, label is handled inside the specific field component
|
|
449
201
|
if (field.type === 'checkbox' || field.type === 'switch') {
|
|
450
202
|
return (
|
|
451
203
|
<div className={cn('space-y-2', field.gridCols && `md:col-span-${field.gridCols}`)}>
|
|
452
|
-
{
|
|
204
|
+
{renderField()}
|
|
453
205
|
{field.description && (
|
|
454
206
|
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
455
207
|
)}
|
|
@@ -462,10 +214,10 @@ export function FormBuilderField({ field, control, onChange, onFieldChange, pare
|
|
|
462
214
|
|
|
463
215
|
// Non-checkbox fields: support labelPlacement
|
|
464
216
|
const placement = field.labelPlacement ?? 'stacked';
|
|
465
|
-
if (placement === 'hidden') {
|
|
217
|
+
if (placement === 'hidden' || field.type === 'array') {
|
|
466
218
|
return (
|
|
467
219
|
<div className={cn('space-y-2', field.gridCols && `md:col-span-${field.gridCols}`)}>
|
|
468
|
-
{
|
|
220
|
+
{renderField()}
|
|
469
221
|
{field.description && (
|
|
470
222
|
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
471
223
|
)}
|
|
@@ -484,7 +236,7 @@ export function FormBuilderField({ field, control, onChange, onFieldChange, pare
|
|
|
484
236
|
{field.label}
|
|
485
237
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
486
238
|
</Label>
|
|
487
|
-
{
|
|
239
|
+
{renderField()}
|
|
488
240
|
</div>
|
|
489
241
|
{field.description && (
|
|
490
242
|
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
@@ -503,7 +255,7 @@ export function FormBuilderField({ field, control, onChange, onFieldChange, pare
|
|
|
503
255
|
{field.label}
|
|
504
256
|
{field.required && <span className="text-destructive ml-1">*</span>}
|
|
505
257
|
</Label>
|
|
506
|
-
{
|
|
258
|
+
{renderField()}
|
|
507
259
|
{field.description && (
|
|
508
260
|
<p className="text-sm text-muted-foreground">{field.description}</p>
|
|
509
261
|
)}
|