@adlas/create-app 1.0.6 → 1.0.8
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/commands/init.js +8 -1
- package/dist/commands/init.js.map +1 -1
- package/package.json +1 -1
- package/templates/boilerplate/components/ui/Autocomplete.tsx +53 -0
- package/templates/boilerplate/components/ui/Breadcrumbs.tsx +49 -0
- package/templates/boilerplate/components/ui/Button.tsx +37 -0
- package/templates/boilerplate/components/ui/Checkbox.tsx +21 -0
- package/templates/boilerplate/components/ui/Chip.tsx +21 -0
- package/templates/boilerplate/components/ui/DatePicker.tsx +82 -0
- package/templates/boilerplate/components/ui/DocumentUpload.tsx +415 -0
- package/templates/boilerplate/components/ui/Icon.tsx +43 -0
- package/templates/boilerplate/components/ui/Input.tsx +27 -0
- package/templates/boilerplate/components/ui/Modal.tsx +51 -0
- package/templates/boilerplate/components/ui/NumberInput.tsx +59 -0
- package/templates/boilerplate/components/ui/PasswordInput.tsx +51 -0
- package/templates/boilerplate/components/ui/RadioGroup.tsx +50 -0
- package/templates/boilerplate/components/ui/Select.tsx +27 -0
- package/templates/boilerplate/components/ui/Tabs.tsx +21 -0
- package/templates/boilerplate/components/ui/Textarea.tsx +41 -0
- package/templates/boilerplate/components/ui/form/Form.tsx +277 -0
- package/templates/boilerplate/components/ui/form/FormContext.tsx +10 -0
- package/templates/boilerplate/components/ui/form/FormGenerator.tsx +304 -0
- package/templates/boilerplate/components/ui/index.ts +19 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps } from 'react';
|
|
4
|
+
|
|
5
|
+
import { extendVariants, Select as HerouiSelect } from '@heroui/react';
|
|
6
|
+
|
|
7
|
+
const BaseSelect = extendVariants(HerouiSelect, {
|
|
8
|
+
variants: {
|
|
9
|
+
color: {},
|
|
10
|
+
isDisabled: { true: {} },
|
|
11
|
+
size: {
|
|
12
|
+
sm: {},
|
|
13
|
+
md: {},
|
|
14
|
+
lg: {},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
size: 'md',
|
|
19
|
+
},
|
|
20
|
+
compoundVariants: [],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const CustomizedSelect = (props: ComponentProps<typeof BaseSelect>) => {
|
|
24
|
+
return <BaseSelect {...props} />;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { CustomizedSelect as Select };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { extendVariants, Tabs as HerouiTabs } from '@heroui/react';
|
|
4
|
+
|
|
5
|
+
const Tabs = extendVariants(HerouiTabs, {
|
|
6
|
+
variants: {
|
|
7
|
+
color: {},
|
|
8
|
+
isDisabled: { true: {} },
|
|
9
|
+
size: {
|
|
10
|
+
sm: {},
|
|
11
|
+
md: {},
|
|
12
|
+
lg: {},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
defaultVariants: {
|
|
16
|
+
size: 'md',
|
|
17
|
+
},
|
|
18
|
+
compoundVariants: [],
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export { Tabs };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ComponentProps, FormEvent } from 'react';
|
|
4
|
+
|
|
5
|
+
import { extendVariants, Textarea as HerouiTextArea } from '@heroui/react';
|
|
6
|
+
|
|
7
|
+
const BaseTextarea = extendVariants(HerouiTextArea, {
|
|
8
|
+
variants: {
|
|
9
|
+
color: {},
|
|
10
|
+
isDisabled: { true: {} },
|
|
11
|
+
size: {
|
|
12
|
+
sm: {},
|
|
13
|
+
md: {},
|
|
14
|
+
lg: {},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
defaultVariants: {
|
|
18
|
+
size: 'md',
|
|
19
|
+
},
|
|
20
|
+
compoundVariants: [],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const Textarea = (props: ComponentProps<typeof BaseTextarea>) => {
|
|
24
|
+
const { onInput, ...restProps } = props;
|
|
25
|
+
|
|
26
|
+
const handleInput = (e: FormEvent<HTMLInputElement>) => {
|
|
27
|
+
const target = e.target as HTMLTextAreaElement;
|
|
28
|
+
|
|
29
|
+
if (target.value.startsWith(' ')) {
|
|
30
|
+
target.value = target.value.trimStart();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (onInput) {
|
|
34
|
+
onInput(e);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return <BaseTextarea onInput={handleInput} {...restProps} />;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export { Textarea };
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { InputProps } from '@heroui/react';
|
|
3
|
+
import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
|
|
4
|
+
import type { FieldValues, Path, Resolver, UseFormReturn } from 'react-hook-form';
|
|
5
|
+
import type { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
import { use, useEffect, useMemo } from 'react';
|
|
8
|
+
|
|
9
|
+
import clsx from 'clsx';
|
|
10
|
+
import { useTranslations } from 'next-intl';
|
|
11
|
+
import { Controller, useForm } from 'react-hook-form';
|
|
12
|
+
import { z as zod } from 'zod';
|
|
13
|
+
|
|
14
|
+
import type { FormFieldValidation } from '@/validations';
|
|
15
|
+
|
|
16
|
+
import { createZodErrorMap } from '@/validations/zodErrorMap';
|
|
17
|
+
|
|
18
|
+
import type { FileValidationError } from '../DocumentUpload';
|
|
19
|
+
|
|
20
|
+
import { FormContext } from './FormContext';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Translation function type
|
|
24
|
+
*/
|
|
25
|
+
type TranslationFn = (key: string, params?: Record<string, unknown>) => string;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Translates error message if it's a translation key
|
|
29
|
+
* Handles both simple keys and keys with parameters
|
|
30
|
+
*/
|
|
31
|
+
function translateErrorMessage(message: string | undefined, t: TranslationFn): string | undefined {
|
|
32
|
+
if (!message) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check if message is a translation key (starts with "Validations.")
|
|
37
|
+
if (message.includes('.') && message.startsWith('Validations.')) {
|
|
38
|
+
try {
|
|
39
|
+
return t(message);
|
|
40
|
+
} catch {
|
|
41
|
+
// If translation fails, return original message
|
|
42
|
+
return message;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Return as-is if not a translation key
|
|
47
|
+
return message;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Type-safe resolver wrapper that handles zod version compatibility
|
|
51
|
+
function createZodResolver<T extends FieldValues>(schema: z.ZodType<T>): Resolver<T> {
|
|
52
|
+
return ((_values: T, _context: object, _options: { criteriaMode?: string; names?: string[] }) => {
|
|
53
|
+
try {
|
|
54
|
+
const result = schema.safeParse(_values);
|
|
55
|
+
|
|
56
|
+
if (result.success) {
|
|
57
|
+
return { values: _values, errors: {} };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const errors: Record<string, { type: string; message: string }> = {};
|
|
61
|
+
|
|
62
|
+
result.error.issues.forEach(issue => {
|
|
63
|
+
const path = issue.path.join('.');
|
|
64
|
+
|
|
65
|
+
if (path) {
|
|
66
|
+
errors[path] = {
|
|
67
|
+
type: 'validation',
|
|
68
|
+
message: issue.message,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { values: {}, errors };
|
|
74
|
+
} catch {
|
|
75
|
+
return { values: {}, errors: {} };
|
|
76
|
+
}
|
|
77
|
+
}) as Resolver<T>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
type FormProps<T extends FieldValues> = {
|
|
81
|
+
children: ReactNode;
|
|
82
|
+
validationSchema?: z.ZodType<T>;
|
|
83
|
+
form?: UseFormReturn<T>;
|
|
84
|
+
fieldValidation?: Record<keyof T, FormFieldValidation>;
|
|
85
|
+
className?: string;
|
|
86
|
+
} & Omit<HTMLAttributes<HTMLDivElement>, 'children'>;
|
|
87
|
+
|
|
88
|
+
export type FieldEvent = {
|
|
89
|
+
target?: {
|
|
90
|
+
value?: string | File | FileValidationError | null;
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export type BaseFormFieldProps = {
|
|
95
|
+
onChange?: (e: FieldEvent) => void;
|
|
96
|
+
onBlur?: () => void;
|
|
97
|
+
onFocus?: () => void;
|
|
98
|
+
value?: string;
|
|
99
|
+
defaultValue?: string;
|
|
100
|
+
isInvalid?: boolean;
|
|
101
|
+
errorMessage?: string;
|
|
102
|
+
name?: string;
|
|
103
|
+
disabled?: boolean;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export type FormFieldProps = BaseFormFieldProps &
|
|
107
|
+
Partial<Omit<InputProps, keyof BaseFormFieldProps>>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* FormFieldKeys provides automatic type suggestions for form field names.
|
|
111
|
+
* When typing the field name as a string, you'll get autocompletion for all available fields
|
|
112
|
+
* defined in your form schema.
|
|
113
|
+
*
|
|
114
|
+
* Usage example:
|
|
115
|
+
* <FormItem name="phone_number"> // You'll get suggestions when typing
|
|
116
|
+
*/
|
|
117
|
+
// export type FormFieldKeys<T extends FieldValues> = Extract<keyof T, string> & Path<T>;
|
|
118
|
+
|
|
119
|
+
export type FormItemPropsT<T extends FieldValues> = Omit<
|
|
120
|
+
HTMLAttributes<HTMLDivElement>,
|
|
121
|
+
'children'
|
|
122
|
+
> & {
|
|
123
|
+
// Use more direct type for better IDE autocompletion
|
|
124
|
+
name: keyof T;
|
|
125
|
+
form?: UseFormReturn<T>;
|
|
126
|
+
label?: string;
|
|
127
|
+
rowsNumber?: 1 | 2;
|
|
128
|
+
validation?: FormFieldValidation;
|
|
129
|
+
overrideOnChange?: (value: string) => string;
|
|
130
|
+
children: (props: BaseFormFieldProps) => ReactElement;
|
|
131
|
+
isDisabled?: boolean;
|
|
132
|
+
extraLabel?: string;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function FormComponent<T extends FieldValues>({
|
|
136
|
+
children,
|
|
137
|
+
validationSchema,
|
|
138
|
+
form: externalForm,
|
|
139
|
+
fieldValidation,
|
|
140
|
+
className,
|
|
141
|
+
...props
|
|
142
|
+
}: FormProps<T>) {
|
|
143
|
+
const t = useTranslations() as TranslationFn;
|
|
144
|
+
|
|
145
|
+
// Set global error map for Zod translations when component mounts
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const errorMap = createZodErrorMap(t);
|
|
148
|
+
zod.setErrorMap(errorMap);
|
|
149
|
+
}, [t]);
|
|
150
|
+
|
|
151
|
+
const internalForm = useForm<T>({
|
|
152
|
+
...(validationSchema && {
|
|
153
|
+
resolver: createZodResolver(validationSchema),
|
|
154
|
+
}),
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
const form = externalForm || internalForm;
|
|
158
|
+
|
|
159
|
+
const contextValue = useMemo(() => ({ form: form as UseFormReturn<FieldValues> }), [form]);
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<FormContext value={contextValue}>
|
|
163
|
+
<div
|
|
164
|
+
className={clsx('grid grid-cols-10 justify-between gap-6 max-sm:gap-4', className)}
|
|
165
|
+
{...props}
|
|
166
|
+
>
|
|
167
|
+
{children}
|
|
168
|
+
</div>
|
|
169
|
+
</FormContext>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function FormItemComponent<T extends FieldValues>({
|
|
174
|
+
name,
|
|
175
|
+
children,
|
|
176
|
+
validation,
|
|
177
|
+
form: propForm,
|
|
178
|
+
className,
|
|
179
|
+
rowsNumber = 1,
|
|
180
|
+
overrideOnChange,
|
|
181
|
+
extraLabel,
|
|
182
|
+
isDisabled,
|
|
183
|
+
...props
|
|
184
|
+
}: FormItemPropsT<T>) {
|
|
185
|
+
const context = use(FormContext);
|
|
186
|
+
const form: UseFormReturn<T> = propForm || (context?.form as UseFormReturn<T>);
|
|
187
|
+
const t = useTranslations() as TranslationFn;
|
|
188
|
+
|
|
189
|
+
if (!form) {
|
|
190
|
+
throw new Error('FormItem must be used within a Form or with a form prop');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Get all available field names from the form schema
|
|
194
|
+
const availableFields = Object.keys(form.getValues());
|
|
195
|
+
|
|
196
|
+
// Convert name to string for validation
|
|
197
|
+
const nameStr = String(name);
|
|
198
|
+
|
|
199
|
+
// Validate the field name
|
|
200
|
+
if (!nameStr || !availableFields.includes(nameStr)) {
|
|
201
|
+
const suggestion = nameStr
|
|
202
|
+
? availableFields.find(field => field.toLowerCase().includes(nameStr.toLowerCase()))
|
|
203
|
+
: null;
|
|
204
|
+
|
|
205
|
+
const errorMessage = nameStr
|
|
206
|
+
? `Field name "${nameStr}" does not exist in the form schema.`
|
|
207
|
+
: 'Empty field name is not allowed.';
|
|
208
|
+
|
|
209
|
+
const suggestions =
|
|
210
|
+
availableFields.length > 0
|
|
211
|
+
? `Available fields are: ${availableFields.join(', ')}`
|
|
212
|
+
: 'No fields available in this form.';
|
|
213
|
+
|
|
214
|
+
const suggestedField = suggestion ? `Did you mean "${suggestion}"?` : '';
|
|
215
|
+
|
|
216
|
+
console.error(`${errorMessage} ${suggestions} ${suggestedField}`);
|
|
217
|
+
|
|
218
|
+
throw new Error(`${errorMessage} ${suggestions} ${suggestedField}`);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const itemClassName = clsx(
|
|
222
|
+
'flex flex-col',
|
|
223
|
+
rowsNumber === 2 ? 'col-span-5! max-xs:col-span-10!' : 'col-span-10',
|
|
224
|
+
className,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<div className={itemClassName} {...props}>
|
|
229
|
+
{extraLabel && (
|
|
230
|
+
<span className="mb-2 text-sm leading-5 font-medium text-default-600">{extraLabel}</span>
|
|
231
|
+
)}
|
|
232
|
+
<Controller
|
|
233
|
+
control={form.control}
|
|
234
|
+
name={nameStr as Path<T>}
|
|
235
|
+
render={({ field: { onChange, disabled, onBlur, value: formValue }, fieldState }) => {
|
|
236
|
+
const fieldProps: BaseFormFieldProps = {
|
|
237
|
+
onChange: (e: FieldEvent) => {
|
|
238
|
+
const targetValue = e?.target?.value;
|
|
239
|
+
|
|
240
|
+
if (targetValue === null || targetValue === undefined) {
|
|
241
|
+
onChange('');
|
|
242
|
+
} else if (typeof targetValue === 'string') {
|
|
243
|
+
onChange(overrideOnChange ? overrideOnChange(targetValue) : targetValue);
|
|
244
|
+
} else if (targetValue instanceof File) {
|
|
245
|
+
// File type
|
|
246
|
+
onChange(targetValue);
|
|
247
|
+
} else {
|
|
248
|
+
// FileValidationError type
|
|
249
|
+
onChange(targetValue);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
onBlur,
|
|
253
|
+
onFocus: () => {
|
|
254
|
+
// Trigger validation if needed
|
|
255
|
+
if (validation) {
|
|
256
|
+
form.trigger(nameStr as Path<T>);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
259
|
+
value: (formValue as string) ?? '',
|
|
260
|
+
defaultValue: (formValue as string) ?? '',
|
|
261
|
+
isInvalid: fieldState.invalid,
|
|
262
|
+
errorMessage: translateErrorMessage(fieldState.error?.message, t),
|
|
263
|
+
name: nameStr,
|
|
264
|
+
disabled: isDisabled || disabled,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return children(fieldProps);
|
|
268
|
+
}}
|
|
269
|
+
/>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
FormComponent.Item = FormItemComponent;
|
|
275
|
+
|
|
276
|
+
export const Form = FormComponent;
|
|
277
|
+
export const FormItem = FormItemComponent;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { FieldValues, UseFormReturn } from 'react-hook-form';
|
|
3
|
+
|
|
4
|
+
import { createContext } from 'react';
|
|
5
|
+
|
|
6
|
+
export type FormContextValue<T extends FieldValues = FieldValues> = {
|
|
7
|
+
form: UseFormReturn<T>;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const FormContext = createContext<FormContextValue<FieldValues> | null>(null);
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import type { DatePickerProps, InputProps, Selection, SelectProps } from '@heroui/react';
|
|
3
|
+
import type { UseFormReturn } from 'react-hook-form';
|
|
4
|
+
|
|
5
|
+
import { AutocompleteItem, SelectItem } from '@heroui/react';
|
|
6
|
+
|
|
7
|
+
import type { BaseFormFieldProps } from './Form';
|
|
8
|
+
|
|
9
|
+
import { Autocomplete } from '../Autocomplete';
|
|
10
|
+
import { DatePicker } from '../DatePicker';
|
|
11
|
+
import { DocumentUpload } from '../DocumentUpload';
|
|
12
|
+
import { Input } from '../Input';
|
|
13
|
+
import { NumberInput } from '../NumberInput';
|
|
14
|
+
import { PasswordInput } from '../PasswordInput';
|
|
15
|
+
import { Select } from '../Select';
|
|
16
|
+
import { Textarea } from '../Textarea';
|
|
17
|
+
import { Form } from './Form';
|
|
18
|
+
|
|
19
|
+
type BaseFieldProps = {
|
|
20
|
+
name: string;
|
|
21
|
+
label?: string;
|
|
22
|
+
placeholder?: string;
|
|
23
|
+
extraLabel?: string;
|
|
24
|
+
className?: string;
|
|
25
|
+
rowsNumber?: 1 | 2;
|
|
26
|
+
overrideOnChange?: (value: string) => string;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type SelectOption = {
|
|
30
|
+
label: string;
|
|
31
|
+
value: string | number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Specific field type definitions
|
|
35
|
+
type FormTextInputField = {
|
|
36
|
+
type: 'textInput' | 'numberInput' | 'passwordInput' | 'textarea' | 'persianInput';
|
|
37
|
+
inputProps?: Omit<InputProps, keyof BaseFormFieldProps>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type FormSelectField = {
|
|
41
|
+
type: 'select' | 'autocomplete';
|
|
42
|
+
options: SelectOption[];
|
|
43
|
+
selectProps?: Omit<SelectProps, keyof BaseFormFieldProps | 'children' | 'ref'>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
type FormDocumentUploadField = {
|
|
47
|
+
type: 'documentUpload';
|
|
48
|
+
description?: string;
|
|
49
|
+
required?: boolean;
|
|
50
|
+
accept?: string;
|
|
51
|
+
maxSize?: number;
|
|
52
|
+
multiple?: boolean;
|
|
53
|
+
/** If true, file will not be uploaded immediately - parent handles upload */
|
|
54
|
+
deferUpload?: boolean;
|
|
55
|
+
/** Callback when file is selected (for deferred upload mode) */
|
|
56
|
+
onFileSelected?: (file: File | null) => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type FormDatePickerField = {
|
|
60
|
+
type: 'datePicker';
|
|
61
|
+
datePickerProps?: Omit<DatePickerProps, keyof BaseFormFieldProps | 'onChange' | 'value'>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type FormGeneratorField = (
|
|
65
|
+
| FormTextInputField
|
|
66
|
+
| FormSelectField
|
|
67
|
+
| FormDocumentUploadField
|
|
68
|
+
| FormDatePickerField
|
|
69
|
+
) &
|
|
70
|
+
BaseFieldProps;
|
|
71
|
+
|
|
72
|
+
export type FormGeneratorProps<
|
|
73
|
+
TFormValues extends Record<string, unknown> = Record<string, unknown>,
|
|
74
|
+
> = {
|
|
75
|
+
fields: FormGeneratorField[];
|
|
76
|
+
className?: string;
|
|
77
|
+
form?: UseFormReturn<TFormValues>;
|
|
78
|
+
onSubmit?: (data: TFormValues) => void | Promise<void>;
|
|
79
|
+
isDisabled?: boolean;
|
|
80
|
+
rowsNumber?: 1 | 2;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const FormGenerator = <TFormValues extends Record<string, unknown> = Record<string, unknown>>({
|
|
84
|
+
fields,
|
|
85
|
+
className,
|
|
86
|
+
form,
|
|
87
|
+
isDisabled,
|
|
88
|
+
rowsNumber,
|
|
89
|
+
}: FormGeneratorProps<TFormValues>) => {
|
|
90
|
+
const renderField = (field: FormGeneratorField) => {
|
|
91
|
+
switch (field.type) {
|
|
92
|
+
case 'textInput':
|
|
93
|
+
return (props: BaseFormFieldProps) => (
|
|
94
|
+
<Input
|
|
95
|
+
label={field.label}
|
|
96
|
+
placeholder={field.placeholder}
|
|
97
|
+
{...field.inputProps}
|
|
98
|
+
{...props}
|
|
99
|
+
ref={undefined}
|
|
100
|
+
isDisabled={isDisabled || field.inputProps?.isDisabled}
|
|
101
|
+
/>
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
case 'textarea':
|
|
105
|
+
return (props: BaseFormFieldProps) => (
|
|
106
|
+
<Textarea
|
|
107
|
+
label={field.label}
|
|
108
|
+
placeholder={field.placeholder}
|
|
109
|
+
{...field.inputProps}
|
|
110
|
+
{...props}
|
|
111
|
+
ref={undefined}
|
|
112
|
+
isDisabled={isDisabled || field.inputProps?.isDisabled}
|
|
113
|
+
/>
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
case 'numberInput':
|
|
117
|
+
return (props: BaseFormFieldProps) => {
|
|
118
|
+
const numberInputProps = {
|
|
119
|
+
label: field.label,
|
|
120
|
+
...field.inputProps,
|
|
121
|
+
...props,
|
|
122
|
+
isDisabled,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<NumberInput
|
|
127
|
+
{...numberInputProps}
|
|
128
|
+
isDisabled={isDisabled || field.inputProps?.isDisabled}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
case 'passwordInput':
|
|
134
|
+
return (props: BaseFormFieldProps) => (
|
|
135
|
+
<PasswordInput
|
|
136
|
+
label={field.label}
|
|
137
|
+
placeholder={field.placeholder}
|
|
138
|
+
{...field.inputProps}
|
|
139
|
+
{...props}
|
|
140
|
+
ref={undefined}
|
|
141
|
+
isDisabled={isDisabled || field.inputProps?.isDisabled}
|
|
142
|
+
/>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
case 'select': {
|
|
146
|
+
const { options } = field;
|
|
147
|
+
|
|
148
|
+
return (props: BaseFormFieldProps) => {
|
|
149
|
+
const { value, onChange, disabled } = props;
|
|
150
|
+
|
|
151
|
+
return (
|
|
152
|
+
<Select
|
|
153
|
+
label={field.label}
|
|
154
|
+
placeholder={field.placeholder}
|
|
155
|
+
aria-label={field.label || field.name}
|
|
156
|
+
selectedKeys={value ? [value] : []}
|
|
157
|
+
isDisabled={disabled || isDisabled || field.selectProps?.isDisabled}
|
|
158
|
+
onSelectionChange={(keys: Selection) => {
|
|
159
|
+
const selectedKey = Array.from(keys)[0];
|
|
160
|
+
onChange?.({ target: { value: selectedKey?.toString() } });
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
163
|
+
{options.map(option => (
|
|
164
|
+
<SelectItem key={option.value} textValue={option.label}>
|
|
165
|
+
{option.label}
|
|
166
|
+
</SelectItem>
|
|
167
|
+
))}
|
|
168
|
+
</Select>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
case 'autocomplete': {
|
|
174
|
+
const { options, ...autocompleteProps } = field;
|
|
175
|
+
|
|
176
|
+
return (props: BaseFormFieldProps) => {
|
|
177
|
+
const { value, onChange, ...restProps } = props;
|
|
178
|
+
|
|
179
|
+
return (
|
|
180
|
+
<Autocomplete
|
|
181
|
+
ref={undefined}
|
|
182
|
+
aria-label={field.label || field.name}
|
|
183
|
+
label={field.label}
|
|
184
|
+
selectedKey={value || null}
|
|
185
|
+
onSelectionChange={key => {
|
|
186
|
+
onChange?.({ target: { value: key ? key?.toString() : '' } });
|
|
187
|
+
}}
|
|
188
|
+
{...autocompleteProps}
|
|
189
|
+
{...restProps}
|
|
190
|
+
isDisabled={isDisabled || field.selectProps?.isDisabled || props.disabled}
|
|
191
|
+
>
|
|
192
|
+
{options.map(option => (
|
|
193
|
+
<AutocompleteItem key={option.value} textValue={option.label}>
|
|
194
|
+
{option.label}
|
|
195
|
+
</AutocompleteItem>
|
|
196
|
+
))}
|
|
197
|
+
</Autocomplete>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
case 'documentUpload': {
|
|
203
|
+
return (props: BaseFormFieldProps) => {
|
|
204
|
+
const { value, onChange, ...restProps } = props;
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<DocumentUpload
|
|
208
|
+
accept={field.accept}
|
|
209
|
+
description={field.description}
|
|
210
|
+
errorMessage={restProps.errorMessage}
|
|
211
|
+
hasError={props.isInvalid}
|
|
212
|
+
label={field.label || ''}
|
|
213
|
+
maxSize={field.maxSize}
|
|
214
|
+
required={field.required}
|
|
215
|
+
deferUpload={field.deferUpload}
|
|
216
|
+
value={typeof value === 'string' ? value : null}
|
|
217
|
+
onChange={fileOrError => {
|
|
218
|
+
if (
|
|
219
|
+
field.deferUpload &&
|
|
220
|
+
fileOrError &&
|
|
221
|
+
typeof fileOrError === 'object' &&
|
|
222
|
+
'isValidationError' in fileOrError
|
|
223
|
+
) {
|
|
224
|
+
// Handle validation error in deferred mode
|
|
225
|
+
onChange?.({ target: { value: '' } });
|
|
226
|
+
field.onFileSelected?.(null);
|
|
227
|
+
} else if (field.deferUpload && fileOrError instanceof File) {
|
|
228
|
+
// In deferred mode, set blob URL first to satisfy validation
|
|
229
|
+
const blobUrl = URL.createObjectURL(fileOrError);
|
|
230
|
+
onChange?.({ target: { value: blobUrl } });
|
|
231
|
+
// Then call the callback with the file
|
|
232
|
+
field.onFileSelected?.(fileOrError);
|
|
233
|
+
} else if (field.deferUpload && fileOrError === null) {
|
|
234
|
+
// File was removed in deferred mode
|
|
235
|
+
onChange?.({ target: { value: '' } });
|
|
236
|
+
field.onFileSelected?.(null);
|
|
237
|
+
} else if (!field.deferUpload) {
|
|
238
|
+
// In immediate mode, don't update form value - wait for onImageUploaded
|
|
239
|
+
}
|
|
240
|
+
}}
|
|
241
|
+
onImageUploaded={(_imageId, imageUrl) => {
|
|
242
|
+
// Update form value with the uploaded image URL string (immediate mode only)
|
|
243
|
+
if (!field.deferUpload) {
|
|
244
|
+
onChange?.({ target: { value: imageUrl } });
|
|
245
|
+
}
|
|
246
|
+
}}
|
|
247
|
+
onError={() => {
|
|
248
|
+
// Clear the value on error
|
|
249
|
+
onChange?.({ target: { value: '' } });
|
|
250
|
+
}}
|
|
251
|
+
{...restProps}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
case 'datePicker': {
|
|
258
|
+
return (props: BaseFormFieldProps) => {
|
|
259
|
+
const { value, onChange, disabled, isInvalid, errorMessage } = props;
|
|
260
|
+
const { color: _, ...datePickerProps } = (field.datePickerProps || {}) as Record<
|
|
261
|
+
string,
|
|
262
|
+
unknown
|
|
263
|
+
> & { color?: string };
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<DatePicker
|
|
267
|
+
label={field.label}
|
|
268
|
+
value={value || null}
|
|
269
|
+
onChange={dateValue => {
|
|
270
|
+
onChange?.({ target: { value: dateValue || '' } });
|
|
271
|
+
}}
|
|
272
|
+
isDisabled={disabled || isDisabled || field.datePickerProps?.isDisabled}
|
|
273
|
+
isInvalid={isInvalid}
|
|
274
|
+
errorMessage={errorMessage}
|
|
275
|
+
{...datePickerProps}
|
|
276
|
+
/>
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
default:
|
|
282
|
+
return (props: BaseFormFieldProps) => <Input {...props} isDisabled={isDisabled} />;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<Form className={className} form={form}>
|
|
288
|
+
{fields.map(field => (
|
|
289
|
+
<Form.Item
|
|
290
|
+
key={field.name}
|
|
291
|
+
className={field.className}
|
|
292
|
+
extraLabel={field.extraLabel}
|
|
293
|
+
name={field.name}
|
|
294
|
+
overrideOnChange={field.overrideOnChange}
|
|
295
|
+
rowsNumber={rowsNumber || field.rowsNumber || 1}
|
|
296
|
+
>
|
|
297
|
+
{renderField(field)}
|
|
298
|
+
</Form.Item>
|
|
299
|
+
))}
|
|
300
|
+
</Form>
|
|
301
|
+
);
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export { FormGenerator };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export * from './Autocomplete';
|
|
2
|
+
export * from './Breadcrumbs';
|
|
3
|
+
export * from './Button';
|
|
4
|
+
export * from './Checkbox';
|
|
5
|
+
export * from './Chip';
|
|
6
|
+
export * from './DatePicker';
|
|
7
|
+
export * from './DocumentUpload';
|
|
8
|
+
export * from './form/Form';
|
|
9
|
+
export * from './form/FormContext';
|
|
10
|
+
export * from './form/FormGenerator';
|
|
11
|
+
export * from './Icon';
|
|
12
|
+
export * from './Input';
|
|
13
|
+
export * from './Modal';
|
|
14
|
+
export * from './NumberInput';
|
|
15
|
+
export * from './PasswordInput';
|
|
16
|
+
export * from './RadioGroup';
|
|
17
|
+
export * from './Select';
|
|
18
|
+
export * from './Tabs';
|
|
19
|
+
export * from './Textarea';
|