Skip to content

Commit

Permalink
(fix) Coded person attribute field should report error if not configu…
Browse files Browse the repository at this point in the history
…red correctly (#914)
  • Loading branch information
brandones committed Jan 18, 2024
1 parent 5c6783d commit a356cf9
Show file tree
Hide file tree
Showing 14 changed files with 281 additions and 163 deletions.
3 changes: 2 additions & 1 deletion packages/esm-patient-registration-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,8 @@ export const esmPatientRegistrationSchema = {
},
},
_default: [],
_description: 'For coded questions only. Provide ability to add custom concept answers.',
_description:
'For coded questions only (obs or person attrbute). A list of custom concept answers. Overrides answers that come from the obs concept or from `answerSetConceptUuid`.',
},
},
// Do not add fields here. If you want to add a field in code, add it to built-in fields above.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import classNames from 'classnames';
import { Field } from 'formik';
import { useTranslation } from 'react-i18next';
Expand All @@ -15,9 +15,10 @@ export interface ObsFieldProps {
}

export function ObsField({ fieldDefinition }: ObsFieldProps) {
const { t } = useTranslation();
const { data: concept, isLoading } = useConcept(fieldDefinition.uuid);

const config = useConfig() as RegistrationConfig;
const config = useConfig<RegistrationConfig>();

if (!config.registrationObs.encounterTypeUuid) {
console.error(
Expand Down Expand Up @@ -57,12 +58,17 @@ export function ObsField({ fieldDefinition }: ObsFieldProps) {
answerConceptSetUuid={fieldDefinition.answerConceptSetUuid}
label={fieldDefinition.label}
required={fieldDefinition.validation.required}
customConceptAnswers={fieldDefinition.customConceptAnswers}
/>
);
default:
return (
<InlineNotification kind="error" title="Error">
Concept has unknown datatype "{concept.datatype.display}"
{t(
'obsFieldUnknownDatatype',
`Concept for obs field '{{fieldDefinitionId}}' has unknown datatype '{{datatypeName}}'`,
{ fieldDefinitionId: fieldDefinition.id, datatypeName: concept.datatype.display },
)}
</InlineNotification>
);
}
Expand Down Expand Up @@ -117,8 +123,6 @@ interface NumericObsFieldProps {
}

function NumericObsField({ concept, label, required }: NumericObsFieldProps) {
const { t } = useTranslation();

const fieldName = `obs.${concept.uuid}`;

return (
Expand Down Expand Up @@ -146,40 +150,32 @@ interface CodedObsFieldProps {
answerConceptSetUuid?: string;
label?: string;
required?: boolean;
customConceptAnswers: Array<{ uuid: string; label?: string }>;
}

function CodedObsField({ concept, answerConceptSetUuid, label, required }: CodedObsFieldProps) {
const config = useConfig() as RegistrationConfig;
function CodedObsField({ concept, answerConceptSetUuid, label, required, customConceptAnswers }: CodedObsFieldProps) {
const { t } = useTranslation();
const fieldName = `obs.${concept.uuid}`;

const { data: conceptAnswers, isLoading: isLoadingConceptAnswers } = useConceptAnswers(
answerConceptSetUuid ?? concept.uuid,
customConceptAnswers.length ? '' : answerConceptSetUuid ?? concept.uuid,
);

const fieldName = `obs.${concept.uuid}`;
const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'obs' && def.uuid === concept.uuid)[0];
const answers = useMemo(
() =>
customConceptAnswers.length
? customConceptAnswers
: isLoadingConceptAnswers
? []
: conceptAnswers.map((answer) => ({ ...answer, label: answer.display })),
[customConceptAnswers, conceptAnswers, isLoadingConceptAnswers],
);

return (
<div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
{!isLoadingConceptAnswers ? (
<Field name={fieldName}>
{({ field, form: { touched, errors }, meta }) => {
if (fieldDefinition?.customConceptAnswers?.length) {
return (
<Layer>
<Select
id={fieldName}
name={fieldName}
required={required}
labelText={label ?? concept?.display}
invalid={errors[fieldName] && touched[fieldName]}
{...field}>
<SelectItem key={`no-answer-select-item-${fieldName}`} value={''} text="" />
{fieldDefinition?.customConceptAnswers.map((answer) => (
<SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
))}
</Select>
</Layer>
);
}
return (
<Layer>
<Select
Expand All @@ -189,9 +185,13 @@ function CodedObsField({ concept, answerConceptSetUuid, label, required }: Coded
required={required}
invalid={errors[fieldName] && touched[fieldName]}
{...field}>
<SelectItem key={`no-answer-select-item-${fieldName}`} value={''} text="" />
{conceptAnswers.map((answer) => (
<SelectItem key={answer.uuid} value={answer.uuid} text={answer.display} />
<SelectItem
key={`no-answer-select-item-${fieldName}`}
value={''}
text={t('selectAnOption', 'Select an option')}
/>
{answers.map((answer) => (
<SelectItem key={answer.uuid} value={answer.uuid} text={answer.label} />
))}
</Select>
</Layer>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,66 +3,80 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useConfig } from '@openmrs/esm-framework';
import { type FieldDefinition } from '../../../config-schema';
import { useConcept, useConceptAnswers } from '../field.resource';
import { ObsField } from './obs-field.component';

const mockUseConfig = useConfig as jest.Mock;

jest.mock('../field.resource', () => ({
useConcept: jest.fn().mockImplementation((uuid: string) => {
let data;
if (uuid == 'weight-uuid') {
data = {
uuid: 'weight-uuid',
display: 'Weight (kg)',
datatype: { display: 'Numeric', uuid: 'num' },
answers: [],
setMembers: [],
};
} else if (uuid == 'chief-complaint-uuid') {
data = {
uuid: 'chief-complaint-uuid',
display: 'Chief Complaint',
datatype: { display: 'Text', uuid: 'txt' },
answers: [],
setMembers: [],
};
} else if (uuid == 'nationality-uuid') {
data = {
uuid: 'nationality-uuid',
display: 'Nationality',
datatype: { display: 'Coded', uuid: 'cdd' },
answers: [
{ display: 'USA', uuid: 'usa' },
{ display: 'Mexico', uuid: 'mex' },
],
setMembers: [],
};
}
jest.mock('../field.resource'); // Mock the useConceptAnswers hook

const mockedUseConcept = useConcept as jest.Mock;
const mockedUseConceptAnswers = useConceptAnswers as jest.Mock;

const useConceptMockImpl = (uuid: string) => {
let data;
if (uuid == 'weight-uuid') {
data = {
uuid: 'weight-uuid',
display: 'Weight (kg)',
datatype: { display: 'Numeric', uuid: 'num' },
answers: [],
setMembers: [],
};
} else if (uuid == 'chief-complaint-uuid') {
data = {
uuid: 'chief-complaint-uuid',
display: 'Chief Complaint',
datatype: { display: 'Text', uuid: 'txt' },
answers: [],
setMembers: [],
};
} else if (uuid == 'nationality-uuid') {
data = {
uuid: 'nationality-uuid',
display: 'Nationality',
datatype: { display: 'Coded', uuid: 'cdd' },
answers: [
{ display: 'USA', uuid: 'usa' },
{ display: 'Mexico', uuid: 'mex' },
],
setMembers: [],
};
} else {
throw Error(`Programming error, you probably didn't mean to do this: unknown concept uuid '${uuid}'`);
}
return {
data: data ?? null,
isLoading: !data,
};
};

const useConceptAnswersMockImpl = (uuid: string) => {
if (uuid == 'nationality-uuid') {
return {
data: data ?? null,
isLoading: !data,
data: [
{ display: 'USA', uuid: 'usa' },
{ display: 'Mexico', uuid: 'mex' },
],
isLoading: false,
};
}),
useConceptAnswers: jest.fn().mockImplementation((uuid: string) => {
if (uuid == 'nationality-uuid') {
return {
data: [
{ display: 'USA', uuid: 'usa' },
{ display: 'Mexico', uuid: 'mex' },
],
isLoading: false,
};
} else if (uuid == 'other-countries-uuid') {
return {
data: [
{ display: 'Kenya', uuid: 'ke' },
{ display: 'Uganda', uuid: 'ug' },
],
isLoading: false,
};
}
}),
}));
} else if (uuid == 'other-countries-uuid') {
return {
data: [
{ display: 'Kenya', uuid: 'ke' },
{ display: 'Uganda', uuid: 'ug' },
],
isLoading: false,
};
} else if (uuid == '') {
return {
data: [],
isLoading: false,
};
} else {
throw Error(`Programming error, you probably didn't mean to do this: unknown concept answer set uuid '${uuid}'`);
}
};

type FieldProps = {
children: ({ field, form: { touched, errors } }) => React.ReactNode;
Expand All @@ -86,12 +100,7 @@ const textFieldDef: FieldDefinition = {
matches: null,
},
answerConceptSetUuid: null,
customConceptAnswers: [
{
uuid: 'concept-uuid',
label: '',
},
],
customConceptAnswers: [],
};

const numberFieldDef: FieldDefinition = {
Expand All @@ -106,12 +115,7 @@ const numberFieldDef: FieldDefinition = {
matches: null,
},
answerConceptSetUuid: null,
customConceptAnswers: [
{
uuid: 'concept-uuid',
label: '',
},
],
customConceptAnswers: [],
};

const codedFieldDef: FieldDefinition = {
Expand All @@ -126,17 +130,14 @@ const codedFieldDef: FieldDefinition = {
matches: null,
},
answerConceptSetUuid: null,
customConceptAnswers: [
{
uuid: 'concept-uuid',
label: 'Kenya',
},
],
customConceptAnswers: [],
};

describe('ObsField', () => {
beforeEach(() => {
mockUseConfig.mockReturnValue({ registrationObs: { encounterTypeUuid: 'reg-enc-uuid' } });
mockedUseConcept.mockImplementation(useConceptMockImpl);
mockedUseConceptAnswers.mockImplementation(useConceptAnswersMockImpl);
});

it("logs an error and doesn't render if no registration encounter type is provided", () => {
Expand Down Expand Up @@ -167,15 +168,10 @@ describe('ObsField', () => {
// expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
expect(select).toHaveDisplayValue('');
expect(select).toHaveDisplayValue('Select an option');
});

it('select uses answerConcept for answers when it is provided', async () => {
mockUseConfig.mockReturnValue({
registrationObs: { encounterTypeUuid: 'reg-enc-uuid' },
fieldDefinitions: [codedFieldDef],
});

const user = userEvent.setup();

render(<ObsField fieldDefinition={{ ...codedFieldDef, answerConceptSetUuid: 'other-countries-uuid' }} />);
Expand All @@ -184,4 +180,26 @@ describe('ObsField', () => {
expect(select).toBeInTheDocument();
await user.selectOptions(select, 'Kenya');
});

it('select uses customConceptAnswers for answers when provided', async () => {
const user = userEvent.setup();

render(
<ObsField
fieldDefinition={{
...codedFieldDef,
customConceptAnswers: [
{
uuid: 'mozambique-uuid',
label: 'Mozambique',
},
],
}}
/>,
);
// expect(screen.getByLabelText("Nationality")).toBeInTheDocument();
const select = screen.getByRole('combobox');
expect(select).toBeInTheDocument();
await user.selectOptions(select, 'Mozambique');
});
});

0 comments on commit a356cf9

Please sign in to comment.