import React, { useEffect, useMemo, useState } from 'react'; import { ToastNotification } from '@carbon/react'; import { Controller, useWatch } from 'react-hook-form'; import { useTranslation } from 'react-i18next'; import { ErrorBoundary } from 'react-error-boundary'; import { useConfig } from '@openmrs/esm-framework'; import { type FormField, type FormFieldInputProps, type FormFieldValueAdapter, type RenderType, type ValidationResult, type ValueAndDisplay, } from '../../../types'; import { getFieldControlWithFallback, getRegisteredControl } from '../../../registry/registry'; import { handleFieldLogic, validateFieldValue } from './fieldLogic'; import { hasRendering } from '../../../utils/common-utils'; import { isEmpty } from '../../../validators/form-validator'; import { isTrue } from '../../../utils/boolean-utils'; import { useFormProviderContext } from '../../../provider/form-provider'; import PreviousValueReview from '../../previous-value-review/previous-value-review.component'; import UnspecifiedField from '../../inputs/unspecified/unspecified.component'; import { shouldRenderField } from './fieldRenderUtils'; import styles from './form-field-renderer.scss'; export interface FormFieldRendererProps { fieldId: string; valueAdapter: FormFieldValueAdapter; repeatOptions?: { targetRendering: RenderType; }; } export const FormFieldRenderer = ({ fieldId, valueAdapter, repeatOptions }: FormFieldRendererProps) => { const [inputComponentWrapper, setInputComponentWrapper] = useState<{ value: React.ComponentType; }>(null); const [errors, setErrors] = useState([]); const [warnings, setWarnings] = useState([]); const [historicalValue, setHistoricalValue] = useState(null); const context = useFormProviderContext(); // Try to get config from external module, fallback to default if not available let hideUnansweredQuestionsInReadonlyForms = false; try { const config = useConfig({ externalModuleName: '@openmrs/esm-form-engine-app', }); hideUnansweredQuestionsInReadonlyForms = config?.hideUnansweredQuestionsInReadonlyForms ?? false; } catch (error) { // If external module config is not available, use default value console.warn( 'Failed to load @openmrs/esm-form engine-app config - using hideUnansweredQuestionsInReadonlyForms=false (empty fields will be visible in readonly mode): ', error, ); } const { methods: { control, getValues, getFieldState }, patient, sessionMode, formFields, formFieldValidators, addInvalidField, removeInvalidField, updateFormField, } = context; const fieldValue = useWatch({ control, name: fieldId, exact: true }); const noop = () => {}; const field = useMemo(() => formFields.find((field) => field.id === fieldId), [fieldId, formFields]); useEffect(() => { if (hasRendering(field, 'repeating') && repeatOptions?.targetRendering) { getRegisteredControl(repeatOptions.targetRendering).then((component) => { if (component) { setInputComponentWrapper({ value: component }); } }); } else { getFieldControlWithFallback(field).then((component) => { if (component) { setInputComponentWrapper({ value: component }); } }); } if (sessionMode === 'enter' && (field.historicalExpression || context.previousDomainObjectValue)) { try { context.processor.getHistoricalValue(field, context).then((value) => { setHistoricalValue(value); }); } catch (error) { console.error(error); } } }, []); useEffect(() => { const { isDirty, isTouched } = getFieldState(field.id); const { submission, unspecified } = field.meta; const { calculate, defaultValue } = field.questionOptions; if ( !isEmpty(fieldValue) && !submission?.newValue && !isDirty && !unspecified && (calculate?.calculateExpression || defaultValue) ) { valueAdapter.transformFieldValue(field, fieldValue, context); } if (isDirty || isTouched) { onAfterChange(fieldValue); } }, [fieldValue]); useEffect(() => { if (field.meta.submission?.errors) { setErrors(field.meta.submission.errors); } if (field.meta.submission?.warnings) { setWarnings(field.meta.submission.warnings); } if (field.meta.submission?.unspecified) { setErrors([]); removeInvalidField(field.id); } }, [field.meta.submission]); const onAfterChange = (value: any) => { const { errors: validationErrors, warnings: validationWarnings } = validateFieldValue( field, value, formFieldValidators, { formFields: formFields, values: getValues(), expressionContext: { patient, mode: sessionMode }, }, ); if (field.meta.submission) { // clear stale submission validation results field.meta.submission.errors = undefined; field.meta.submission.warnings = undefined; } if (errors.length && !validationErrors.length) { removeInvalidField(field.id); setErrors([]); } else if (validationErrors.length) { setErrors(validationErrors); addInvalidField(field); } if (!validationErrors.length) { valueAdapter.transformFieldValue(field, value, context); } setWarnings(validationWarnings); handleFieldLogic(field, context); if (field.meta.groupId) { const group = formFields.find((f) => f.id === field.meta.groupId); if (group) { group.questions = group.questions.map((child) => { if (child.id === field.id) { return field; } return child; }); updateFormField(group); } } }; if (!inputComponentWrapper) { return null; } const InputComponent = inputComponentWrapper.value; if (!repeatOptions?.targetRendering && isGroupField(field.questionOptions.rendering)) { return ( ); } // In 'embedded-view' mode, empty fields are hidden if they are transient // or if the config flag `hideUnansweredQuestionsInReadonlyForms` is enabled. if ( !shouldRenderField( sessionMode, !!field.questionOptions.isTransient, isEmpty(fieldValue), hideUnansweredQuestionsInReadonlyForms, ) ) { return null; } return ( (
{ onChange(val); onAfterChange(val); onBlur(); }} /> {isUnspecifiedSupported(field) && (
{field.unspecified && ( )}
)} {historicalValue?.value && (
)}
)} />
); }; export function ErrorFallback({ error }) { const { t } = useTranslation(); return ( ); } /** * Determines whether a field can be unspecified */ export function isUnspecifiedSupported(question: FormField) { const { rendering } = question.questionOptions; return ( isTrue(question.unspecified) && rendering != 'toggle' && rendering != 'group' && rendering != 'repeating' && rendering != 'markdown' && rendering != 'extension-widget' && rendering != 'workspace-launcher' ); } export function isGroupField(rendering: RenderType) { return rendering === 'group' || rendering === 'repeating'; }