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