Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow multiple reasons for test #91

Merged
merged 10 commits into from
Nov 14, 2022
495 changes: 462 additions & 33 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@types/node": "^16.11.27",
"@types/react": "^18.0.6",
"@types/react-dom": "^18.0.2",
"@types/react-select": "^5.0.1",
"buffer": "^6.0.3",
"fhirclient": "^2.4.0",
"formik": "^2.2.9",
Expand All @@ -19,6 +20,7 @@
"react-dom": "^18.2.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.1",
"react-select": "^5.6.0",
"typescript": "^4.6.3",
"web-vitals": "^2.1.4",
"yup": "^0.32.11"
Expand Down
48 changes: 48 additions & 0 deletions src/components/reports/CustomSelectField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { FieldProps } from "formik";
import Select from "react-select";

interface Option {
label: string;
value: string;
}

interface Props extends FieldProps {
options: Option[];
isMulti: boolean;
className?: string;
placeholder?: string;
}

const CustomSelectField = ({ className, placeholder, field, form, options, isMulti = false }: Props) => {
const onChange = (option: Option | Option[]) => {
form.setFieldValue(
field.name,
isMulti ? (option as Option[]).map((item: Option) => item.value) : (option as Option).value,
);
};

const getValue = () => {
if (options) {
return isMulti
? options.filter((option: any) => field?.value?.indexOf(option.value) >= 0)
: options.find((option: any) => option.value === field.value);
} else {
return isMulti ? [] : ("" as any);
}
};

return (
<Select
inputId={field.name}
className={className}
name={field.name}
value={getValue()}
onChange={onChange}
placeholder={placeholder}
options={options}
isMulti={isMulti}
/>
);
};

export default CustomSelectField;
19 changes: 18 additions & 1 deletion src/components/reports/FieldSet.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ErrorMessage, Field } from "formik";
import { ChangeEventHandler, FC } from "react";
import { RequiredCoding } from "../../code_systems/types";
import CustomSelectField from "./CustomSelectField";

