From d2ac5d6d2f8717b1376146e402fd80be6fd1668f Mon Sep 17 00:00:00 2001 From: Vadim Laletin Date: Thu, 5 Dec 2024 12:51:02 +0100 Subject: [PATCH] Fix validation for single/multiple choice/reference. Closes #392 (#397) * Fix validation for single/multiple choice/reference. Closes #392 * Add test cases for complex type validation. Refs #392 --- .../BaseQuestionnaireResponseForm/hooks.tsx | 4 + .../widgets/ReferenceRadioButton/index.tsx | 4 +- .../widgets/choice/index.tsx | 4 +- .../widgets/reference.tsx | 44 +---- .../complex-type-validation.test.ts | 166 ++++++++++++++++++ .../__tests__/questionnaire/reference.test.ts | 90 ---------- src/utils/questionnaire.ts | 14 +- 7 files changed, 183 insertions(+), 143 deletions(-) create mode 100644 src/utils/__tests__/questionnaire/complex-type-validation.test.ts delete mode 100644 src/utils/__tests__/questionnaire/reference.test.ts diff --git a/src/components/BaseQuestionnaireResponseForm/hooks.tsx b/src/components/BaseQuestionnaireResponseForm/hooks.tsx index b05a3d46..5b6c174d 100644 --- a/src/components/BaseQuestionnaireResponseForm/hooks.tsx +++ b/src/components/BaseQuestionnaireResponseForm/hooks.tsx @@ -55,9 +55,13 @@ export function useFieldController(fieldName: any, questionItem: QuestionnaireIt [repeats, field], ); + // This is a wrapper for react-select that always wrap single value into array + const onSelect = useCallback((option: any) => field.onChange([].concat(option)), [field]); + return { ...field, onMultiChange, + onSelect, fieldState, disabled: readOnly || qrfContext.readOnly, formItem, diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/ReferenceRadioButton/index.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/ReferenceRadioButton/index.tsx index 41c7fd09..7fd12b9a 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/ReferenceRadioButton/index.tsx +++ b/src/components/BaseQuestionnaireResponseForm/widgets/ReferenceRadioButton/index.tsx @@ -18,7 +18,7 @@ function ReferenceRadioButtonUnsafe onChange(answerOption)} + onChange={() => onSelect(answerOption)} data-testid={`inline-choice__${_.kebabCase( JSON.stringify(getDisplay(answerOption.value)), )}`} diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/choice/index.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/choice/index.tsx index c7b131c4..9c1c21d8 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/choice/index.tsx +++ b/src/components/BaseQuestionnaireResponseForm/widgets/choice/index.tsx @@ -52,9 +52,7 @@ export function QuestionChoice({ parentPath, questionItem }: QuestionItemProps) const { linkId, answerOption, repeats, answerValueSet, choiceColumn } = questionItem; const fieldName = [...parentPath, linkId]; - const { value, formItem, onChange, placeholder = t`Select...` } = useFieldController(fieldName, questionItem); - - const onSelect = useCallback((option: any) => onChange([].concat(option)), [onChange]); + const { value, formItem, onSelect, placeholder = t`Select...` } = useFieldController(fieldName, questionItem); if (answerValueSet) { return ( diff --git a/src/components/BaseQuestionnaireResponseForm/widgets/reference.tsx b/src/components/BaseQuestionnaireResponseForm/widgets/reference.tsx index 96538403..6a029ef1 100644 --- a/src/components/BaseQuestionnaireResponseForm/widgets/reference.tsx +++ b/src/components/BaseQuestionnaireResponseForm/widgets/reference.tsx @@ -97,10 +97,9 @@ export function useAnswerReference) { - const { linkId, repeats, required, answerExpression, choiceColumn, text, entryFormat, referenceResource } = - questionItem; + const { linkId, repeats, answerExpression, choiceColumn, text, entryFormat, referenceResource } = questionItem; const rootFieldPath = [...parentPath, linkId]; - const fieldPath = [...rootFieldPath, ...(repeats ? [] : ['0'])]; + const fieldPath = [...rootFieldPath]; const rootFieldName = rootFieldPath.join('.'); const fieldName = fieldPath.join('.'); @@ -115,9 +114,10 @@ export function useAnswerReference { - return parseFhirQueryExpression(answerExpression!.expression!, context); - }, [answerExpression?.expression!, context]); + return parseFhirQueryExpression(expression, context); + }, [expression, context]); const loadOptions = useCallback( async (searchText: string) => { @@ -157,31 +157,6 @@ export function useAnswerReference | MultiValue, - action: ActionMeta, - ) => { - if (!repeats || action.action !== 'select-option') { - return; - } - }; - - const validate = required - ? (inputValue: any) => { - if (repeats) { - if (!inputValue || !inputValue.length) { - return 'Choose at least one option'; - } - } else { - if (!inputValue) { - return 'Required'; - } - } - - return undefined; - } - : undefined; - const depsUrl = `${resourceType}?${buildQueryParams(searchParams as any)}`; const deps = [linkId, depsUrl]; @@ -190,8 +165,6 @@ export function useAnswerReference, ) { const { debouncedLoadOptions, fieldController, repeats, placeholder, optionsRD } = useAnswerReference(props); - const { formItem } = fieldController; + + const { formItem, onSelect } = fieldController; return ( {(options) => ( getAnswerDisplay(option.value)} diff --git a/src/utils/__tests__/questionnaire/complex-type-validation.test.ts b/src/utils/__tests__/questionnaire/complex-type-validation.test.ts new file mode 100644 index 00000000..b856a603 --- /dev/null +++ b/src/utils/__tests__/questionnaire/complex-type-validation.test.ts @@ -0,0 +1,166 @@ +import { Questionnaire, QuestionnaireResponseItemAnswer } from '@beda.software/aidbox-types'; + +import { questionnaireToValidationSchema } from 'src/utils'; + +type QuestionnaireData = { + questionnaire: Questionnaire; + answer: Record; + success: boolean; +}; + +const QUESTIONNAIRES_TEST_DATA: QuestionnaireData[] = [ + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-required', + title: 'Reference required', + status: 'active', + item: [ + { + linkId: 'reference-required-filled', + type: 'reference', + text: 'Reference required', + repeats: true, + required: true, + }, + ], + }, + answer: { + 'reference-required-filled': [ + { + value: { + Reference: { + resourceType: 'Patient', + id: '1', + display: 'Patient 1', + }, + }, + }, + ], + }, + success: true, + }, + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-required', + title: 'Reference required', + status: 'active', + item: [ + { + linkId: 'reference-required-filled', + type: 'reference', + text: 'Reference required', + repeats: false, + required: true, + }, + ], + }, + answer: { + 'reference-required-filled': undefined, + }, + success: false, + }, + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-required', + title: 'Reference required', + status: 'active', + item: [ + { + linkId: 'reference-required-filled', + type: 'reference', + text: 'Reference required', + repeats: true, + required: true, + }, + ], + }, + answer: { + 'reference-required-filled': [], + }, + success: false, + }, + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-optional', + title: 'Reference optional', + status: 'active', + item: [ + { + linkId: 'reference-optional-filled', + type: 'reference', + text: 'Reference optional', + repeats: true, + required: false, + }, + ], + }, + answer: { + 'reference-optional-filled': [], + }, + success: true, + }, + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-required', + title: 'Reference required', + status: 'active', + item: [ + { + linkId: 'reference-required-filled', + type: 'reference', + text: 'Reference required', + repeats: false, + required: false, + }, + ], + }, + answer: { + 'reference-required-filled': undefined, + }, + success: true, + }, + { + questionnaire: { + resourceType: 'Questionnaire', + id: 'reference-required', + title: 'Reference required', + status: 'active', + item: [ + { + linkId: 'reference-required-filled', + type: 'reference', + text: 'Reference required', + repeats: false, + required: false, + }, + ], + }, + answer: { + 'reference-required-filled': [ + { + value: { + Reference: { + resourceType: 'Patient', + id: '1', + display: 'Patient 1', + }, + }, + }, + ], + }, + success: true, + }, +]; + +describe('Complex type validation', () => { + test.each(QUESTIONNAIRES_TEST_DATA)('should return the correct result', async (questionnaireData) => { + const validationSchema = questionnaireToValidationSchema(questionnaireData.questionnaire); + const isValid = await validationSchema.isValid(questionnaireData.answer); + expect(isValid).toBe(questionnaireData.success); + }); +}); diff --git a/src/utils/__tests__/questionnaire/reference.test.ts b/src/utils/__tests__/questionnaire/reference.test.ts deleted file mode 100644 index a12f7e76..00000000 --- a/src/utils/__tests__/questionnaire/reference.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Questionnaire } from '@beda.software/aidbox-types'; - -import { questionnaireToValidationSchema } from 'src/utils'; - -type QuestionnaireData = { - questionnaire: Questionnaire; - answer: any; - success: boolean; -}; - -const REFERENCE_QUESTIONAIRES: QuestionnaireData[] = [ - { - questionnaire: { - resourceType: 'Questionnaire', - id: 'reference-required', - title: 'Reference required', - status: 'active', - item: [ - { - linkId: 'reference-required-filed', - type: 'reference', - text: 'Reference required', - required: true, - }, - ], - }, - answer: { - 'reference-required-filed': [ - { - value: { - Reference: { - resourceType: 'Patient', - id: '1', - display: 'Patient 1', - }, - }, - }, - ], - }, - success: true, - }, - { - questionnaire: { - resourceType: 'Questionnaire', - id: 'reference-optional', - title: 'Reference optional', - status: 'active', - item: [ - { - linkId: 'reference-optional-filed', - type: 'reference', - text: 'Reference optional', - required: false, - }, - ], - }, - answer: { - 'reference-optional-filed': [undefined], - }, - success: true, - }, - { - questionnaire: { - resourceType: 'Questionnaire', - id: 'reference-required', - title: 'Reference required', - status: 'active', - item: [ - { - linkId: 'reference-required-filed', - type: 'reference', - text: 'Reference required', - required: true, - }, - ], - }, - answer: { - 'reference-required-filed': [undefined], - }, - success: false, - }, -]; - -describe('Reference', () => { - test.each(REFERENCE_QUESTIONAIRES)('should return the correct reference', async (questionnaireData) => { - const validationSchema = questionnaireToValidationSchema(questionnaireData.questionnaire); - const isValid = await validationSchema.isValid(questionnaireData.answer); - expect(isValid).toBe(questionnaireData.success); - }); -}); diff --git a/src/utils/questionnaire.ts b/src/utils/questionnaire.ts index 03330608..3071674a 100644 --- a/src/utils/questionnaire.ts +++ b/src/utils/questionnaire.ts @@ -97,20 +97,8 @@ export function questionnaireItemsToValidationSchema(questionnaireItems: Questio schema = yup.date(); if (item.required) schema = schema.required(); schema = createSchemaArrayOfValues(yup.object({ date: schema })).required(); - } else if (item.type === 'reference') { - schema = yup.object({ - resourceType: yup.string().required(), - display: yup.string().nullable(), - id: yup.string().required(), - }); - - if (item.required) { - schema = createSchemaArrayOfValues(yup.object({ Reference: schema })).required(); - } else { - schema = yup.mixed().nullable(); - } } else { - schema = item.required ? yup.mixed().required() : yup.mixed().nullable(); + schema = item.required ? yup.array().of(yup.mixed()).min(1).required() : yup.mixed().nullable(); } if (item.enableWhen) { item.enableWhen.forEach((itemEnableWhen) => {