import React, { useState, useCallback, useMemo } from 'react'; import { ZodType, ZodError, ZodIssue } from 'zod'; import { Logger } from '@hashgraphonline/standards-sdk'; import { motion, AnimatePresence } from 'framer-motion'; import { SchemaParser } from './SchemaParser'; import { FormField } from './FormField'; import { Button } from '../../ui/Button'; import Typography from '../../ui/Typography'; import { Alert, AlertDescription } from '../../ui/alert'; import { FiSend, FiRefreshCw, FiAlertCircle } from 'react-icons/fi'; import { cn } from '../../../lib/utils'; import type { FormState, FormActions, UseFormReturn, ValidationError, FormGenerationOptions, FormGroup } from './types'; interface FormGeneratorProps> { schema: ZodType; onSubmit?: (values: TSchema) => void | Promise; onValueChange?: (path: string[], value: unknown) => void; options?: FormGenerationOptions; initialValues?: Partial; className?: string; title?: string; description?: string; } /** * Main form generation engine that transforms Zod schemas into React forms * with progressive disclosure support */ export function FormGenerator>({ schema, onSubmit, onValueChange, options = {}, initialValues = {}, className, title, description }: FormGeneratorProps) { const logger = useMemo(() => new Logger({ module: 'FormGenerator' }), []); const schemaParser = useMemo(() => new SchemaParser(schema), [schema]); const parsedSchema = useMemo(() => schemaParser.parse(), [schemaParser]); const [values, setValues] = useState>(() => ({ ...schemaParser.getDefaultValues(), ...initialValues })); const [errors, setErrors] = useState([]); const [_isValidating, _setIsValidating] = useState(false); const [isSubmitting, _setIsSubmitting] = useState(false); const [touchedFields, setTouchedFields] = useState>(new Set()); const [expandedGroups, setExpandedGroups] = useState>(() => { const defaultExpanded = new Set(); if (options.progressiveDisclosure?.defaultExpanded) { options.progressiveDisclosure.defaultExpanded.forEach((group: string) => defaultExpanded.add(group) ); } if (options.progressiveDisclosure?.groups) { options.progressiveDisclosure.groups .filter((group: FormGroup) => group.priority === 'essential' || group.defaultExpanded) .forEach((group: FormGroup) => defaultExpanded.add(group.name)); } return defaultExpanded; }); const isDirty = useMemo(() => { const defaultValues = schemaParser.getDefaultValues(); return JSON.stringify(values) !== JSON.stringify({ ...defaultValues, ...initialValues }); }, [values, schemaParser, initialValues]); const setValue = useCallback((path: string[], value: unknown) => { const pathKey = path.join('.'); setValues(prev => { const newValues = { ...prev }; let current = newValues; for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in current)) { current[path[i]] = {}; } current = current[path[i]] as Record; } current[path[path.length - 1]] = value; return newValues; }); setErrors(prev => prev.filter(error => error.path.join('.') !== pathKey)); setTouchedFields(prev => new Set([...prev, pathKey])); onValueChange?.(path, value); }, [onValueChange]); const _setFieldValues = useCallback((newValues: Partial) => { setValues(prev => ({ ...prev, ...newValues })); }, []); const _setError = useCallback((path: string[], message: string) => { const pathKey = path.join('.'); setErrors(prev => [ ...prev.filter(error => error.path.join('.') !== pathKey), { path, message, code: 'custom' } ]); }, []); const _clearError = useCallback((path: string[]) => { const pathKey = path.join('.'); setErrors(prev => prev.filter(error => error.path.join('.') !== pathKey)); }, []); const _clearErrors = useCallback(() => { setErrors([]); }, []); const reset = useCallback(() => { const defaultValues = schemaParser.getDefaultValues(); setValues({ ...defaultValues, ...initialValues }); setErrors([]); setTouchedFields(new Set()); }, [schemaParser, initialValues]); const validate = useCallback(async (): Promise => { _setIsValidating(true); try { schema.parse(values); setErrors([]); return true; } catch (error) { if (error instanceof ZodError) { const validationErrors: ValidationError[] = error.issues.map((err: ZodIssue) => ({ path: err.path.map(String), message: err.message, code: err.code })); setErrors(validationErrors); } return false; } finally { _setIsValidating(false); } }, [schema, values]); const submit = useCallback(async () => { if (!onSubmit) return; _setIsSubmitting(true); try { const isValid = await validate(); if (isValid) { await onSubmit(values as TSchema); } } catch (error) { logger.error('Form submission error:', error); } finally { _setIsSubmitting(false); } }, [onSubmit, validate, values, logger]); const touchField = useCallback((path: string[]) => { const pathKey = path.join('.'); setTouchedFields(prev => new Set([...prev, pathKey])); }, []); const toggleGroup = useCallback((groupName: string) => { setExpandedGroups(prev => { const newSet = new Set(prev); if (newSet.has(groupName)) { newSet.delete(groupName); } else { newSet.add(groupName); } return newSet; }); }, []); const getFieldsByGroup = useCallback(() => { const groups: Record = {}; const ungrouped: typeof parsedSchema.fields = {}; Object.entries(parsedSchema.fields).forEach(([fieldName, fieldConfig]) => { const groupName = fieldConfig.progressive?.group || 'ungrouped'; if (groupName === 'ungrouped') { ungrouped[fieldName] = fieldConfig; } else { if (!groups[groupName]) { groups[groupName] = {}; } groups[groupName][fieldName] = fieldConfig; } }); return { groups, ungrouped }; }, [parsedSchema]); const { groups, ungrouped } = getFieldsByGroup(); const progressiveConfig = options.progressiveDisclosure; return ( {/* Form Header */} {(title || description) && (
{title && ( {title} )} {description && ( {description} )}
)} {/* Validation Errors */} {errors.length > 0 && (
{errors.map((error, index) => (
{error.path.join('.')}: {error.message}
))}
)} {/* Form Fields */}
{/* Ungrouped Fields (Essential/Common priority) */} {Object.keys(ungrouped).length > 0 && (
{Object.entries(ungrouped).map(([fieldName, fieldConfig]) => ( setValue([fieldName], value)} error={errors.find(err => err.path[0] === fieldName)?.message} disabled={isSubmitting} touched={touchedFields.has(fieldName)} onTouch={() => touchField([fieldName])} /> ))}
)} {/* Progressive Disclosure Groups */} {progressiveConfig?.enabled !== false && Object.entries(groups).map(([groupName, groupFields]) => { const groupConfig = progressiveConfig?.groups?.find((g: FormGroup) => g.name === groupName); const isExpanded = expandedGroups.has(groupName); const fieldCount = Object.keys(groupFields).length; const priority = groupConfig?.priority || 'common'; if (priority === 'essential') { return (
{groupConfig?.description && ( {groupConfig.description} )} {Object.entries(groupFields).map(([fieldName, fieldConfig]) => ( setValue([fieldName], value)} error={errors.find(err => err.path[0] === fieldName)?.message} disabled={isSubmitting} touched={touchedFields.has(fieldName)} onTouch={() => touchField([fieldName])} /> ))}
); } return (
{/* Group Header */} {/* Group Content */} {isExpanded && (
{groupConfig?.description && ( {groupConfig.description} )} {Object.entries(groupFields).map(([fieldName, fieldConfig]) => ( setValue([fieldName], value)} error={errors.find(err => err.path[0] === fieldName)?.message} disabled={isSubmitting} touched={touchedFields.has(fieldName)} onTouch={() => touchField([fieldName])} /> ))}
)}
); })}
{/* Form Actions */} {(options.showSubmitButton !== false || options.showResetButton) && (
{options.showResetButton && ( )} {options.showSubmitButton !== false && onSubmit && ( )}
)}
); } /** * Hook for using form state and actions separately */ export function useFormGenerator>( schema: ZodType, initialValues: Partial = {} ): UseFormReturn { const schemaParser = useMemo(() => new SchemaParser(schema), [schema]); const [values, setValues] = useState>(() => ({ ...schemaParser.getDefaultValues(), ...initialValues })); const [errors, setErrors] = useState([]); const [_isValidating, _setIsValidating] = useState(false); const [isSubmitting, _setIsSubmitting] = useState(false); const [touchedFields, setTouchedFields] = useState>(new Set()); const isDirty = useMemo(() => { const defaultValues = schemaParser.getDefaultValues(); return JSON.stringify(values) !== JSON.stringify({ ...defaultValues, ...initialValues }); }, [values, schemaParser, initialValues]); const setValue = useCallback((path: string[], value: unknown) => { const pathKey = path.join('.'); setValues(prev => { const newValues = { ...prev }; let current = newValues; for (let i = 0; i < path.length - 1; i++) { if (!(path[i] in current)) { current[path[i]] = {}; } current = current[path[i]] as Record; } current[path[path.length - 1]] = value; return newValues; }); setErrors(prev => prev.filter(error => error.path.join('.') !== pathKey)); setTouchedFields(prev => new Set([...prev, pathKey])); }, []); const formState: FormState = { values: values as TSchema, errors, isValidating: _isValidating, isSubmitting, isDirty, touchedFields }; const formActions: FormActions = { setValue, setValues: (newValues) => setValues(prev => ({ ...prev, ...newValues })), setError: (path, message) => setErrors(prev => [...prev, { path, message, code: 'custom' }]), clearError: (path) => setErrors(prev => prev.filter(e => e.path.join('.') !== path.join('.'))), clearErrors: () => setErrors([]), reset: () => { const defaultValues = schemaParser.getDefaultValues(); setValues({ ...defaultValues, ...initialValues }); setErrors([]); setTouchedFields(new Set()); }, submit: async () => { try { schema.parse(values); setErrors([]); } catch (error) { if (error instanceof ZodError) { setErrors(error.issues.map((err: ZodIssue) => ({ path: err.path.map(String), message: err.message, code: err.code }))); } } }, validate: async () => { try { schema.parse(values); setErrors([]); return true; } catch { return false; } }, touchField: (path) => setTouchedFields(prev => new Set([...prev, path.join('.')])) }; return { state: formState, actions: formActions }; }