@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,221 +1,22 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
1
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
3
|
-
import { useForm, useWatch, type
|
|
2
|
+
import { useForm, useWatch, type FieldValues, type Path } from 'react-hook-form';
|
|
4
3
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
5
4
|
import { z } from 'zod';
|
|
6
5
|
import { cn } from '../../../../shadcn/lib/utils';
|
|
7
6
|
import { Button } from '../../../../shadcn/ui/button';
|
|
8
|
-
import { FormBuilderField } from './FormBuilderField';
|
|
9
7
|
import SectionBuilder from '../../section/SectionBuilder';
|
|
8
|
+
import { buildSectionNodes } from './sectionNodes';
|
|
9
|
+
import { FormBuilderContext, type FormBuilderContextValue } from './FormBuilderContext';
|
|
10
10
|
import type {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
} from '../../section/types';
|
|
16
|
-
import type {
|
|
17
|
-
AutocompleteFetcher,
|
|
18
|
-
AutocompleteOption,
|
|
19
|
-
} from '../../../components/autocomplete/types';
|
|
20
|
-
import type { Accept } from 'react-dropzone';
|
|
21
|
-
import type {
|
|
22
|
-
FileRecord,
|
|
23
|
-
FileUploaderLayout,
|
|
24
|
-
} from '../../../components/fileuploader/types';
|
|
25
|
-
|
|
26
|
-
export interface FormBuilderFieldConfig {
|
|
27
|
-
id?: string; // Optional ID for test fixtures
|
|
28
|
-
name: string;
|
|
29
|
-
label: string;
|
|
30
|
-
type:
|
|
31
|
-
| 'text'
|
|
32
|
-
| 'email'
|
|
33
|
-
| 'password'
|
|
34
|
-
| 'number'
|
|
35
|
-
| 'textarea'
|
|
36
|
-
| 'select'
|
|
37
|
-
| 'autocomplete'
|
|
38
|
-
| 'checkbox'
|
|
39
|
-
| 'switch'
|
|
40
|
-
| 'radio'
|
|
41
|
-
| 'date' // native input date
|
|
42
|
-
| 'date_picker' // UI DatePicker
|
|
43
|
-
| 'date_range' // UI DateRangePicker
|
|
44
|
-
| 'month' // UI MonthPicker (single month Date)
|
|
45
|
-
| 'month_range' // UI MonthRangePicker { start: Date, end: Date }
|
|
46
|
-
| 'time' // UI TimePicker (Date with time part)
|
|
47
|
-
| 'time_range' // UI TimeRangePicker { from: Date, to: Date }
|
|
48
|
-
| 'date_time' // UI DateTimePicker (Date with date+time)
|
|
49
|
-
| 'date_time_range' // UI DateTimeRangePicker { from: Date, to: Date }
|
|
50
|
-
| 'file'
|
|
51
|
-
| 'object'
|
|
52
|
-
| 'array';
|
|
53
|
-
placeholder?: string;
|
|
54
|
-
description?: string;
|
|
55
|
-
required?: boolean;
|
|
56
|
-
disabled?: boolean;
|
|
57
|
-
options?: { label: string; value: string | number | null }[];
|
|
58
|
-
// Autocomplete specific (client/server)
|
|
59
|
-
autocompleteMode?: 'client' | 'server';
|
|
60
|
-
fetcher?: AutocompleteFetcher;
|
|
61
|
-
pageSize?: number;
|
|
62
|
-
searchPlaceholder?: string;
|
|
63
|
-
renderOption?: (
|
|
64
|
-
option: AutocompleteOption,
|
|
65
|
-
selected: boolean
|
|
66
|
-
) => React.ReactNode;
|
|
67
|
-
// New autocomplete features
|
|
68
|
-
multiple?: boolean;
|
|
69
|
-
allowCustomValue?: boolean;
|
|
70
|
-
chipVariant?: 'default' | 'secondary' | 'destructive' | 'outline';
|
|
71
|
-
chipClassName?: string;
|
|
72
|
-
clearable?: boolean;
|
|
73
|
-
initialSelectedOptions?: AutocompleteOption | AutocompleteOption[] | null;
|
|
74
|
-
loadSelected?: (values: Array<string | number>) => Promise<AutocompleteOption[]>;
|
|
75
|
-
validation?:
|
|
76
|
-
| z.ZodType<unknown>
|
|
77
|
-
| {
|
|
78
|
-
pattern?: { value: RegExp; message: string };
|
|
79
|
-
min?: { value: number; message: string };
|
|
80
|
-
max?: { value: number; message: string };
|
|
81
|
-
minLength?: { value: number; message: string };
|
|
82
|
-
maxLength?: { value: number; message: string };
|
|
83
|
-
// For array-like fields (e.g., file uploader)
|
|
84
|
-
minItems?: { value: number; message: string };
|
|
85
|
-
maxItems?: { value: number; message: string };
|
|
86
|
-
};
|
|
87
|
-
defaultValue?: unknown;
|
|
88
|
-
fields?: FormBuilderFieldConfig[]; // For nested object/array fields
|
|
89
|
-
dependencies?: {
|
|
90
|
-
field: string;
|
|
91
|
-
condition: (value: unknown) => boolean;
|
|
92
|
-
action: 'show' | 'hide' | 'enable' | 'disable' | 'setValue';
|
|
93
|
-
value?: unknown;
|
|
94
|
-
}[];
|
|
95
|
-
onChange?: (
|
|
96
|
-
value: unknown,
|
|
97
|
-
setValue: (field: string, value: unknown) => void,
|
|
98
|
-
getValues: () => Record<string, unknown>
|
|
99
|
-
) => void;
|
|
100
|
-
className?: string;
|
|
101
|
-
gridCols?: number;
|
|
102
|
-
rows?: number; // For textarea fields
|
|
103
|
-
itemType?: string; // For array fields
|
|
104
|
-
// Array field layout: default 'card'
|
|
105
|
-
arrayLayout?: 'card' | 'table' | 'custom';
|
|
106
|
-
// Custom renderer for array fields when arrayLayout === 'custom'
|
|
107
|
-
arrayRender?: (params: {
|
|
108
|
-
field: FormBuilderFieldConfig;
|
|
109
|
-
control: Control<FieldValues>;
|
|
110
|
-
fieldPath: string;
|
|
111
|
-
value: unknown;
|
|
112
|
-
onChange: (value: unknown) => void;
|
|
113
|
-
addItem: () => void;
|
|
114
|
-
removeItem: (index: number) => void;
|
|
115
|
-
disabled?: boolean;
|
|
116
|
-
rows?: { id: string }[]; // useFieldArray rows for stable rendering
|
|
117
|
-
}) => React.ReactNode;
|
|
118
|
-
// Optional styling for array layouts (used mainly for 'table')
|
|
119
|
-
arrayColors?: {
|
|
120
|
-
headerBgClass?: string; // e.g. 'bg-teal-700'
|
|
121
|
-
headerTextClass?: string; // e.g. 'text-white'
|
|
122
|
-
rowAltBgClass?: string; // e.g. 'bg-teal-50'
|
|
123
|
-
};
|
|
124
|
-
conditional?: {
|
|
125
|
-
field: string;
|
|
126
|
-
value: unknown;
|
|
127
|
-
}; // For conditional field visibility
|
|
128
|
-
hidden?: boolean; // Declarative hide
|
|
129
|
-
// Label placement control across inputs
|
|
130
|
-
labelPlacement?: 'stacked' | 'inline' | 'hidden';
|
|
131
|
-
// Wrapper container className (applies to the field wrapper, not the input)
|
|
132
|
-
wrapperClassName?: string;
|
|
133
|
-
// Picker-specific optional props (passed through to components when applicable)
|
|
134
|
-
minDate?: Date;
|
|
135
|
-
maxDate?: Date;
|
|
136
|
-
disabledDates?: Array<Date | { from: Date; to: Date }>;
|
|
137
|
-
numberOfMonths?: number;
|
|
138
|
-
popoverSide?: 'top' | 'right' | 'bottom' | 'left';
|
|
139
|
-
showFooter?: boolean;
|
|
140
|
-
cancelLabel?: string;
|
|
141
|
-
applyLabel?: string;
|
|
142
|
-
// Time picker specific
|
|
143
|
-
timePrecision?: 'hour' | 'minute' | 'second';
|
|
144
|
-
hourCycle?: 12 | 24;
|
|
145
|
-
minuteStep?: number;
|
|
146
|
-
secondStep?: number;
|
|
147
|
-
// File uploader specific options
|
|
148
|
-
fileMultiple?: boolean;
|
|
149
|
-
fileMaxFiles?: number;
|
|
150
|
-
fileAccept?: Accept;
|
|
151
|
-
fileLayout?: FileUploaderLayout;
|
|
152
|
-
fileWithDownload?: boolean;
|
|
153
|
-
fileUploader?: (
|
|
154
|
-
file: File,
|
|
155
|
-
onProgress: (pct: number) => void,
|
|
156
|
-
) => Promise<Partial<FileRecord>>;
|
|
157
|
-
fileOnUploadSuccess?: (file: FileRecord) => void;
|
|
158
|
-
fileOnUploadError?: (file: FileRecord, error: unknown) => void;
|
|
159
|
-
fileOnRemove?: (file: FileRecord) => void | Promise<void>;
|
|
160
|
-
fileOnRetry?: (file: FileRecord) => void;
|
|
161
|
-
fileOnRetryAll?: (files: FileRecord[]) => void;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export interface FormBuilderSectionConfig {
|
|
165
|
-
id?: string;
|
|
166
|
-
title?: string;
|
|
167
|
-
description?: string;
|
|
168
|
-
fields?: FormBuilderFieldConfig[];
|
|
169
|
-
variant?: 'card' | 'separator' | 'plain';
|
|
170
|
-
className?: string;
|
|
171
|
-
collapsible?: boolean;
|
|
172
|
-
defaultCollapsed?: boolean;
|
|
173
|
-
layout?: SectionLayout;
|
|
174
|
-
grid?: SectionGridOptions;
|
|
175
|
-
flex?: SectionFlexOptions;
|
|
176
|
-
hidden?: boolean; // Declarative hide
|
|
177
|
-
// Tabs layout support: when layout === 'tabs', provide tabs instead of direct fields
|
|
178
|
-
tabs?: Array<{
|
|
179
|
-
id: string;
|
|
180
|
-
label: React.ReactNode;
|
|
181
|
-
sections: FormBuilderSectionConfig[];
|
|
182
|
-
className?: string;
|
|
183
|
-
contentClassName?: string;
|
|
184
|
-
}>;
|
|
185
|
-
defaultTabId?: string;
|
|
186
|
-
tabsListClassName?: string;
|
|
187
|
-
tabsContentClassName?: string;
|
|
188
|
-
}
|
|
11
|
+
FormBuilderProps,
|
|
12
|
+
FormBuilderFieldConfig,
|
|
13
|
+
FormBuilderSectionConfig,
|
|
14
|
+
} from '../types';
|
|
189
15
|
|
|
190
|
-
export
|
|
191
|
-
sections: FormBuilderSectionConfig[];
|
|
192
|
-
schema?: z.ZodType<unknown>;
|
|
193
|
-
defaultValues?: Record<string, unknown> | null;
|
|
194
|
-
onSubmit: (data: unknown) => void | Promise<void>;
|
|
195
|
-
onCancel?: () => void;
|
|
196
|
-
onReset?: () => void;
|
|
197
|
-
onFieldChange?: (
|
|
198
|
-
name: string,
|
|
199
|
-
value: unknown,
|
|
200
|
-
allValues: Record<string, unknown>
|
|
201
|
-
) => void;
|
|
202
|
-
submitLabel?: string;
|
|
203
|
-
cancelLabel?: string;
|
|
204
|
-
resetLabel?: string;
|
|
205
|
-
isSubmitting?: boolean;
|
|
206
|
-
className?: string;
|
|
207
|
-
formClassName?: string;
|
|
208
|
-
actionsClassName?: string;
|
|
209
|
-
showActions?: boolean;
|
|
210
|
-
customActions?: React.ReactNode;
|
|
211
|
-
// UI: show a separator line above action buttons
|
|
212
|
-
showActionsSeparator?: boolean;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function FormBuilder({
|
|
16
|
+
export function FormBuilder<TFieldValues extends FieldValues = FieldValues>({
|
|
216
17
|
sections,
|
|
217
18
|
schema,
|
|
218
|
-
defaultValues
|
|
19
|
+
defaultValues,
|
|
219
20
|
onSubmit,
|
|
220
21
|
onCancel,
|
|
221
22
|
onReset,
|
|
@@ -230,13 +31,14 @@ export function FormBuilder({
|
|
|
230
31
|
showActions = true,
|
|
231
32
|
customActions,
|
|
232
33
|
showActionsSeparator = true,
|
|
233
|
-
|
|
34
|
+
form,
|
|
35
|
+
}: FormBuilderProps<TFieldValues>) {
|
|
234
36
|
// Generate schema from field configs if not provided
|
|
235
37
|
const generatedSchema = useMemo(() => {
|
|
236
38
|
if (schema) return schema;
|
|
237
39
|
|
|
238
40
|
const generateFieldSchema = (
|
|
239
|
-
field: FormBuilderFieldConfig
|
|
41
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
|
|
240
42
|
): z.ZodType<unknown> => {
|
|
241
43
|
if (field.validation && field.validation instanceof z.ZodType) {
|
|
242
44
|
return field.validation;
|
|
@@ -430,7 +232,7 @@ export function FormBuilder({
|
|
|
430
232
|
case 'object':
|
|
431
233
|
if (field.fields) {
|
|
432
234
|
const objectSchema: Record<string, z.ZodType<unknown>> = {};
|
|
433
|
-
for (const subField of field.fields) {
|
|
235
|
+
for (const subField of field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>) {
|
|
434
236
|
objectSchema[subField.name] = generateFieldSchema(subField);
|
|
435
237
|
}
|
|
436
238
|
fieldSchema = z.object(objectSchema);
|
|
@@ -442,9 +244,9 @@ export function FormBuilder({
|
|
|
442
244
|
if (field.fields && field.fields.length > 0) {
|
|
443
245
|
const arrayItemSchema =
|
|
444
246
|
field.fields.length === 1
|
|
445
|
-
? generateFieldSchema(field.fields[0])
|
|
247
|
+
? generateFieldSchema(field.fields[0] as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>)
|
|
446
248
|
: z.object(
|
|
447
|
-
field.fields.reduce((acc, subField) => {
|
|
249
|
+
(field.fields as Array<FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>>).reduce((acc, subField) => {
|
|
448
250
|
acc[subField.name] = generateFieldSchema(subField);
|
|
449
251
|
return acc;
|
|
450
252
|
}, {} as Record<string, z.ZodType<unknown>>)
|
|
@@ -463,7 +265,7 @@ export function FormBuilder({
|
|
|
463
265
|
|
|
464
266
|
const schemaObject: Record<string, z.ZodType<unknown>> = {};
|
|
465
267
|
|
|
466
|
-
const forEachField = (secs: FormBuilderSectionConfig[]) => {
|
|
268
|
+
const forEachField = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
|
|
467
269
|
for (const section of secs) {
|
|
468
270
|
// Traverse tabs if present
|
|
469
271
|
if (section.tabs && section.tabs.length > 0) {
|
|
@@ -472,21 +274,21 @@ export function FormBuilder({
|
|
|
472
274
|
}
|
|
473
275
|
}
|
|
474
276
|
for (const field of (section.fields ?? [])) {
|
|
475
|
-
schemaObject[field.name] = generateFieldSchema(field);
|
|
277
|
+
schemaObject[field.name] = generateFieldSchema(field as FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>);
|
|
476
278
|
}
|
|
477
279
|
}
|
|
478
280
|
};
|
|
479
281
|
|
|
480
282
|
forEachField(sections);
|
|
481
283
|
|
|
482
|
-
return z.object(schemaObject)
|
|
284
|
+
return z.object(schemaObject) as unknown as z.ZodType<TFieldValues>;
|
|
483
285
|
}, [sections, schema]);
|
|
484
286
|
|
|
485
287
|
// Generate default values from field configs
|
|
486
288
|
const generatedDefaultValues = useMemo(() => {
|
|
487
|
-
const values: Record<string, unknown> = { ...defaultValues };
|
|
289
|
+
const values: Record<string, unknown> = { ...((defaultValues ?? {}) as Record<string, unknown>) };
|
|
488
290
|
|
|
489
|
-
const processFields = (fields: FormBuilderFieldConfig[]) => {
|
|
291
|
+
const processFields = (fields: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>[]) => {
|
|
490
292
|
for (const field of fields) {
|
|
491
293
|
if (
|
|
492
294
|
values[field.name] === undefined &&
|
|
@@ -518,7 +320,7 @@ export function FormBuilder({
|
|
|
518
320
|
}
|
|
519
321
|
};
|
|
520
322
|
|
|
521
|
-
const forEachSection = (secs: FormBuilderSectionConfig[]) => {
|
|
323
|
+
const forEachSection = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
|
|
522
324
|
for (const section of secs) {
|
|
523
325
|
if (section.tabs && section.tabs.length > 0) {
|
|
524
326
|
for (const tab of section.tabs) {
|
|
@@ -534,19 +336,21 @@ export function FormBuilder({
|
|
|
534
336
|
return values;
|
|
535
337
|
}, [sections, defaultValues]);
|
|
536
338
|
|
|
537
|
-
const
|
|
339
|
+
const internalForm = useForm<TFieldValues>({
|
|
538
340
|
// Dynamic schema shape: cast to any to satisfy resolver generics
|
|
539
341
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
540
|
-
resolver: zodResolver(generatedSchema as any) as unknown as import('react-hook-form').Resolver<
|
|
541
|
-
defaultValues: generatedDefaultValues as
|
|
342
|
+
resolver: zodResolver(generatedSchema as any) as unknown as import('react-hook-form').Resolver<TFieldValues, any, TFieldValues>,
|
|
343
|
+
defaultValues: generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>,
|
|
542
344
|
});
|
|
543
345
|
|
|
544
|
-
const
|
|
346
|
+
const activeForm = form ?? internalForm;
|
|
347
|
+
|
|
348
|
+
const { control, handleSubmit, reset, setValue, getValues } = activeForm;
|
|
545
349
|
|
|
546
350
|
// Determine dependency fields to watch
|
|
547
351
|
const dependencyFields = useMemo(() => {
|
|
548
|
-
const set = new Set<
|
|
549
|
-
const forEachField = (secs: FormBuilderSectionConfig[]) => {
|
|
352
|
+
const set = new Set<Path<TFieldValues>>();
|
|
353
|
+
const forEachField = (secs: FormBuilderSectionConfig<TFieldValues>[]) => {
|
|
550
354
|
for (const section of secs) {
|
|
551
355
|
if (section.tabs && section.tabs.length > 0) {
|
|
552
356
|
for (const tab of section.tabs) {
|
|
@@ -586,7 +390,7 @@ export function FormBuilder({
|
|
|
586
390
|
);
|
|
587
391
|
|
|
588
392
|
const handleFieldDependencies = useCallback(
|
|
589
|
-
(field: FormBuilderFieldConfig) => {
|
|
393
|
+
(field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>) => {
|
|
590
394
|
if (!hasDependencies || !field.dependencies) return {};
|
|
591
395
|
|
|
592
396
|
const result: { disabled?: boolean; hidden?: boolean } = {};
|
|
@@ -610,11 +414,11 @@ export function FormBuilder({
|
|
|
610
414
|
break;
|
|
611
415
|
case 'setValue':
|
|
612
416
|
if (conditionMet && dep.value !== undefined) {
|
|
613
|
-
const currentValue = getValues(field.name);
|
|
417
|
+
const currentValue = getValues(field.name as unknown as Path<TFieldValues>);
|
|
614
418
|
if (currentValue !== dep.value) {
|
|
615
419
|
// Defer the update to an effect to prevent state changes during render
|
|
616
420
|
pendingValueUpdatesRef.current.push({
|
|
617
|
-
name: field.name,
|
|
421
|
+
name: field.name as unknown as string,
|
|
618
422
|
value: dep.value,
|
|
619
423
|
});
|
|
620
424
|
}
|
|
@@ -638,9 +442,10 @@ export function FormBuilder({
|
|
|
638
442
|
}
|
|
639
443
|
pendingValueUpdatesRef.current = [];
|
|
640
444
|
for (const [name, value] of updatesMap) {
|
|
641
|
-
const
|
|
445
|
+
const pathName = name as unknown as Path<TFieldValues>;
|
|
446
|
+
const current = getValues(pathName);
|
|
642
447
|
if (current !== value) {
|
|
643
|
-
setValue(
|
|
448
|
+
setValue(pathName, value as unknown as never, {
|
|
644
449
|
shouldDirty: false,
|
|
645
450
|
shouldTouch: false,
|
|
646
451
|
shouldValidate: false,
|
|
@@ -651,16 +456,20 @@ export function FormBuilder({
|
|
|
651
456
|
|
|
652
457
|
// Handle field change with custom onChange
|
|
653
458
|
const handleFieldChange = useCallback(
|
|
654
|
-
(
|
|
459
|
+
(
|
|
460
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>,
|
|
461
|
+
value: unknown,
|
|
462
|
+
...extras: unknown[]
|
|
463
|
+
) => {
|
|
655
464
|
if (field.onChange) {
|
|
656
|
-
field.onChange(value, setValue, getValues);
|
|
465
|
+
field.onChange(value, extras, setValue, getValues);
|
|
657
466
|
}
|
|
658
467
|
},
|
|
659
468
|
[setValue, getValues]
|
|
660
469
|
);
|
|
661
470
|
|
|
662
471
|
const handleFormSubmit = useCallback(
|
|
663
|
-
async (data:
|
|
472
|
+
async (data: TFieldValues) => {
|
|
664
473
|
try {
|
|
665
474
|
await onSubmit(data);
|
|
666
475
|
} catch (error) {
|
|
@@ -671,154 +480,89 @@ export function FormBuilder({
|
|
|
671
480
|
);
|
|
672
481
|
|
|
673
482
|
const handleReset = useCallback(() => {
|
|
674
|
-
reset(generatedDefaultValues);
|
|
483
|
+
reset(generatedDefaultValues as unknown as import('react-hook-form').DefaultValues<TFieldValues>);
|
|
675
484
|
onReset?.();
|
|
676
485
|
}, [reset, generatedDefaultValues, onReset]);
|
|
677
486
|
|
|
678
487
|
// Build SectionBuilder nodes from form sections/fields
|
|
679
|
-
const sectionNodes
|
|
680
|
-
|
|
681
|
-
(
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
className: field.wrapperClassName,
|
|
692
|
-
hidden: field.hidden,
|
|
693
|
-
content: (
|
|
694
|
-
<FormBuilderField
|
|
695
|
-
key={field.name}
|
|
696
|
-
field={{
|
|
697
|
-
...field,
|
|
698
|
-
disabled: field.disabled || fieldState.disabled,
|
|
699
|
-
}}
|
|
700
|
-
control={control}
|
|
701
|
-
onChange={(value) => {
|
|
702
|
-
handleFieldChange(field, value);
|
|
703
|
-
onFieldChange?.(field.name, value, getValues());
|
|
704
|
-
}}
|
|
705
|
-
onFieldChange={onFieldChange}
|
|
706
|
-
/>
|
|
707
|
-
),
|
|
708
|
-
};
|
|
709
|
-
})
|
|
710
|
-
.filter(Boolean) as SectionNode['children'];
|
|
711
|
-
|
|
712
|
-
const buildSectionNode = (
|
|
713
|
-
section: FormBuilderSectionConfig,
|
|
714
|
-
sectionIndex: number,
|
|
715
|
-
): SectionNode => {
|
|
716
|
-
const baseNode: SectionNode = {
|
|
717
|
-
id: section.id ?? `section-${sectionIndex}`,
|
|
718
|
-
title: section.title,
|
|
719
|
-
subtitle: section.description,
|
|
720
|
-
variant: section.variant ?? 'plain',
|
|
721
|
-
className: section.className,
|
|
722
|
-
layout: section.layout ?? (section.tabs && section.tabs.length > 0 ? 'tabs' : 'grid'),
|
|
723
|
-
grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
|
|
724
|
-
flex: section.flex,
|
|
725
|
-
hidden: section.hidden,
|
|
726
|
-
};
|
|
727
|
-
|
|
728
|
-
// Tabs layout
|
|
729
|
-
if (baseNode.layout === 'tabs' && section.tabs && section.tabs.length > 0) {
|
|
730
|
-
baseNode.defaultTabId = section.defaultTabId ?? section.tabs[0]?.id;
|
|
731
|
-
baseNode.tabsListClassName = section.tabsListClassName;
|
|
732
|
-
baseNode.tabsContentClassName = section.tabsContentClassName;
|
|
733
|
-
baseNode.tabs = section.tabs.map((tab, _tabIdx) => {
|
|
734
|
-
// Each tab can contain multiple sub-sections; wrap them under a container node
|
|
735
|
-
const nestedNodes = tab.sections.map((subSection, subIdx) => buildSectionNode(subSection, subIdx));
|
|
736
|
-
const containerNode: SectionNode = {
|
|
737
|
-
id: `${baseNode.id}-tab-${tab.id}`,
|
|
738
|
-
title: undefined,
|
|
739
|
-
subtitle: undefined,
|
|
740
|
-
variant: 'plain',
|
|
741
|
-
layout: 'grid',
|
|
742
|
-
grid: section.grid ?? { cols: 1, mdCols: 2, gap: 'gap-4' },
|
|
743
|
-
children: nestedNodes,
|
|
744
|
-
} as SectionNode;
|
|
745
|
-
return {
|
|
746
|
-
id: tab.id,
|
|
747
|
-
label: tab.label,
|
|
748
|
-
className: tab.className,
|
|
749
|
-
contentClassName: tab.contentClassName,
|
|
750
|
-
node: containerNode,
|
|
751
|
-
};
|
|
752
|
-
});
|
|
753
|
-
return baseNode;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
// Regular non-tab section with direct fields
|
|
757
|
-
baseNode.children = buildLeavesFromFields(section.fields);
|
|
758
|
-
return baseNode;
|
|
759
|
-
};
|
|
488
|
+
const sectionNodes = useMemo(
|
|
489
|
+
() =>
|
|
490
|
+
buildSectionNodes({
|
|
491
|
+
sections,
|
|
492
|
+
control,
|
|
493
|
+
handleFieldDependencies,
|
|
494
|
+
handleFieldChange,
|
|
495
|
+
onFieldChange,
|
|
496
|
+
getValues,
|
|
497
|
+
}),
|
|
498
|
+
[sections, control, handleFieldDependencies, handleFieldChange, onFieldChange, getValues],
|
|
499
|
+
);
|
|
760
500
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
501
|
+
const contextValue = useMemo(
|
|
502
|
+
() => ({
|
|
503
|
+
control,
|
|
504
|
+
getValues,
|
|
505
|
+
setValue,
|
|
506
|
+
onFieldChange,
|
|
507
|
+
handleFieldDependencies,
|
|
508
|
+
handleFieldChange,
|
|
509
|
+
}) satisfies FormBuilderContextValue<TFieldValues>,
|
|
510
|
+
[control, getValues, setValue, onFieldChange, handleFieldDependencies, handleFieldChange],
|
|
511
|
+
);
|
|
770
512
|
|
|
771
513
|
return (
|
|
772
|
-
<
|
|
773
|
-
<
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
<Button
|
|
789
|
-
type="submit"
|
|
790
|
-
disabled={isSubmitting}
|
|
791
|
-
className="sm:order-last"
|
|
514
|
+
<FormBuilderContext.Provider value={contextValue as unknown as FormBuilderContextValue<FieldValues>}>
|
|
515
|
+
<div className={cn('space-y-6', className)}>
|
|
516
|
+
<form
|
|
517
|
+
onSubmit={handleSubmit(handleFormSubmit)}
|
|
518
|
+
className={cn('space-y-6', formClassName)}
|
|
519
|
+
>
|
|
520
|
+
<SectionBuilder sections={sectionNodes} />
|
|
521
|
+
|
|
522
|
+
{showActions && (
|
|
523
|
+
<div
|
|
524
|
+
className={cn(
|
|
525
|
+
'flex flex-col sm:flex-row gap-3',
|
|
526
|
+
showActionsSeparator && 'pt-6',
|
|
527
|
+
showActionsSeparator && 'border-t',
|
|
528
|
+
actionsClassName
|
|
529
|
+
)}
|
|
792
530
|
>
|
|
793
|
-
{isSubmitting ? 'Submitting...' : submitLabel}
|
|
794
|
-
</Button>
|
|
795
|
-
|
|
796
|
-
{onCancel && (
|
|
797
|
-
<Button
|
|
798
|
-
type="button"
|
|
799
|
-
variant="outline"
|
|
800
|
-
onClick={onCancel}
|
|
801
|
-
disabled={isSubmitting}
|
|
802
|
-
>
|
|
803
|
-
{cancelLabel}
|
|
804
|
-
</Button>
|
|
805
|
-
)}
|
|
806
|
-
|
|
807
|
-
{onReset && (
|
|
808
531
|
<Button
|
|
809
|
-
type="
|
|
810
|
-
variant="outline"
|
|
811
|
-
onClick={handleReset}
|
|
532
|
+
type="submit"
|
|
812
533
|
disabled={isSubmitting}
|
|
534
|
+
className="sm:order-last"
|
|
813
535
|
>
|
|
814
|
-
{
|
|
536
|
+
{isSubmitting ? 'Submitting...' : submitLabel}
|
|
815
537
|
</Button>
|
|
816
|
-
)}
|
|
817
538
|
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
539
|
+
{onCancel && (
|
|
540
|
+
<Button
|
|
541
|
+
type="button"
|
|
542
|
+
variant="outline"
|
|
543
|
+
onClick={onCancel}
|
|
544
|
+
disabled={isSubmitting}
|
|
545
|
+
>
|
|
546
|
+
{cancelLabel}
|
|
547
|
+
</Button>
|
|
548
|
+
)}
|
|
549
|
+
|
|
550
|
+
{onReset && (
|
|
551
|
+
<Button
|
|
552
|
+
type="button"
|
|
553
|
+
variant="outline"
|
|
554
|
+
onClick={handleReset}
|
|
555
|
+
disabled={isSubmitting}
|
|
556
|
+
>
|
|
557
|
+
{resetLabel}
|
|
558
|
+
</Button>
|
|
559
|
+
)}
|
|
560
|
+
|
|
561
|
+
{customActions}
|
|
562
|
+
</div>
|
|
563
|
+
)}
|
|
564
|
+
</form>
|
|
565
|
+
</div>
|
|
566
|
+
</FormBuilderContext.Provider>
|
|
823
567
|
);
|
|
824
568
|
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react';
|
|
2
|
+
import type {
|
|
3
|
+
Control,
|
|
4
|
+
FieldValues,
|
|
5
|
+
Path,
|
|
6
|
+
UseFormGetValues,
|
|
7
|
+
UseFormSetValue,
|
|
8
|
+
} from 'react-hook-form';
|
|
9
|
+
import type { FormBuilderFieldConfig } from '../types';
|
|
10
|
+
|
|
11
|
+
interface DependencyState {
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
hidden?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FormBuilderContextValue<TFieldValues extends FieldValues = FieldValues> {
|
|
17
|
+
control: Control<TFieldValues>;
|
|
18
|
+
getValues: UseFormGetValues<TFieldValues>;
|
|
19
|
+
setValue: UseFormSetValue<TFieldValues>;
|
|
20
|
+
onFieldChange?: (
|
|
21
|
+
name: Path<TFieldValues> | string,
|
|
22
|
+
value: unknown,
|
|
23
|
+
allValues: TFieldValues
|
|
24
|
+
) => void;
|
|
25
|
+
handleFieldDependencies: (
|
|
26
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>
|
|
27
|
+
) => DependencyState;
|
|
28
|
+
handleFieldChange: (
|
|
29
|
+
field: FormBuilderFieldConfig<TFieldValues, string | Path<TFieldValues>>,
|
|
30
|
+
value: unknown,
|
|
31
|
+
...extras: unknown[]
|
|
32
|
+
) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const FormBuilderContext = createContext<FormBuilderContextValue<FieldValues> | null>(null);
|
|
36
|
+
|
|
37
|
+
export function useFormBuilderContext<TFieldValues extends FieldValues = FieldValues>() {
|
|
38
|
+
const value = useContext(FormBuilderContext) as FormBuilderContextValue<TFieldValues> | null;
|
|
39
|
+
if (!value) {
|
|
40
|
+
throw new Error('FormBuilderGroup must be used within a FormBuilder.');
|
|
41
|
+
}
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { FormBuilderContext };
|