type Props = {
name: string;
Expand All @@ -10,17 +11,19 @@ type Props = {
selectOptions?: RequiredCoding[];
disabled?: boolean;
onChange?: ChangeEventHandler<HTMLInputElement>;
isMulti?: boolean;
};

/**
* Field Set component to wrap around Formik input fields.
* @param name html id and name of the field
* @param label label to display to the user
* @param selectOptions if present, will display as a select drop-down with these options
* @param isMulti if true, then multiple-selection from a drop-down
* @param rest any other props to pass though to Formik
* @constructor
*/
const FieldSet: FC<Props> = ({ name, label, selectOptions, ...rest }) => {
const FieldSet: FC<Props> = ({ name, label, selectOptions, isMulti, ...rest }) => {
let field: JSX.Element = <Field id={name} name={name} {...rest} />;

if (selectOptions !== undefined) {
Expand All @@ -37,6 +40,20 @@ const FieldSet: FC<Props> = ({ name, label, selectOptions, ...rest }) => {
);
}

if (isMulti && selectOptions) {
field = (
<Field
id={name}
className="custom-select" // can apply custom styles if needed
name={name}
options={selectOptions.map((opt) => ({ label: `${opt.display} (${opt.code})`, value: opt.code }))}
component={CustomSelectField}
placeholder="Select multi options..."
isMulti={true}
/>
);
}

return (
<>
<label htmlFor={name}>{label}</label>
Expand Down
4 changes: 2 additions & 2 deletions src/components/reports/FormDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const initialValues: FormValues = {
collectionDateTime: "04/06/2019 12:00",
receivedDateTime: "04/06/2019 15:00",
authorisedDateTime: "04/06/2019 15:30",
reasonForTest: "R59", // epilepsy
reasonForTest: ["R59"], // epilepsy
reasonForTestText:
"Sequence variant screening in Donald Duck because of epilepsy and atypical absences. " +
"An SLC2A1 variant is suspected.",
Expand Down Expand Up @@ -107,7 +107,7 @@ export const noValues: FormValues = {
collectionDateTime: "",
receivedDateTime: "",
authorisedDateTime: "",
reasonForTest: "",
reasonForTest: [""],
reasonForTestText: "",
},
variant: [],
Expand Down
23 changes: 16 additions & 7 deletions src/components/reports/ReportForm.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type DropDown = {
value: string;
};

const setDummyValues = (withDates: boolean, dropDowns?: DropDown[]) => {
const setDummyValues = (withDates: boolean, dropDowns?: DropDown[], multiSelect?: DropDown[]) => {
const dummyValue = "Always_the_same";
const form = screen.getByRole("form");

Expand Down Expand Up @@ -53,6 +53,14 @@ const setDummyValues = (withDates: boolean, dropDowns?: DropDown[]) => {
});
}

if (multiSelect) {
multiSelect.map((singleSelect) => {
const field = within(form).getByLabelText(singleSelect.field);
clearAndType(field, singleSelect.value);
userEvent.tab();
});
}

if (withDates) {
within(form)
.queryAllByLabelText(/date/i)
Expand Down Expand Up @@ -85,9 +93,9 @@ async function setLabAndPatient() {
});
}

async function setDummyAndNext(withDates: boolean, dropDowns?: DropDown[]) {
async function setDummyAndNext(withDates: boolean, dropDowns?: DropDown[], multiSelect?: DropDown[]) {
await act(async () => {
setDummyValues(withDates, dropDowns);
setDummyValues(withDates, dropDowns, multiSelect);
});

await act(async () => {
Expand All @@ -96,10 +104,11 @@ async function setDummyAndNext(withDates: boolean, dropDowns?: DropDown[]) {
}

const setSample = () => {
return setDummyAndNext(true, [
{ field: /specimen type/i, value: "122555007" },
{ field: /test reason/i, value: "R59" },
]);
return setDummyAndNext(
true,
[{ field: /specimen type/i, value: "122555007" }],
[{ field: /test reason/i, value: "R59" }],
);
};

async function setVariantFields() {
Expand Down
33 changes: 32 additions & 1 deletion src/components/reports/formDataValidation.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { optionalDateTime, patientSchema, requiredDate, requiredDateTime } from "./formDataValidation";
import {
optionalDateTime,
patientSchema,
requiredDate,
requiredDateTime,
requiredStringArray,
} from "./formDataValidation";
import * as Yup from "yup";
import { ValidationError } from "yup";
import { Patient } from "@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IPatient";
Expand Down Expand Up @@ -67,6 +73,31 @@ describe("Custom form validation", () => {
};
await patientSchema.validate(model);
}

await expect(validateModel).rejects.toThrow(ValidationError);
});

test.each([
["undefined", undefined, false],
["undefined value in array", [undefined], false],
["empty value in array", [""], false],
["single value", ["one"], true],
["multiple values", ["one", "two", "three"], true],
])(
"String array with '%s' pass validation",
async (description: string, value: string[] | undefined | undefined[], validates: boolean) => {
const schema = Yup.object({
requiredStringArray: requiredStringArray,
}).required();

const model = { requiredStringArray: value };
const validateModel = async () => await schema.validate(model);

if (validates) {
await expect(validateModel).resolves;
} else {
await expect(validateModel).rejects.toThrow(ValidationError);
}
},
);
});
6 changes: 5 additions & 1 deletion src/components/reports/formDataValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export const dateTime = Yup.string()
});
export const requiredDateTime = dateTime.required();
export const optionalDateTime = dateTime.optional();
export const requiredStringArray = Yup.array()
.of(Yup.string().required())
.required()
.test("required", "Select at least one value", (value) => value !== undefined && value.length !== 0);

const boolField = Yup.boolean().default(false).nullable(false);

Expand Down Expand Up @@ -68,7 +72,7 @@ export const sampleSchema = Yup.object({
receivedDateTime: requiredDateTime,
authorisedDateTime: optionalDateTime,
specimenType: requiredString,
reasonForTest: requiredString,
reasonForTest: requiredStringArray,
reasonForTestText: optionalString,
});

Expand Down
2 changes: 1 addition & 1 deletion src/components/reports/formSteps/Sample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ const Sample: FC = () => {
<FieldSet name="sample.receivedDateTime" label="Sample received datetime" />
<FieldSet name="sample.authorisedDateTime" label="Sample authorised datetime" />
<FieldSet name="sample.reasonForTestText" label="Reason for test" />
<FieldSet name="sample.reasonForTest" label="Test reason" selectOptions={diseases} />
<FieldSet name="sample.reasonForTest" label="Test reason (s)" isMulti={true} selectOptions={diseases} />
</>
);
};
Expand Down
4 changes: 2 additions & 2 deletions src/fhir/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,10 @@ export const bundleRequest = (form: FormValues, reportedGenes: RequiredCoding[])
};

/**
* Generates a unique report identifier that is unique for a sample and a reason for testing.
* Generates a unique report identifier that is unique for a sample and reasons for testing.
* @param sample data from the sample form
*/
const getUniqueReportIdentifier = (sample: SampleSchema) => `${sample.specimenCode}_${sample.reasonForTest}`;
const getUniqueReportIdentifier = (sample: SampleSchema) => `${sample.specimenCode}_${sample.reasonForTest.join("-")}`;

export const createBundle = (form: FormValues, reportedGenes: RequiredCoding[]): Bundle => {
const reportIdentifier = getUniqueReportIdentifier(form.sample);
Expand Down
12 changes: 5 additions & 7 deletions src/fhir/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -521,12 +521,10 @@ export const serviceRequestAndId = (
},
],
};
request.reasonCode = [
{
coding: [codedValue(diseases, sample.reasonForTest)],
text: sample.reasonForTestText,
},
];
request.reasonCode = sample.reasonForTest.map((reason) => ({
coding: [codedValue(diseases, reason)],
text: sample.reasonForTestText,
}));

const identifier = createIdentifier(reportIdentifier);
request.identifier = [identifier];
Expand Down Expand Up @@ -559,7 +557,7 @@ export const reportAndId = (
reference("Practitioner", authoriserIdentifier),
];
report.code = {
coding: [codedValue(diseases, sample.reasonForTest)],
coding: sample.reasonForTest.map((reason) => codedValue(diseases, reason)),
};
report.conclusion = result.clinicalConclusion;
const identifier = createIdentifier(reportIdentifier);
Expand Down