Skip to content

Commit

Permalink
Merge pull request #732 from MeasureAuthoringTool/MAT-7864
Browse files Browse the repository at this point in the history
MAT-7864 SPIKE/POC: How to Display Extensions as First Level Elements
  • Loading branch information
adongare authored Jan 16, 2025
2 parents 0770053 + a21177b commit a307178
Show file tree
Hide file tree
Showing 13 changed files with 1,384 additions and 31 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { StructureDefinition } from "fhir/r4";

export interface StructureDefinitionDto {
resourceName: string;
category: string;
primaryCodePath: string;
definition: string;
definition: StructureDefinition;
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,22 +78,13 @@ export class FhirDefinitionsServiceApi {
}

getBasePath(resource: any): string {
// const elements = [...resource?.definition?.snapshot?.element];
// return elements?.[0].path;
return resource?.definition?.snapshot?.element?.[0]?.path;
}

getTopLevelElements(resource: any) {
getTopLevelElements(resource: StructureDefinitionDto) {
const elements = [...resource?.definition?.snapshot?.element];
const basePath = this.getBasePath(resource);
return elements?.filter((e) => e.path.split(".")?.length === 2);
}

getRequiredElements(resource: any) {
const elements = [...resource?.definition?.snapshot?.element];
const basePath = this.getBasePath(resource);
return elements?.filter(
(e) => e.min > 0 && e.path.split(".")?.length === 2
(e) => e.path.split(".")?.length === 2 && e.max !== "0"
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useRef, useState, useEffect, useCallback } from "react";
import React, { useRef, useState, useEffect } from "react";
import { Box } from "@mui/material";
import * as _ from "lodash";
import useFhirDefinitionsServiceApi from "../../../../../../../api/useFhirDefinitionsService";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from "react";
import React from "react";
import * as _ from "lodash";
import { Box } from "@mui/material";
import TypeEditor from "./TypeEditor";
Expand All @@ -20,6 +20,8 @@ const Element = ({ element, label, resource, handleChange, canEdit }) => {
structureDefinition={element}
canEdit={canEdit}
label={label}
resource={resource}
parentStructureDefinition={null}
/>
</Box>
);
Expand Down Expand Up @@ -76,13 +78,15 @@ const ElementEditorChildren = ({
{/* given root definition we do a base level render */}
<TypeEditor
type={type.code}
resource={resource}
required={required}
value={elementValue}
onChange={(e) => {
elementValue = e;
handleChange(elemPath, e);
}}
structureDefinition={rootDefinition}
parentStructureDefinition={null}
canEdit={canEdit}
label={rootDefinition?.id}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,31 @@ import Autocomplete from "@mui/material/Autocomplete";
import { Checkbox, TextField } from "@mui/material";
import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank";
import CheckBoxIcon from "@mui/icons-material/CheckBox";
import { ElementDefinition } from "fhir/r4";

const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;

interface ElementSelectorProps {
basePath: string;
options: any[];
value: any;
onChange: (event, newValue: any | null) => void;
options: ElementDefinition[];
value: ElementDefinition[];
onChange: (event, newValue: ElementDefinition[] | null) => void;
}

/**
* Prepares the label for element selector options
* for slice- it will be slice:sliceName. e.g. Patient.extension:race results into extension:race
* for regular element- it will be the path of an element. e.g. Patient.gender results into gender
*/
const getOptionLabel = (option: ElementDefinition, basePath: string) => {
const label = option.path?.substring(basePath.length + 1);
if (option.sliceName) {
return `${label}:${option.sliceName}`;
}
return label;
};

const ElementSelector = ({
basePath,
options,
Expand All @@ -31,7 +45,7 @@ const ElementSelector = ({
value={value}
onChange={onChange}
disableCloseOnSelect
getOptionLabel={(option) => `${option.path}`}
getOptionLabel={(option) => getOptionLabel(option, basePath)}
renderOption={(props, option, { selected }) => (
<li {...props}>
<Checkbox
Expand All @@ -40,7 +54,7 @@ const ElementSelector = ({
style={{ marginRight: 8 }}
checked={selected}
/>
{option.path?.substring(basePath.length + 1)}
{getOptionLabel(option, basePath)}
</li>
)}
renderInput={(params) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as React from "react";
import { structuredDefinitionUSCoreEthnicity } from "../../../../../../../__mocks__/structuredDefinitions/StructureDefinition-us-core-ethnicity";
import { render, screen, waitFor } from "@testing-library/react";
import TypeEditor from "./TypeEditor";
import axios from "../../../../../../../../../../api/axios-instance";
import { FormikProvider } from "formik";
import {
ApiContextProvider,
ServiceConfig,
} from "../../../../../../../../../../api/ServiceContext";

jest.mock("../../../../../../../../../../api/axios-instance");
const mockedAxios = axios as jest.Mocked<typeof axios>;

jest.mock("@madie/madie-util", () => ({
useOktaTokens: jest.fn(() => ({
getAccessToken: () => "test.jwt",
})),
}));

const mockServiceConfig = {
fhirService: {
baseUrl: "string",
},
} as ServiceConfig;

jest.mock("../../../../../../../../../../api/useServiceConfig", () => {
return jest.fn(() => mockServiceConfig);
});

const getNestedProperty = (obj, path) => {
return path.split(".").reduce((current, key) => current && current[key], obj);
};

const patientResource = {
Patient: {
"extention:ethnicity": undefined,
},
};

//@ts-ignore
const mockFormik: FormikContextType<any> = {
values: {
...patientResource,
},
getFieldProps: (label) => {
const name = getNestedProperty(patientResource, label);
return {
value: name,
name,
onChange: jest.fn(),
onBlur: jest.fn(),
};
},
handleChange: () => {},
setFieldValue: jest.fn(),
};

describe("TypeEditor for profiled extensions/slices ", () => {
it("should render form for Patient.extension:ethnicity", async () => {
const handleChange = jest.fn();
const label = "Patient.extension:ethnicity";
const resource = {
id: "dc41859f8107",
resourceType: "Patient",
meta: {
profile: [
"http://hl7.org/fhir/us/qicore/StructureDefinition/qicore-patient",
],
},
};
const qiCoreEthnicityStructureDefinition = {
id: "Patient.extension:ethnicity",
min: 0,
max: 1,
path: "Patient.extension",
short: "(QI-Core)(USCDI) US Core ethnicity Extension",
sliceName: "ethnicity",
type: [
{
code: "Extension",
profile: [
"http://hl7.org/fhir/us/core/StructureDefinition/us-core-ethnicity",
],
},
],
};
mockedAxios.get.mockResolvedValue({
data: { definition: structuredDefinitionUSCoreEthnicity },
});

render(
<ApiContextProvider value={mockServiceConfig}>
<FormikProvider value={mockFormik}>
<TypeEditor
type="Extension"
resource={resource}
required={false}
value={undefined}
onChange={handleChange}
structureDefinition={qiCoreEthnicityStructureDefinition}
parentStructureDefinition={null}
label={label}
canEdit={true}
/>
</FormikProvider>
</ApiContextProvider>
);
await waitFor(() => {
expect(screen.getByTestId("string-field-input-id")).toBeInTheDocument();
});
expect(screen.getByTestId("extension:ombCategory")).toBeInTheDocument();
expect(screen.getByTestId("extension:detailed")).toBeInTheDocument();
expect(screen.getByTestId("extension:text")).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,17 @@ import CodesComponent from "./types/CodesComponent";
import { Instant } from "@madie/madie-design-system/dist/react";
import TimeComponent from "./types/TimeComponent";
import { useFormikContext } from "formik";
import ExtensionComponent from "./types/ExtensionComponent";
import ProfiledExtensionComponent from "./types/ProfiledExtensionComponent";

const TypeEditor = ({
type,
resource,
required,
value,
onChange,
structureDefinition,
parentStructureDefinition,
canEdit,
label,
}) => {
Expand Down Expand Up @@ -64,6 +68,7 @@ const TypeEditor = ({
error={getNestedProperty(formik.errors, label)}
structureDefinition={null}
fieldRequired={required}
value={value}
{...formik.getFieldProps(label)}
/>
</Box>
Expand Down Expand Up @@ -204,6 +209,23 @@ const TypeEditor = ({
structureDefinition={structureDefinition}
/>
);
case "Extension":
return _.isEmpty(structureDefinition?.type?.[0]?.profile) ? (
<ExtensionComponent
canEdit={canEdit}
onChange={onChange}
fhirResource={resource}
elementDefinition={structureDefinition}
parentStructureDefinition={parentStructureDefinition}
/>
) : (
<ProfiledExtensionComponent
canEdit={canEdit}
structureDefinition={structureDefinition}
fieldRequired={false}
resource={resource}
/>
);
default:
return <div>Unsupported Type [{type}]</div>;
}
Expand All @@ -216,12 +238,14 @@ const TypeEditor = ({
return (
<TypeEditor
type={childType?.code}
resource={resource}
onChange={(e) => {}}
value={null}
structureDefinition={childTypeDef}
required={childRequired}
canEdit={canEdit}
label={childTypeDef?.id}
parentStructureDefinition={structureDefinition}
/>
);
})}
Expand Down
Loading

0 comments on commit a307178

Please sign in to comment.