Skip to content

Commit

Permalink
Fix validation for single/multiple choice/reference. Closes #392 (#397)
Browse files Browse the repository at this point in the history
* Fix validation for single/multiple choice/reference. Closes #392

* Add test cases for complex type validation. Refs #392
  • Loading branch information
ruscoder authored Dec 5, 2024
1 parent 6a7f83a commit d2ac5d6
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 143 deletions.
4 changes: 4 additions & 0 deletions src/components/BaseQuestionnaireResponseForm/hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function ReferenceRadioButtonUnsafe<R extends Resource = any, IR extends Resourc

const { optionsRD, fieldController } = useAnswerReference(props);

const { formItem, value, onChange, disabled } = fieldController;
const { formItem, value, onSelect, disabled } = fieldController;

return (
<RenderRemoteData
Expand All @@ -34,7 +34,7 @@ function ReferenceRadioButtonUnsafe<R extends Resource = any, IR extends Resourc
key={JSON.stringify(answerOption)}
checked={_.isEqual(value?.value, answerOption.value)}
disabled={disabled}
onChange={() => onChange(answerOption)}
onChange={() => onSelect(answerOption)}
data-testid={`inline-choice__${_.kebabCase(
JSON.stringify(getDisplay(answerOption.value)),
)}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
44 changes: 9 additions & 35 deletions src/components/BaseQuestionnaireResponseForm/widgets/reference.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,9 @@ export function useAnswerReference<R extends Resource = any, IR extends Resource
context,
overrideGetDisplay,
}: AnswerReferenceProps<R, IR>) {
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('.');
Expand All @@ -115,9 +114,10 @@ export function useAnswerReference<R extends Resource = any, IR extends Resource
}, [choiceColumn, context, overrideGetDisplay]);

// TODO: add support for fhirpath and application/x-fhir-query
const expression = answerExpression!.expression!;
const [resourceType, searchParams] = useMemo(() => {
return parseFhirQueryExpression(answerExpression!.expression!, context);
}, [answerExpression?.expression!, context]);
return parseFhirQueryExpression(expression, context);
}, [expression, context]);

const loadOptions = useCallback(
async (searchText: string) => {
Expand Down Expand Up @@ -157,31 +157,6 @@ export function useAnswerReference<R extends Resource = any, IR extends Resource
return await loadOptions('');
}, [JSON.stringify(searchParams)]);

const onChange = (
_value: SingleValue<QuestionnaireItemAnswerOption> | MultiValue<QuestionnaireItemAnswerOption>,
action: ActionMeta<QuestionnaireItemAnswerOption>,
) => {
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];
Expand All @@ -190,8 +165,6 @@ export function useAnswerReference<R extends Resource = any, IR extends Resource
rootFieldName,
fieldName,
debouncedLoadOptions,
onChange,
validate,
loadOptions,
optionsRD,
searchParams,
Expand All @@ -208,15 +181,16 @@ function QuestionReferenceUnsafe<R extends Resource = any, IR extends Resource =
props: AnswerReferenceProps<R, IR>,
) {
const { debouncedLoadOptions, fieldController, repeats, placeholder, optionsRD } = useAnswerReference(props);
const { formItem } = fieldController;

const { formItem, onSelect } = fieldController;

return (
<RenderRemoteData remoteData={optionsRD}>
{(options) => (
<Form.Item {...formItem}>
<AsyncSelect
onChange={fieldController.onChange}
value={repeats ? fieldController.value : [fieldController.value]}
onChange={onSelect}
value={fieldController.value}
loadOptions={debouncedLoadOptions}
defaultOptions={options}
getOptionLabel={(option) => getAnswerDisplay(option.value)}
Expand Down
166 changes: 166 additions & 0 deletions src/utils/__tests__/questionnaire/complex-type-validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { Questionnaire, QuestionnaireResponseItemAnswer } from '@beda.software/aidbox-types';

import { questionnaireToValidationSchema } from 'src/utils';

type QuestionnaireData = {
questionnaire: Questionnaire;
answer: Record<string, QuestionnaireResponseItemAnswer[] | undefined>;
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);
});
});
90 changes: 0 additions & 90 deletions src/utils/__tests__/questionnaire/reference.test.ts

This file was deleted.

Loading

0 comments on commit d2ac5d6

Please sign in to comment.