import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { createErrorHandler } from '@openmrs/esm-framework'; import type { FormField, FormFieldInputProps, RenderType } from '../../types'; import { FormFieldRenderer } from '../renderer/field/form-field-renderer.component'; import { clearSubmission, isViewMode } from '../../utils/common-utils'; import { cloneRepeatField } from './helpers'; import { evaluateAsyncExpression, evaluateExpression } from '../../utils/expression-runner'; import { isEmpty } from '../../validators/form-validator'; import { useFormFactory } from '../../provider/form-factory-provider'; import { useFormProviderContext } from '../../provider/form-provider'; import RepeatControls from './repeat-controls.component'; import styles from './repeat.scss'; const renderingByTypeMap: Record = { obsGroup: 'group', testOrder: 'select', diagnosis: 'ui-select-extended', }; const Repeat: React.FC = ({ field }) => { const [rows, setRows] = useState([]); const context = useFormProviderContext(); const { handleConfirmQuestionDeletion } = useFormFactory(); const { patient, sessionMode, formFieldAdapters, formFields, methods: { getValues, setValue, unregister }, addFormField, removeFormField, deletedFields, setDeletedFields, visit, } = context; useEffect(() => { const repeatedFields = formFields.filter( (_field) => _field.questionOptions.concept === field.questionOptions.concept && _field.id.startsWith(field.id) && !_field.meta?.repeat?.wasDeleted, ); setRows(repeatedFields); }, [formFields, field]); const handleAdd = useCallback( (counter: number) => { const clonedFieldsBuffer: FormField[] = []; function evaluateExpressions(field: FormField) { if (field.hide?.hideWhenExpression) { field.isHidden = evaluateExpression( field.hide.hideWhenExpression, { value: field, type: 'field' }, [...formFields, ...clonedFieldsBuffer], getValues(), { mode: sessionMode, patient: patient, visit, }, ); } if (field.questionOptions.calculate?.calculateExpression) { evaluateAsyncExpression( field.questionOptions.calculate?.calculateExpression, { value: field, type: 'field' }, [...formFields, ...clonedFieldsBuffer], getValues(), { mode: sessionMode, patient: patient, visit, }, ).then((result) => { if (!isEmpty(result)) { setValue(field.id, result); formFieldAdapters[field.type]?.transformFieldValue(field, result, context); } }); } } const clonedField = cloneRepeatField(field, null, counter); clonedFieldsBuffer.push(clonedField); // Handle nested questions if (clonedField.type === 'obsGroup') { clonedField.questions?.forEach((childField) => { clonedFieldsBuffer.push(childField); }); } clonedFieldsBuffer.forEach((field) => { evaluateExpressions(field); addFormField(field); }); setRows([...rows, clonedField]); }, [formFields, field, rows, context], ); const removeNthRow = (field: FormField) => { if (field.meta.initialValue?.omrsObject) { formFieldAdapters[field.type]?.transformFieldValue(field, null, context); field.meta.repeat = { ...(field.meta.repeat || {}), wasDeleted: true }; if (field.type === 'obsGroup') { field.questions.forEach((child) => { child.meta.repeat = { ...(field.meta.repeat || {}), wasDeleted: true }; formFieldAdapters[child.type]?.transformFieldValue(child, null, context); }); } } else { clearSubmission(field); // Unregister from react-hook-form so a re-added row with the same id // starts empty instead of rehydrating the deleted row's value. unregister(field.id); field.questions?.forEach((child) => unregister(child.id)); } setRows(rows.filter((q) => q.id !== field.id)); setDeletedFields([...deletedFields, field]); removeFormField(field.id); }; const onClickDeleteQuestion = (field: Readonly) => { if (handleConfirmQuestionDeletion && typeof handleConfirmQuestionDeletion === 'function') { const result = handleConfirmQuestionDeletion(field); if (result && typeof result.then === 'function' && typeof result.catch === 'function') { result.then(() => removeNthRow(field)).catch(() => createErrorHandler()); } else if (typeof result === 'boolean') { result && removeNthRow(field); } else { removeNthRow(field); } } else { removeNthRow(field); } }; const nodes = useMemo(() => { return rows.map((row, index) => { const component = ( ); return (
{index !== 0 && (

)}
{component}
{!isViewMode(sessionMode) && ( { onClickDeleteQuestion(row); }} handleAdd={() => { // Use max-existing-suffix + 1 so new rows don't collide with a surviving // row after a middle row is deleted (the buggy `length - 1 + 1` would). const nextSuffix = rows.reduce((max, r) => { const suffix = Number(r.id.slice(field.id.length + 1)); return Number.isFinite(suffix) ? Math.max(max, suffix) : max; }, 0) + 1; handleAdd(nextSuffix); }} /> )}
); }); }, [rows]); if (field.isHidden || !nodes || !hasVisibleField(field)) { return null; } return (
{nodes}
); }; function hasVisibleField(field: FormField) { if (field.questions?.length) { return field.questions?.some((child) => !child.isHidden); } return !field.isHidden; } function getQuestionWithSupportedRendering(field: FormField) { return { ...field, questionOptions: { ...field.questionOptions, rendering: renderingByTypeMap[field.type] || null, }, }; } export default Repeat;