@k3-universe/react-kit 0.0.13 → 0.0.15
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/index.js +1773 -1739
- package/dist/kit/builder/data-table/types.d.ts +1 -1
- package/dist/kit/builder/data-table/types.d.ts.map +1 -1
- package/dist/kit/builder/form/components/FormBuilder.d.ts +3 -172
- package/dist/kit/builder/form/components/FormBuilder.d.ts.map +1 -1
- package/dist/kit/builder/form/components/FormBuilderContext.d.ts +18 -0
- package/dist/kit/builder/form/components/FormBuilderContext.d.ts.map +1 -0
- package/dist/kit/builder/form/components/FormBuilderField.d.ts +8 -8
- package/dist/kit/builder/form/components/FormBuilderField.d.ts.map +1 -1
- package/dist/kit/builder/form/components/fields/types.d.ts +3 -3
- package/dist/kit/builder/form/components/fields/types.d.ts.map +1 -1
- package/dist/kit/builder/form/components/sectionNodes.d.ts +17 -0
- package/dist/kit/builder/form/components/sectionNodes.d.ts.map +1 -0
- package/dist/kit/builder/form/index.d.ts +1 -0
- package/dist/kit/builder/form/index.d.ts.map +1 -1
- package/dist/kit/builder/form/types.d.ts +176 -0
- package/dist/kit/builder/form/types.d.ts.map +1 -0
- package/dist/kit/builder/form/utils/common-forms.d.ts +1 -1
- package/dist/kit/builder/form/utils/common-forms.d.ts.map +1 -1
- package/dist/kit/builder/form/utils/field-factories.d.ts +3 -3
- package/dist/kit/builder/form/utils/field-factories.d.ts.map +1 -1
- package/dist/kit/builder/form/utils/section-factories.d.ts +4 -4
- package/dist/kit/builder/form/utils/section-factories.d.ts.map +1 -1
- package/dist/kit/builder/stack-dialog/provider.d.ts.map +1 -1
- package/dist/kit/builder/stack-dialog/renderer.d.ts.map +1 -1
- package/dist/kit/components/autocomplete/Autocomplete.d.ts +8 -8
- package/dist/kit/components/autocomplete/Autocomplete.d.ts.map +1 -1
- package/dist/kit/components/autocomplete/types.d.ts +6 -4
- package/dist/kit/components/autocomplete/types.d.ts.map +1 -1
- package/dist/kit/themes/clean-slate.css +3 -3
- package/dist/kit/themes/default.css +4 -4
- package/dist/kit/themes/minimal-modern.css +3 -3
- package/dist/kit/themes/spotify.css +3 -3
- package/package.json +1 -1
- package/src/kit/builder/data-table/components/DataTable.tsx +1 -1
- package/src/kit/builder/data-table/types.ts +1 -1
- package/src/kit/builder/form/components/FormBuilder.tsx +113 -369
- package/src/kit/builder/form/components/FormBuilderContext.tsx +45 -0
- package/src/kit/builder/form/components/FormBuilderField.tsx +42 -34
- package/src/kit/builder/form/components/fields/AutocompleteField.tsx +2 -2
- package/src/kit/builder/form/components/fields/types.ts +3 -3
- package/src/kit/builder/form/components/sectionNodes.tsx +116 -0
- package/src/kit/builder/form/index.ts +1 -0
- package/src/kit/builder/form/types.ts +200 -0
- package/src/kit/builder/form/utils/common-forms.ts +1 -1
- package/src/kit/builder/form/utils/field-factories.ts +5 -5
- package/src/kit/builder/form/utils/section-factories.ts +10 -10
- package/src/kit/builder/stack-dialog/provider.tsx +2 -1
- package/src/kit/builder/stack-dialog/renderer.tsx +6 -7
- package/src/kit/components/autocomplete/Autocomplete.tsx +34 -26
- package/src/kit/components/autocomplete/types.ts +7 -5
- package/src/kit/themes/default.css +1 -1
- package/src/shadcn/ui/button.tsx +1 -1
- package/src/shadcn/ui/command.tsx +1 -1
- package/src/shadcn/ui/input.tsx +1 -1
- package/src/shadcn/ui/popover.tsx +1 -1
- package/src/shadcn/ui/select.tsx +1 -1
- package/src/shadcn/ui/textarea.tsx +1 -1
- package/src/stories/kit/builder/Form.MultipleFormBuilder.stories.tsx +335 -0
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
|
-
import type { Control, FieldValues } from 'react-hook-form';
|
|
2
|
+
import type { Control, FieldValues, Path } from 'react-hook-form';
|
|
3
3
|
import { useController } from 'react-hook-form';
|
|
4
4
|
import { cn } from '../../../../shadcn/lib/utils';
|
|
5
5
|
import { Label } from '../../../../shadcn/ui/label';
|
|
6
|
-
import type { FormBuilderFieldConfig } from '
|
|
6
|
+
import type { FormBuilderFieldConfig } from '../types';
|
|
7
7
|
import {
|
|
8
8
|
AutocompleteField,
|
|
9
9
|
TextField,
|
|
@@ -27,29 +27,37 @@ import {
|
|
|
27
27
|
ArrayField,
|
|
28
28
|
} from './fields';
|
|
29
29
|
|
|
30
|
-
export interface FormBuilderFieldProps
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
export interface FormBuilderFieldProps<
|
|
31
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
32
|
+
TName extends string | Path<TFieldValues> = Path<TFieldValues>
|
|
33
|
+
> {
|
|
34
|
+
field: FormBuilderFieldConfig<TFieldValues, TName>;
|
|
35
|
+
control: Control<TFieldValues>;
|
|
36
|
+
onChange?: (value: unknown, ...extras: unknown[]) => void;
|
|
37
|
+
onFieldChange?: (name: import('react-hook-form').Path<TFieldValues> | string, value: unknown, allValues: TFieldValues) => void;
|
|
35
38
|
parentPath?: string;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export function FormBuilderField
|
|
39
|
-
|
|
41
|
+
export function FormBuilderField<
|
|
42
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
43
|
+
TName extends string | Path<TFieldValues> = Path<TFieldValues>
|
|
44
|
+
>({ field, control, onChange, parentPath }: FormBuilderFieldProps<TFieldValues, TName>) {
|
|
45
|
+
const fieldPath = parentPath ? `${parentPath}.${field.name}` : (field.name as string);
|
|
40
46
|
|
|
41
47
|
const {
|
|
42
48
|
field: controllerField,
|
|
43
49
|
fieldState: { error },
|
|
44
|
-
} = useController({
|
|
45
|
-
name: fieldPath
|
|
50
|
+
} = useController<TFieldValues>({
|
|
51
|
+
name: fieldPath as unknown as Path<TFieldValues>,
|
|
46
52
|
control,
|
|
47
53
|
disabled: field.disabled,
|
|
48
54
|
});
|
|
49
55
|
|
|
50
|
-
const handleChange = useCallback((value: unknown) => {
|
|
56
|
+
const handleChange = useCallback((value: unknown, ...extras: unknown[]) => {
|
|
57
|
+
// Only patch the RHF value with the first argument (the canonical value)
|
|
51
58
|
controllerField.onChange(value);
|
|
52
|
-
|
|
59
|
+
// Forward any extra metadata upstream (e.g., option, raw)
|
|
60
|
+
onChange?.(value, ...extras);
|
|
53
61
|
}, [controllerField, onChange]);
|
|
54
62
|
const baseClassName = cn(
|
|
55
63
|
error && 'border-destructive focus-visible:ring-destructive',
|
|
@@ -62,7 +70,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
62
70
|
return (
|
|
63
71
|
<AutocompleteField
|
|
64
72
|
field={field}
|
|
65
|
-
control={control}
|
|
73
|
+
control={control as unknown as Control<FieldValues>}
|
|
66
74
|
fieldPath={fieldPath}
|
|
67
75
|
value={controllerField.value}
|
|
68
76
|
onChange={handleChange}
|
|
@@ -75,7 +83,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
75
83
|
return (
|
|
76
84
|
<TextField
|
|
77
85
|
field={field}
|
|
78
|
-
control={control}
|
|
86
|
+
control={control as unknown as Control<FieldValues>}
|
|
79
87
|
fieldPath={fieldPath}
|
|
80
88
|
value={controllerField.value}
|
|
81
89
|
onChange={handleChange}
|
|
@@ -86,7 +94,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
86
94
|
return (
|
|
87
95
|
<NumberField
|
|
88
96
|
field={field}
|
|
89
|
-
control={control}
|
|
97
|
+
control={control as unknown as Control<FieldValues>}
|
|
90
98
|
fieldPath={fieldPath}
|
|
91
99
|
value={controllerField.value}
|
|
92
100
|
onChange={handleChange}
|
|
@@ -97,7 +105,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
97
105
|
return (
|
|
98
106
|
<TextareaField
|
|
99
107
|
field={field}
|
|
100
|
-
control={control}
|
|
108
|
+
control={control as unknown as Control<FieldValues>}
|
|
101
109
|
fieldPath={fieldPath}
|
|
102
110
|
value={controllerField.value}
|
|
103
111
|
onChange={handleChange}
|
|
@@ -108,7 +116,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
108
116
|
return (
|
|
109
117
|
<SelectField
|
|
110
118
|
field={field}
|
|
111
|
-
control={control}
|
|
119
|
+
control={control as unknown as Control<FieldValues>}
|
|
112
120
|
fieldPath={fieldPath}
|
|
113
121
|
value={controllerField.value}
|
|
114
122
|
onChange={handleChange}
|
|
@@ -119,7 +127,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
119
127
|
return (
|
|
120
128
|
<CheckboxField
|
|
121
129
|
field={field}
|
|
122
|
-
control={control}
|
|
130
|
+
control={control as unknown as Control<FieldValues>}
|
|
123
131
|
fieldPath={fieldPath}
|
|
124
132
|
value={controllerField.value}
|
|
125
133
|
onChange={handleChange}
|
|
@@ -130,7 +138,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
130
138
|
return (
|
|
131
139
|
<SwitchField
|
|
132
140
|
field={field}
|
|
133
|
-
control={control}
|
|
141
|
+
control={control as unknown as Control<FieldValues>}
|
|
134
142
|
fieldPath={fieldPath}
|
|
135
143
|
value={controllerField.value}
|
|
136
144
|
onChange={handleChange}
|
|
@@ -141,7 +149,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
141
149
|
return (
|
|
142
150
|
<RadioField
|
|
143
151
|
field={field}
|
|
144
|
-
control={control}
|
|
152
|
+
control={control as unknown as Control<FieldValues>}
|
|
145
153
|
fieldPath={fieldPath}
|
|
146
154
|
value={controllerField.value}
|
|
147
155
|
onChange={handleChange}
|
|
@@ -152,7 +160,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
152
160
|
return (
|
|
153
161
|
<DateField
|
|
154
162
|
field={field}
|
|
155
|
-
control={control}
|
|
163
|
+
control={control as unknown as Control<FieldValues>}
|
|
156
164
|
fieldPath={fieldPath}
|
|
157
165
|
value={controllerField.value}
|
|
158
166
|
onChange={handleChange}
|
|
@@ -163,7 +171,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
163
171
|
return (
|
|
164
172
|
<DatePickerField
|
|
165
173
|
field={field}
|
|
166
|
-
control={control}
|
|
174
|
+
control={control as unknown as Control<FieldValues>}
|
|
167
175
|
fieldPath={fieldPath}
|
|
168
176
|
value={controllerField.value}
|
|
169
177
|
onChange={handleChange}
|
|
@@ -174,7 +182,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
174
182
|
return (
|
|
175
183
|
<DateRangePickerField
|
|
176
184
|
field={field}
|
|
177
|
-
control={control}
|
|
185
|
+
control={control as unknown as Control<FieldValues>}
|
|
178
186
|
fieldPath={fieldPath}
|
|
179
187
|
value={controllerField.value}
|
|
180
188
|
onChange={handleChange}
|
|
@@ -185,7 +193,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
185
193
|
return (
|
|
186
194
|
<MonthPickerField
|
|
187
195
|
field={field}
|
|
188
|
-
control={control}
|
|
196
|
+
control={control as unknown as Control<FieldValues>}
|
|
189
197
|
fieldPath={fieldPath}
|
|
190
198
|
value={controllerField.value}
|
|
191
199
|
onChange={handleChange}
|
|
@@ -196,7 +204,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
196
204
|
return (
|
|
197
205
|
<MonthRangePickerField
|
|
198
206
|
field={field}
|
|
199
|
-
control={control}
|
|
207
|
+
control={control as unknown as Control<FieldValues>}
|
|
200
208
|
fieldPath={fieldPath}
|
|
201
209
|
value={controllerField.value}
|
|
202
210
|
onChange={handleChange}
|
|
@@ -207,7 +215,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
207
215
|
return (
|
|
208
216
|
<TimePickerField
|
|
209
217
|
field={field}
|
|
210
|
-
control={control}
|
|
218
|
+
control={control as unknown as Control<FieldValues>}
|
|
211
219
|
fieldPath={fieldPath}
|
|
212
220
|
value={controllerField.value}
|
|
213
221
|
onChange={handleChange}
|
|
@@ -218,7 +226,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
218
226
|
return (
|
|
219
227
|
<TimeRangePickerField
|
|
220
228
|
field={field}
|
|
221
|
-
control={control}
|
|
229
|
+
control={control as unknown as Control<FieldValues>}
|
|
222
230
|
fieldPath={fieldPath}
|
|
223
231
|
value={controllerField.value}
|
|
224
232
|
onChange={handleChange}
|
|
@@ -229,7 +237,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
229
237
|
return (
|
|
230
238
|
<DateTimePickerField
|
|
231
239
|
field={field}
|
|
232
|
-
control={control}
|
|
240
|
+
control={control as unknown as Control<FieldValues>}
|
|
233
241
|
fieldPath={fieldPath}
|
|
234
242
|
value={controllerField.value}
|
|
235
243
|
onChange={handleChange}
|
|
@@ -240,7 +248,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
240
248
|
return (
|
|
241
249
|
<DateTimeRangePickerField
|
|
242
250
|
field={field}
|
|
243
|
-
control={control}
|
|
251
|
+
control={control as unknown as Control<FieldValues>}
|
|
244
252
|
fieldPath={fieldPath}
|
|
245
253
|
value={controllerField.value}
|
|
246
254
|
onChange={handleChange}
|
|
@@ -251,7 +259,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
251
259
|
return (
|
|
252
260
|
<FileField
|
|
253
261
|
field={field}
|
|
254
|
-
control={control}
|
|
262
|
+
control={control as unknown as Control<FieldValues>}
|
|
255
263
|
fieldPath={fieldPath}
|
|
256
264
|
value={controllerField.value}
|
|
257
265
|
onChange={handleChange}
|
|
@@ -262,7 +270,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
262
270
|
return (
|
|
263
271
|
<ObjectField
|
|
264
272
|
field={field}
|
|
265
|
-
control={control}
|
|
273
|
+
control={control as unknown as Control<FieldValues>}
|
|
266
274
|
fieldPath={fieldPath}
|
|
267
275
|
value={controllerField.value}
|
|
268
276
|
onChange={handleChange}
|
|
@@ -273,7 +281,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
273
281
|
return (
|
|
274
282
|
<ArrayField
|
|
275
283
|
field={field}
|
|
276
|
-
control={control}
|
|
284
|
+
control={control as unknown as Control<FieldValues>}
|
|
277
285
|
fieldPath={fieldPath}
|
|
278
286
|
value={controllerField.value}
|
|
279
287
|
onChange={handleChange}
|
|
@@ -284,7 +292,7 @@ export function FormBuilderField({ field, control, onChange, parentPath }: FormB
|
|
|
284
292
|
return (
|
|
285
293
|
<TextField
|
|
286
294
|
field={field}
|
|
287
|
-
control={control}
|
|
295
|
+
control={control as unknown as Control<FieldValues>}
|
|
288
296
|
fieldPath={fieldPath}
|
|
289
297
|
value={controllerField.value}
|
|
290
298
|
onChange={handleChange}
|
|
@@ -5,7 +5,7 @@ import type { FieldRenderProps } from './types'
|
|
|
5
5
|
export function AutocompleteField({ field, value, onChange, className }: FieldRenderProps) {
|
|
6
6
|
const options: AutocompleteOption[] = (field.options ?? [])
|
|
7
7
|
.filter((o): o is { label: string; value: string | number } => o.value !== null && o.value !== undefined)
|
|
8
|
-
.map(o => ({ label: o.label, value: o.value as string | number }))
|
|
8
|
+
.map(o => ({ label: o.label, value: o.value as string | number, raw: (o as unknown as AutocompleteOption).raw }))
|
|
9
9
|
|
|
10
10
|
// Shape defaultValue according to single/multiple
|
|
11
11
|
let defaultValueShaped: string | number | null | Array<string | number> | undefined = undefined
|
|
@@ -42,7 +42,7 @@ export function AutocompleteField({ field, value, onChange, className }: FieldRe
|
|
|
42
42
|
initialSelectedOptions={field.initialSelectedOptions ?? null}
|
|
43
43
|
loadSelected={field.loadSelected}
|
|
44
44
|
value={field.multiple ? ((Array.isArray(value) ? value : (value ? [value] : [])) as Array<string | number>) : ((value as string | number | null) ?? null)}
|
|
45
|
-
onChange={(val) => onChange(val)}
|
|
45
|
+
onChange={(val, option, raw) => onChange(val, option, raw)}
|
|
46
46
|
placeholder={field.placeholder}
|
|
47
47
|
searchPlaceholder={field.searchPlaceholder}
|
|
48
48
|
renderOption={field.renderOption}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import type { Control, FieldValues } from 'react-hook-form'
|
|
2
|
-
import type { FormBuilderFieldConfig } from '
|
|
2
|
+
import type { FormBuilderFieldConfig } from '../../types'
|
|
3
3
|
|
|
4
4
|
export interface FieldRenderProps {
|
|
5
|
-
field: FormBuilderFieldConfig
|
|
5
|
+
field: FormBuilderFieldConfig<any, any>
|
|
6
6
|
control: Control<FieldValues>
|
|
7
7
|
fieldPath: string
|
|
8
8
|
value: unknown
|
|
9
|
-
onChange: (value: unknown) => void
|
|
9
|
+
onChange: (value: unknown, ...extras: unknown[]) => void
|
|
10
10
|
className?: string
|
|
11
11
|
disabled?: boolean
|
|
12
12
|
errorMessage?: string
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { FieldValues, Path, Control, UseFormGetValues } from 'react-hook-form'
|
|
2
|
+
import type { SectionNode } from '../../section/types'
|
|
3
|
+
import { FormBuilderField } from './FormBuilderField'
|
|
4
|
+
import type { FormBuilderFieldConfig, FormBuilderSectionConfig } from '../types'
|
|
5
|
+
|
|
6
|
+
interface BuildSectionNodesOptions<TFieldValues extends FieldValues> {
|
|
7
|
+
sections: Array<FormBuilderSectionConfig<TFieldValues>>
|
|
8
|
+
control: Control<TFieldValues>
|
|
9
|
+
handleFieldDependencies: (
|
|
10
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
|
|
11
|
+
) => { disabled?: boolean; hidden?: boolean } | Record<string, never>
|
|
12
|
+
handleFieldChange: (
|
|
13
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>,
|
|
14
|
+
value: unknown,
|
|
15
|
+
...extras: unknown[]
|
|
16
|
+
) => void
|
|
17
|
+
onFieldChange?: (
|
|
18
|
+
name: Path<TFieldValues> | string,
|
|
19
|
+
value: unknown,
|
|
20
|
+
allValues: TFieldValues
|
|
21
|
+
) => void
|
|
22
|
+
getValues: UseFormGetValues<TFieldValues>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildSectionNodes<TFieldValues extends FieldValues>(
|
|
26
|
+
options: BuildSectionNodesOptions<TFieldValues>
|
|
27
|
+
): SectionNode[] {
|
|
28
|
+
const {
|
|
29
|
+
sections,
|
|
30
|
+
control,
|
|
31
|
+
handleFieldDependencies,
|
|
32
|
+
handleFieldChange,
|
|
33
|
+
onFieldChange,
|
|
34
|
+
getValues,
|
|
35
|
+
} = options
|
|
36
|
+
|
|
37
|
+
const buildLeavesFromFields = (
|
|
38
|
+
fields?: Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>
|
|
39
|
+
): SectionNode['children'] =>
|
|
40
|
+
(fields ?? [])
|
|
41
|
+
.map((field) => {
|
|
42
|
+
const fieldState = handleFieldDependencies(field)
|
|
43
|
+
if (field.hidden || fieldState.hidden) return null
|
|
44
|
+
|
|
45
|
+
const spanMd = Math.max(1, Math.min(12, field.gridCols ?? 1))
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
key: field.name,
|
|
49
|
+
span: { base: 1, md: spanMd },
|
|
50
|
+
className: field.wrapperClassName,
|
|
51
|
+
hidden: field.hidden,
|
|
52
|
+
content: (
|
|
53
|
+
<FormBuilderField
|
|
54
|
+
key={field.name}
|
|
55
|
+
field={{
|
|
56
|
+
...field,
|
|
57
|
+
disabled: field.disabled || fieldState.disabled,
|
|
58
|
+
}}
|
|
59
|
+
control={control}
|
|
60
|
+
onChange={(value, ...extras) => {
|
|
61
|
+
handleFieldChange(field, value, ...extras)
|
|
62
|
+
onFieldChange?.(field.name as Path<TFieldValues> | string, value, getValues())
|
|
63
|
+
}}
|
|
64
|
+
onFieldChange={onFieldChange}
|
|
65
|
+
/>
|
|
66
|
+
),
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean) as SectionNode['children']
|
|
70
|
+
|
|
71
|
+
const buildSectionNode = (
|
|
72
|
+
section: FormBuilderSectionConfig<TFieldValues>,
|
|
73
|
+
sectionIndex: number,
|
|
74
|
+
): SectionNode => {
|
|
75
|
+
const baseNode: SectionNode = {
|
|
76
|
+
id: section.id ?? `section-${sectionIndex}`,
|
|
77
|
+
title: section.title,
|
|
78
|
+
subtitle: section.description,
|
|
79
|
+
variant: section.variant ?? 'plain',
|
|
80
|
+
className: section.className,
|
|
81
|
+
layout: section.layout ?? (section.tabs && section.tabs.length > 0 ? 'tabs' : 'grid'),
|
|
82
|
+
grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
|
|
83
|
+
flex: section.flex,
|
|
84
|
+
hidden: section.hidden,
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (baseNode.layout === 'tabs' && section.tabs && section.tabs.length > 0) {
|
|
88
|
+
baseNode.defaultTabId = section.defaultTabId ?? section.tabs[0]?.id
|
|
89
|
+
baseNode.tabsListClassName = section.tabsListClassName
|
|
90
|
+
baseNode.tabsContentClassName = section.tabsContentClassName
|
|
91
|
+
baseNode.tabs = section.tabs.map((tab, _tabIdx) => {
|
|
92
|
+
const nestedNodes = tab.sections.map((subSection, subIdx) => buildSectionNode(subSection, subIdx))
|
|
93
|
+
const containerNode: SectionNode = {
|
|
94
|
+
id: `${baseNode.id}-tab-${tab.id}`,
|
|
95
|
+
variant: 'plain',
|
|
96
|
+
layout: 'grid',
|
|
97
|
+
grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
|
|
98
|
+
children: nestedNodes,
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
id: tab.id,
|
|
102
|
+
label: tab.label,
|
|
103
|
+
className: tab.className,
|
|
104
|
+
contentClassName: tab.contentClassName,
|
|
105
|
+
node: containerNode,
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
return baseNode
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
baseNode.children = buildLeavesFromFields(section.fields)
|
|
112
|
+
return baseNode
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return sections.map((section, sectionIndex) => buildSectionNode(section, sectionIndex))
|
|
116
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import type React from 'react'
|
|
2
|
+
import type {
|
|
3
|
+
Control,
|
|
4
|
+
DeepPartial,
|
|
5
|
+
FieldValues,
|
|
6
|
+
Path,
|
|
7
|
+
UseFormGetValues,
|
|
8
|
+
UseFormReturn,
|
|
9
|
+
UseFormSetValue,
|
|
10
|
+
DefaultValues,
|
|
11
|
+
} from 'react-hook-form'
|
|
12
|
+
import type { z } from 'zod'
|
|
13
|
+
import type { Accept } from 'react-dropzone'
|
|
14
|
+
import type { SectionFlexOptions, SectionGridOptions, SectionLayout } from '../section/types'
|
|
15
|
+
import type { AutocompleteFetcher, AutocompleteOption } from '../../components/autocomplete/types'
|
|
16
|
+
import type { FileRecord, FileUploaderLayout } from '../../components/fileuploader/types'
|
|
17
|
+
|
|
18
|
+
export type FieldType =
|
|
19
|
+
| 'text'
|
|
20
|
+
| 'email'
|
|
21
|
+
| 'password'
|
|
22
|
+
| 'number'
|
|
23
|
+
| 'textarea'
|
|
24
|
+
| 'select'
|
|
25
|
+
| 'autocomplete'
|
|
26
|
+
| 'checkbox'
|
|
27
|
+
| 'switch'
|
|
28
|
+
| 'radio'
|
|
29
|
+
| 'date'
|
|
30
|
+
| 'date_picker'
|
|
31
|
+
| 'date_range'
|
|
32
|
+
| 'month'
|
|
33
|
+
| 'month_range'
|
|
34
|
+
| 'time'
|
|
35
|
+
| 'time_range'
|
|
36
|
+
| 'date_time'
|
|
37
|
+
| 'date_time_range'
|
|
38
|
+
| 'file'
|
|
39
|
+
| 'object'
|
|
40
|
+
| 'array'
|
|
41
|
+
|
|
42
|
+
export interface Dependency<TFieldValues extends FieldValues> {
|
|
43
|
+
field: Path<TFieldValues>
|
|
44
|
+
condition: (value: unknown) => boolean
|
|
45
|
+
action: 'show' | 'hide' | 'enable' | 'disable' | 'setValue'
|
|
46
|
+
value?: unknown
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface FormBuilderFieldConfig<
|
|
50
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
51
|
+
TName extends Path<TFieldValues> | string = Path<TFieldValues>
|
|
52
|
+
> {
|
|
53
|
+
id?: string
|
|
54
|
+
name: TName
|
|
55
|
+
label: string
|
|
56
|
+
type: FieldType
|
|
57
|
+
placeholder?: string
|
|
58
|
+
description?: string
|
|
59
|
+
required?: boolean
|
|
60
|
+
disabled?: boolean
|
|
61
|
+
options?: { label: string; value: string | number | null }[]
|
|
62
|
+
autocompleteMode?: 'client' | 'server'
|
|
63
|
+
fetcher?: AutocompleteFetcher
|
|
64
|
+
pageSize?: number
|
|
65
|
+
searchPlaceholder?: string
|
|
66
|
+
renderOption?: (option: AutocompleteOption, selected: boolean) => React.ReactNode
|
|
67
|
+
multiple?: boolean
|
|
68
|
+
allowCustomValue?: boolean
|
|
69
|
+
chipVariant?: 'default' | 'secondary' | 'destructive' | 'outline'
|
|
70
|
+
chipClassName?: string
|
|
71
|
+
clearable?: boolean
|
|
72
|
+
initialSelectedOptions?: AutocompleteOption | AutocompleteOption[] | null
|
|
73
|
+
loadSelected?: (values: Array<string | number>) => Promise<AutocompleteOption[]>
|
|
74
|
+
validation?:
|
|
75
|
+
| z.ZodType<unknown>
|
|
76
|
+
| {
|
|
77
|
+
pattern?: { value: RegExp; message: string }
|
|
78
|
+
min?: { value: number; message: string }
|
|
79
|
+
max?: { value: number; message: string }
|
|
80
|
+
minLength?: { value: number; message: string }
|
|
81
|
+
maxLength?: { value: number; message: string }
|
|
82
|
+
minItems?: { value: number; message: string }
|
|
83
|
+
maxItems?: { value: number; message: string }
|
|
84
|
+
}
|
|
85
|
+
// Accept any for defaultValue to support relative string names for nested fields
|
|
86
|
+
defaultValue?: unknown
|
|
87
|
+
// For nested object/array fields, use relative names (eg. 'uomName')
|
|
88
|
+
fields?: Array<FormBuilderFieldConfig<TFieldValues, string>>
|
|
89
|
+
dependencies?: Array<Dependency<TFieldValues>>
|
|
90
|
+
onChange?: (
|
|
91
|
+
value: unknown,
|
|
92
|
+
extras: unknown,
|
|
93
|
+
setValue: UseFormSetValue<TFieldValues>,
|
|
94
|
+
getValues: UseFormGetValues<TFieldValues>
|
|
95
|
+
) => void
|
|
96
|
+
className?: string
|
|
97
|
+
gridCols?: number
|
|
98
|
+
rows?: number
|
|
99
|
+
itemType?: string
|
|
100
|
+
arrayLayout?: 'card' | 'table' | 'custom'
|
|
101
|
+
arrayRender?: (params: {
|
|
102
|
+
field: FormBuilderFieldConfig<TFieldValues>
|
|
103
|
+
control: Control<TFieldValues>
|
|
104
|
+
fieldPath: string
|
|
105
|
+
value: unknown
|
|
106
|
+
onChange: (value: unknown) => void
|
|
107
|
+
addItem: () => void
|
|
108
|
+
removeItem: (index: number) => void
|
|
109
|
+
disabled?: boolean
|
|
110
|
+
rows?: { id: string }[]
|
|
111
|
+
}) => React.ReactNode
|
|
112
|
+
arrayColors?: {
|
|
113
|
+
headerBgClass?: string
|
|
114
|
+
headerTextClass?: string
|
|
115
|
+
rowAltBgClass?: string
|
|
116
|
+
}
|
|
117
|
+
conditional?: {
|
|
118
|
+
field: Path<TFieldValues>
|
|
119
|
+
value: unknown
|
|
120
|
+
}
|
|
121
|
+
hidden?: boolean
|
|
122
|
+
labelPlacement?: 'stacked' | 'inline' | 'hidden'
|
|
123
|
+
wrapperClassName?: string
|
|
124
|
+
minDate?: Date
|
|
125
|
+
maxDate?: Date
|
|
126
|
+
disabledDates?: Array<Date | { from: Date; to: Date }>
|
|
127
|
+
numberOfMonths?: number
|
|
128
|
+
popoverSide?: 'top' | 'right' | 'bottom' | 'left'
|
|
129
|
+
showFooter?: boolean
|
|
130
|
+
cancelLabel?: string
|
|
131
|
+
applyLabel?: string
|
|
132
|
+
timePrecision?: 'hour' | 'minute' | 'second'
|
|
133
|
+
hourCycle?: 12 | 24
|
|
134
|
+
minuteStep?: number
|
|
135
|
+
secondStep?: number
|
|
136
|
+
fileMultiple?: boolean
|
|
137
|
+
fileMaxFiles?: number
|
|
138
|
+
fileAccept?: Accept
|
|
139
|
+
fileLayout?: FileUploaderLayout
|
|
140
|
+
fileWithDownload?: boolean
|
|
141
|
+
fileUploader?: (file: File, onProgress: (pct: number) => void) => Promise<Partial<FileRecord>>
|
|
142
|
+
fileOnUploadSuccess?: (file: FileRecord) => void
|
|
143
|
+
fileOnUploadError?: (file: FileRecord, error: unknown) => void
|
|
144
|
+
fileOnRemove?: (file: FileRecord) => void | Promise<void>
|
|
145
|
+
fileOnRetry?: (file: FileRecord) => void
|
|
146
|
+
fileOnRetryAll?: (files: FileRecord[]) => void
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface FormBuilderSectionConfig<TFieldValues extends FieldValues = FieldValues> {
|
|
150
|
+
id?: string
|
|
151
|
+
title?: string
|
|
152
|
+
description?: string
|
|
153
|
+
fields?: Array<FormBuilderFieldConfig<TFieldValues>>
|
|
154
|
+
variant?: 'card' | 'separator' | 'plain'
|
|
155
|
+
className?: string
|
|
156
|
+
collapsible?: boolean
|
|
157
|
+
defaultCollapsed?: boolean
|
|
158
|
+
layout?: SectionLayout
|
|
159
|
+
grid?: SectionGridOptions
|
|
160
|
+
flex?: SectionFlexOptions
|
|
161
|
+
hidden?: boolean
|
|
162
|
+
tabs?: Array<{
|
|
163
|
+
id: string
|
|
164
|
+
label: React.ReactNode
|
|
165
|
+
sections: Array<FormBuilderSectionConfig<TFieldValues>>
|
|
166
|
+
className?: string
|
|
167
|
+
contentClassName?: string
|
|
168
|
+
}>
|
|
169
|
+
defaultTabId?: string
|
|
170
|
+
tabsListClassName?: string
|
|
171
|
+
tabsContentClassName?: string
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export interface FormBuilderProps<TFieldValues extends FieldValues = FieldValues> {
|
|
175
|
+
sections: Array<FormBuilderSectionConfig<TFieldValues>>
|
|
176
|
+
schema?: z.ZodType<TFieldValues>
|
|
177
|
+
defaultValues?: DeepPartial<TFieldValues> | DefaultValues<TFieldValues> | null
|
|
178
|
+
onSubmit: (data: TFieldValues) => void | Promise<void>
|
|
179
|
+
onCancel?: () => void
|
|
180
|
+
onReset?: () => void
|
|
181
|
+
onFieldChange?: (
|
|
182
|
+
name: Path<TFieldValues> | string,
|
|
183
|
+
value: unknown,
|
|
184
|
+
allValues: TFieldValues
|
|
185
|
+
) => void
|
|
186
|
+
submitLabel?: string
|
|
187
|
+
cancelLabel?: string
|
|
188
|
+
resetLabel?: string
|
|
189
|
+
isSubmitting?: boolean
|
|
190
|
+
className?: string
|
|
191
|
+
formClassName?: string
|
|
192
|
+
actionsClassName?: string
|
|
193
|
+
showActions?: boolean
|
|
194
|
+
customActions?: React.ReactNode
|
|
195
|
+
showActionsSeparator?: boolean
|
|
196
|
+
form?: UseFormReturn<TFieldValues>
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Re-export for external consumers that build custom section nodes
|
|
200
|
+
export type { SectionNode } from '../section/types'
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormBuilderSectionConfig } from '../
|
|
1
|
+
import type { FormBuilderSectionConfig } from '../types';
|
|
2
2
|
import { createField } from './field-factories';
|
|
3
3
|
import { createSection } from './section-factories';
|
|
4
4
|
import { commonValidations } from './validations';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FormBuilderFieldConfig } from '../
|
|
1
|
+
import type { FormBuilderFieldConfig } from '../types';
|
|
2
2
|
import { commonValidations } from './validations';
|
|
3
3
|
|
|
4
4
|
// Field factory functions
|
|
@@ -7,7 +7,7 @@ export const createField = {
|
|
|
7
7
|
name: string,
|
|
8
8
|
label: string,
|
|
9
9
|
options: Partial<FormBuilderFieldConfig> = {},
|
|
10
|
-
): FormBuilderFieldConfig => ({
|
|
10
|
+
): FormBuilderFieldConfig<any> => ({
|
|
11
11
|
name,
|
|
12
12
|
label,
|
|
13
13
|
type: 'text',
|
|
@@ -147,9 +147,9 @@ export const createField = {
|
|
|
147
147
|
object: (
|
|
148
148
|
name: string,
|
|
149
149
|
label: string,
|
|
150
|
-
fields: FormBuilderFieldConfig[],
|
|
151
|
-
options: Partial<FormBuilderFieldConfig
|
|
152
|
-
): FormBuilderFieldConfig => ({
|
|
150
|
+
fields: FormBuilderFieldConfig<any>[],
|
|
151
|
+
options: Partial<FormBuilderFieldConfig<any>> = {},
|
|
152
|
+
): FormBuilderFieldConfig<any> => ({
|
|
153
153
|
name,
|
|
154
154
|
label,
|
|
155
155
|
type: 'object',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { FormBuilderFieldConfig, FormBuilderSectionConfig } from '../
|
|
1
|
+
import type { FormBuilderFieldConfig, FormBuilderSectionConfig } from '../types';
|
|
2
2
|
|
|
3
3
|
// Section factory functions
|
|
4
4
|
export const createSection = {
|
|
5
5
|
card: (
|
|
6
6
|
title: string,
|
|
7
|
-
fields: FormBuilderFieldConfig[],
|
|
8
|
-
options: Partial<FormBuilderSectionConfig
|
|
9
|
-
): FormBuilderSectionConfig => ({
|
|
7
|
+
fields: FormBuilderFieldConfig<any>[],
|
|
8
|
+
options: Partial<FormBuilderSectionConfig<any>> = {},
|
|
9
|
+
): FormBuilderSectionConfig<any> => ({
|
|
10
10
|
title,
|
|
11
11
|
fields,
|
|
12
12
|
variant: 'card',
|
|
@@ -15,9 +15,9 @@ export const createSection = {
|
|
|
15
15
|
|
|
16
16
|
separator: (
|
|
17
17
|
title: string,
|
|
18
|
-
fields: FormBuilderFieldConfig[],
|
|
19
|
-
options: Partial<FormBuilderSectionConfig
|
|
20
|
-
): FormBuilderSectionConfig => ({
|
|
18
|
+
fields: FormBuilderFieldConfig<any>[],
|
|
19
|
+
options: Partial<FormBuilderSectionConfig<any>> = {},
|
|
20
|
+
): FormBuilderSectionConfig<any> => ({
|
|
21
21
|
title,
|
|
22
22
|
fields,
|
|
23
23
|
variant: 'separator',
|
|
@@ -25,9 +25,9 @@ export const createSection = {
|
|
|
25
25
|
}),
|
|
26
26
|
|
|
27
27
|
plain: (
|
|
28
|
-
fields: FormBuilderFieldConfig[],
|
|
29
|
-
options: Partial<FormBuilderSectionConfig
|
|
30
|
-
): FormBuilderSectionConfig => ({
|
|
28
|
+
fields: FormBuilderFieldConfig<any>[],
|
|
29
|
+
options: Partial<FormBuilderSectionConfig<any>> = {},
|
|
30
|
+
): FormBuilderSectionConfig<any> => ({
|
|
31
31
|
fields,
|
|
32
32
|
variant: 'plain',
|
|
33
33
|
...options,
|