Skip to content

Commit

Permalink
Merge pull request #91 from gosh-dre/custom-multi-select
Browse files Browse the repository at this point in the history
Allow multiple reasons for test
  • Loading branch information
stefpiatek authored Nov 14, 2022
2 parents 92d6cb1 + 6f0d1fb commit 50111b5
Show file tree
Hide file tree
Showing 11 changed files with 593 additions and 55 deletions.
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 @@ -19,7 +19,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 @@ -55,6 +55,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 @@ -87,9 +95,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 @@ -98,10 +106,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

0 comments on commit 50111b5

Please sign in to comment.