import { zodResolver } from '@hookform/resolvers/zod'; import { forwardRef, useEffect, useImperativeHandle, useMemo } from 'react'; import { DefaultValues, FieldValues, SubmitHandler, useForm, useFormContext, } from 'react-hook-form'; import { ZodSchema, z } from 'zod'; import EditTag from '@/components/edit-tag'; import { SelectWithSearch } from '@/components/originui/select-with-search'; import { RAGFlowFormItem } from '@/components/ragflow-form'; import { Checkbox } from '@/components/ui/checkbox'; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; import { t } from 'i18next'; import { Loader } from 'lucide-react'; // Field type enumeration export enum FormFieldType { Text = 'text', Email = 'email', Password = 'password', Number = 'number', Textarea = 'textarea', Select = 'select', Checkbox = 'checkbox', Tag = 'tag', } // Field configuration interface export interface FormFieldConfig { name: string; label: string; type: FormFieldType; hidden?: boolean; required?: boolean; placeholder?: string; options?: { label: string; value: string }[]; defaultValue?: any; validation?: { pattern?: RegExp; minLength?: number; maxLength?: number; min?: number; max?: number; message?: string; }; render?: (fieldProps: any) => React.ReactNode; horizontal?: boolean; onChange?: (value: any) => void; } // Component props interface interface DynamicFormProps { fields: FormFieldConfig[]; onSubmit: SubmitHandler; className?: string; children?: React.ReactNode; defaultValues?: DefaultValues; } // Form ref interface export interface DynamicFormRef { submit: () => void; getValues: () => any; reset: (values?: any) => void; } // Generate Zod validation schema based on field configurations const generateSchema = (fields: FormFieldConfig[]): ZodSchema => { const schema: Record = {}; const nestedSchemas: Record> = {}; fields.forEach((field) => { let fieldSchema: ZodSchema; // Create base validation schema based on field type switch (field.type) { case FormFieldType.Email: fieldSchema = z.string().email('Please enter a valid email address'); break; case FormFieldType.Number: fieldSchema = z.coerce.number(); if (field.validation?.min !== undefined) { fieldSchema = (fieldSchema as z.ZodNumber).min( field.validation.min, field.validation.message || `Value cannot be less than ${field.validation.min}`, ); } if (field.validation?.max !== undefined) { fieldSchema = (fieldSchema as z.ZodNumber).max( field.validation.max, field.validation.message || `Value cannot be greater than ${field.validation.max}`, ); } break; case FormFieldType.Checkbox: fieldSchema = z.boolean(); break; case FormFieldType.Tag: fieldSchema = z.array(z.string()); break; default: fieldSchema = z.string(); break; } // Handle required fields if (field.required) { if (field.type === FormFieldType.Checkbox) { fieldSchema = (fieldSchema as z.ZodBoolean).refine( (val) => val === true, { message: `${field.label} is required`, }, ); } else if (field.type === FormFieldType.Tag) { fieldSchema = (fieldSchema as z.ZodArray).min(1, { message: `${field.label} is required`, }); } else { fieldSchema = (fieldSchema as z.ZodString).min(1, { message: `${field.label} is required`, }); } } if (!field.required) { fieldSchema = fieldSchema.optional(); } // Handle other validation rules if ( field.type !== FormFieldType.Number && field.type !== FormFieldType.Checkbox && field.type !== FormFieldType.Tag && field.required ) { fieldSchema = fieldSchema as z.ZodString; if (field.validation?.minLength !== undefined) { fieldSchema = (fieldSchema as z.ZodString).min( field.validation.minLength, field.validation.message || `Enter at least ${field.validation.minLength} characters`, ); } if (field.validation?.maxLength !== undefined) { fieldSchema = (fieldSchema as z.ZodString).max( field.validation.maxLength, field.validation.message || `Enter up to ${field.validation.maxLength} characters`, ); } if (field.validation?.pattern) { fieldSchema = (fieldSchema as z.ZodString).regex( field.validation.pattern, field.validation.message || 'Invalid input format', ); } } if (field.name.includes('.')) { const keys = field.name.split('.'); const firstKey = keys[0]; if (!nestedSchemas[firstKey]) { nestedSchemas[firstKey] = {}; } let currentSchema = nestedSchemas[firstKey]; for (let i = 1; i < keys.length - 1; i++) { const key = keys[i]; if (!currentSchema[key]) { currentSchema[key] = {}; } currentSchema = currentSchema[key]; } const lastKey = keys[keys.length - 1]; currentSchema[lastKey] = fieldSchema; } else { schema[field.name] = fieldSchema; } }); Object.keys(nestedSchemas).forEach((key) => { const buildNestedSchema = (obj: Record): ZodSchema => { const nestedSchema: Record = {}; Object.keys(obj).forEach((subKey) => { if ( typeof obj[subKey] === 'object' && !(obj[subKey] instanceof z.ZodType) ) { nestedSchema[subKey] = buildNestedSchema(obj[subKey]); } else { nestedSchema[subKey] = obj[subKey]; } }); return z.object(nestedSchema); }; schema[key] = buildNestedSchema(nestedSchemas[key]); }); return z.object(schema); }; // Generate default values based on field configurations const generateDefaultValues = ( fields: FormFieldConfig[], ): DefaultValues => { const defaultValues: Record = {}; fields.forEach((field) => { if (field.name.includes('.')) { const keys = field.name.split('.'); let current = defaultValues; for (let i = 0; i < keys.length - 1; i++) { const key = keys[i]; if (!current[key]) { current[key] = {}; } current = current[key]; } const lastKey = keys[keys.length - 1]; if (field.defaultValue !== undefined) { current[lastKey] = field.defaultValue; } else if (field.type === FormFieldType.Checkbox) { current[lastKey] = false; } else if (field.type === FormFieldType.Tag) { current[lastKey] = []; } else { current[lastKey] = ''; } } else { if (field.defaultValue !== undefined) { defaultValues[field.name] = field.defaultValue; } else if (field.type === FormFieldType.Checkbox) { defaultValues[field.name] = false; } else if (field.type === FormFieldType.Tag) { defaultValues[field.name] = []; } else { defaultValues[field.name] = ''; } } }); return defaultValues as DefaultValues; }; // Dynamic form component const DynamicForm = { Root: forwardRef( ( { fields, onSubmit, className = '', children, defaultValues: formDefaultValues = {} as DefaultValues, }: DynamicFormProps, ref: React.Ref, ) => { // Generate validation schema and default values const schema = useMemo(() => generateSchema(fields), [fields]); const defaultValues = useMemo(() => { const value = { ...generateDefaultValues(fields), ...formDefaultValues, }; console.log('generateDefaultValues', fields, value); return value; }, [fields, formDefaultValues]); // Initialize form const form = useForm({ resolver: zodResolver(schema), defaultValues, }); // Expose form methods via ref useImperativeHandle(ref, () => ({ submit: () => form.handleSubmit(onSubmit)(), getValues: () => form.getValues(), reset: (values?: T) => { if (values) { form.reset(values); } else { form.reset(); } }, setError: form.setError, clearErrors: form.clearErrors, trigger: form.trigger, })); useEffect(() => { if (formDefaultValues && Object.keys(formDefaultValues).length > 0) { form.reset({ ...generateDefaultValues(fields), ...formDefaultValues, }); } }, [form, formDefaultValues, fields]); // Submit handler // const handleSubmit = form.handleSubmit(onSubmit); // Render form fields const renderField = (field: FormFieldConfig) => { if (field.render) { return ( {(fieldProps) => { const finalFieldProps = field.onChange ? { ...fieldProps, onChange: (e: any) => { fieldProps.onChange(e); field.onChange?.(e.target?.value ?? e); }, } : fieldProps; return field.render?.(finalFieldProps); }} ); } switch (field.type) { case FormFieldType.Textarea: return ( {(fieldProps) => { const finalFieldProps = field.onChange ? { ...fieldProps, onChange: (e: any) => { fieldProps.onChange(e); field.onChange?.(e.target.value); }, } : fieldProps; return (