Skip to content

Commit

Permalink
Add support for adding criteria from a list of codes
Browse files Browse the repository at this point in the history
  • Loading branch information
tjennison-work committed Jan 27, 2025
1 parent a9e4c0c commit 706b4aa
Show file tree
Hide file tree
Showing 21 changed files with 587 additions and 86 deletions.
1 change: 1 addition & 0 deletions docs/generated/PROTOCOL_BUFFERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ which have condition_name of "Diabetes").
| default_sort | [tanagra.SortOrder](#tanagra-SortOrder) | | The sort order to use in the list view, or in hierarchies where no sort order has been specified. |
| limit | [int32](#int32) | optional | Number of values to display in the list view for each entity group. Otherwise, a default value is applied. |
| nameAttribute | [string](#string) | optional | The attribute used to name selections if not the first column. This can be used to include extra context with the selected values that's not visible in the table view. |
| codeAttributes | [string](#string) | repeated | Optional attributes to search when adding criteria by code. It's recommended to enable multi_select when using codeAttributes because multiple codes can be added at the same time which forces the criteria into multi_select mode regardless of the setting. |



Expand Down
274 changes: 274 additions & 0 deletions ui/src/addByCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import Button from "@mui/material/Button";
import Chip from "@mui/material/Chip";
import { createCriteria, lookupCriteria, LookupEntry } from "cohort";
import Checkbox from "components/checkbox";
import Loading from "components/loading";
import {
TreeGrid,
TreeGridColumn,
TreeGridId,
useArrayAsTreeGridData,
} from "components/treegrid";
import ActionBar from "actionBar";
import { DataKey } from "data/types";
import { useUnderlaySource } from "data/underlaySourceContext";
import { useUnderlay, useCohortGroupSectionAndGroup } from "hooks";
import { GridBox } from "layout/gridBox";
import GridLayout from "layout/gridLayout";
import { useCallback, useMemo, useState } from "react";
import useSWRMutation from "swr/mutation";
import { useImmer } from "use-immer";
import { TextField } from "@mui/material";
import { useNavigate } from "util/searchState";
import { cohortURL, useIsSecondBlock } from "router";
import { insertCohortCriteria, useCohortContext } from "cohortContext";
import { isValid } from "util/valid";

type LookupEntryItem = {
config?: JSX.Element;
code: DataKey;
name?: string;
entry?: LookupEntry;
};

export function AddByCode() {
const underlay = useUnderlay();
const underlaySource = useUnderlaySource();
const context = useCohortContext();
const navigate = useNavigate();
const { cohort, section } = useCohortGroupSectionAndGroup();
const secondBlock = useIsSecondBlock();

const [query, setQuery] = useState<string | undefined>();
const [selected, updateSelected] = useImmer(new Set<DataKey>());

const configMap = useMemo(
() =>
new Map(underlay.criteriaSelectors.map((s) => [s.name, s.displayName])),
[underlay.criteriaSelectors]
);

const lookupEntriesState = useSWRMutation<LookupEntryItem[]>(
{
component: "AddByCode",
query,
},
async () => {
const codes = [
...new Set(
query
?.split(/[\s,]+/)
.map((c) => c.trim())
.filter((c) => c.length)
),
];
if (!codes) {
return [];
}

const entries = (
await lookupCriteria(
underlaySource,
underlay.criteriaSelectors.filter((s) => s.isEnabledForCohorts),
codes
)
)?.data;
if (!entries) {
return [];
}

const entryMap = new Map(entries.map((e) => [e.code, e]));
const mappedEntries = codes.map((c) => {
const entry = entryMap.get(c);
if (!entry) {
return {
config: <Chip color="error" label="Not found" size="small" />,
code: c,
name: "There were no matches for this code",
};
}
return {
config: <Chip label={configMap.get(entry.config)} size="small" />,
code: entry.code,
name: entry.name,
entry: entry,
};
});

updateSelected((s) => {
s.clear();
mappedEntries.forEach((e) => {
if (e.entry) {
s.add(e.code);
}
});
});
return mappedEntries;
}
);

const data = useArrayAsTreeGridData(lookupEntriesState?.data ?? [], "code");

const onInsert = useCallback(() => {
const configMap = new Map<string, LookupEntryItem[]>();
lookupEntriesState.data?.forEach((e) => {
if (!e.entry || !selected.has(e.code)) {
return;
}

const list = configMap.get(e.entry.config) ?? [];
list.push(e);
configMap.set(e.entry.config, list);
});

const criteria = Array.from(configMap.entries()).map(([c, entries]) => {
const config = underlay.criteriaSelectors.find(
(config) => config.name === c
);
if (!config) {
throw new Error(`Config ${c} not found.`);
}

return createCriteria(
underlaySource,
config,
entries.map((e) => e.entry?.data).filter(isValid)
);
});

const group = insertCohortCriteria(
context,
section.id,
criteria,
secondBlock
);
navigate("../../../" + cohortURL(cohort.id, section.id, group.id));
}, [context, cohort.id, section.id, navigate]);

return (
<GridLayout rows fillRow={1}>
<ActionBar title={"Adding criteria by codes"} />
<GridLayout
rows
sx={{
backgroundColor: (theme) => theme.palette.background.paper,
}}
>
<GridBox
sx={{
px: 5,
py: 3,
height: "auto",
}}
>
<GridLayout cols fillCol={0} spacing={1}>
<TextField
multiline
fullWidth
autoFocus
autoComplete="off"
minRows={3}
maxRows={10}
placeholder={
"Enter codes to lookup separated by commas, spaces, or line breaks"
}
value={query}
sx={{
backgroundColor: (theme) => theme.palette.info.main,
borderRadius: "16px",
"& .MuiOutlinedInput-root": {
borderRadius: "16px",
},
}}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setQuery(event.target.value);
}}
/>
<GridLayout rows rowAlign="bottom">
<Button
variant="contained"
onClick={() => lookupEntriesState.trigger()}
>
Lookup
</Button>
</GridLayout>
</GridLayout>
</GridBox>
<Loading immediate showProgressOnMutate status={lookupEntriesState}>
{lookupEntriesState.data?.length ? (
<TreeGrid
columns={columns}
data={data}
rowCustomization={(id: TreeGridId) => {
if (!lookupEntriesState.data) {
return undefined;
}

const item = data[id]?.data as LookupEntryItem;
if (!item) {
return undefined;
}

const sel = selected.has(id);
return [
{
column: 2,
prefixElements: (
<Checkbox
size="small"
fontSize="inherit"
checked={sel}
disabled={!item.entry}
onChange={() => {
updateSelected((selected) => {
if (sel) {
selected.delete(id);
} else {
selected.add(id);
}
});
}}
/>
),
},
];
}}
/>
) : null}
</Loading>
</GridLayout>
<GridLayout
colAlign="right"
sx={{
p: 1,
}}
>
<Button
variant="contained"
size="large"
disabled={selected.size === 0}
onClick={() => onInsert()}
>
Add selected criteria
</Button>
</GridLayout>
</GridLayout>
);
}

const columns: TreeGridColumn[] = [
{
key: "config",
width: "160px",
},
{
key: "code",
width: "130px",
title: "Code",
},
{
key: "name",
width: "100%",
title: "Name",
},
];
17 changes: 16 additions & 1 deletion ui/src/addCriteria.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { useCallback, useMemo } from "react";
import {
addCohortCriteriaURL,
addFeatureSetCriteriaURL,
addByCodeURL,
cohortURL,
featureSetURL,
newCriteriaURL,
Expand Down Expand Up @@ -214,6 +215,20 @@ function AddCriteria(props: AddCriteriaProps) {
},
});

if (underlay.uiConfiguration.featureConfig?.enableAddByCode) {
options.push({
name: "tAddByCode",
title: "Add criteria by code",
category: "Other",
tags: [],
cohort: true,
showMore: false,
fn: () => {
navigate(addByCodeURL());
},
});
}

return options;
}, [props.onInsertPredefinedCriteria, selectors, predefinedCriteria]);

Expand Down Expand Up @@ -322,7 +337,7 @@ function AddCriteria(props: AddCriteriaProps) {
const criteria = createCriteria(
underlaySource,
option.selector,
dataEntry
dataEntry ? [dataEntry] : undefined
);
if (!!getCriteriaPlugin(criteria).renderEdit && !dataEntry) {
navigate(newCriteriaURL(option.name));
Expand Down
5 changes: 5 additions & 0 deletions ui/src/appRoutes.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { CohortRevision } from "activityLog/cohortRevision";
import { AddCohort } from "addCohort";
import { AddByCode } from "addByCode";
import { AddCohortCriteria, AddFeatureSetCriteria } from "addCriteria";
import { LoginPage, LogoutPage } from "auth/provider";
import { CohortReview } from "cohortReview/cohortReview";
Expand Down Expand Up @@ -86,6 +87,10 @@ function cohortRootRoutes() {
path: "tAddFeatureSet",
element: <AddFeatureSet />,
},
{
path: "tAddByCode",
element: <AddByCode />,
},
],
},
{
Expand Down
Loading

0 comments on commit 706b4aa

Please sign in to comment.