/** * DynamicField Component * * Renders form fields dynamically based on Frappe's field configuration. * Supports conditional visibility, mandatory, read-only states, and various field types. */ import React, { useMemo } from 'react'; import { type FieldConfig, evaluateFieldState, parseSelectOptions, getInputType } from '../utils/frappeExpressionEvaluator'; import LinkField from './LinkField'; interface DynamicFieldProps { fieldConfig: FieldConfig; value: any; onChange: (fieldname: string, value: any) => void; doc: Record; disabled?: boolean; compact?: boolean; className?: string; error?: string; } const DynamicField: React.FC = ({ fieldConfig, value, onChange, doc, disabled = false, compact = false, className = '', error }) => { // Evaluate field state based on current document const fieldState = useMemo(() => { return evaluateFieldState(fieldConfig, doc); }, [fieldConfig, doc]); // Don't render if field is not visible if (!fieldState.isVisible) { return null; } // Skip layout fields (Section Break, Column Break, Tab Break) if (['Section Break', 'Column Break', 'Tab Break', 'HTML'].includes(fieldConfig.fieldtype)) { return null; } const isDisabled = disabled || fieldState.isReadOnly; const isRequired = fieldState.isMandatory; const inputType = getInputType(fieldConfig.fieldtype); const labelClasses = compact ? 'block text-[10px] font-medium text-gray-700 dark:text-gray-300 mb-0.5' : 'block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1'; const inputClasses = compact ? `w-full px-2 py-1 text-xs border rounded focus:outline-none focus:ring-1 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white ${ isDisabled ? 'bg-gray-100 dark:bg-gray-800 cursor-not-allowed' : '' } ${error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}` : `w-full px-3 py-2 text-sm border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-white ${ isDisabled ? 'bg-gray-100 dark:bg-gray-800 cursor-not-allowed' : '' } ${error ? 'border-red-500' : 'border-gray-300 dark:border-gray-600'}`; const handleChange = (newValue: any) => { onChange(fieldConfig.fieldname, newValue); }; // Render based on field type const renderField = () => { switch (fieldConfig.fieldtype) { case 'Link': return ( ); case 'Select': const options = parseSelectOptions(fieldConfig.options); return (
{error &&

{error}

} {fieldConfig.description && !error && (

{fieldConfig.description}

)}
); case 'Check': return (
handleChange(e.target.checked ? 1 : 0)} disabled={isDisabled} className="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600" />
); case 'Date': return (
handleChange(e.target.value)} disabled={isDisabled} className={inputClasses} /> {error &&

{error}

}
); case 'Datetime': return (
handleChange(e.target.value.replace('T', ' '))} disabled={isDisabled} className={inputClasses} /> {error &&

{error}

}
); case 'Int': return (
handleChange(e.target.value ? parseInt(e.target.value) : null)} disabled={isDisabled} className={inputClasses} step="1" /> {error &&

{error}

}
); case 'Float': case 'Currency': case 'Percent': return (
handleChange(e.target.value ? parseFloat(e.target.value) : null)} disabled={isDisabled} className={inputClasses} step="0.01" /> {error &&

{error}

}
); case 'Small Text': case 'Text': case 'Long Text': return (