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

(feat) Timeline Results Grouping O3-1060 #591

Merged
merged 14 commits into from
Mar 11, 2022
44 changes: 42 additions & 2 deletions packages/esm-patient-test-results-app/src/config-schema.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
export const configSchema = {};
import { Type } from '@openmrs/esm-framework';

export interface ConfigObject {}
export const configSchema = {
concepts: {
_type: Type.Array,
_elements: {
conceptUuid: {
_type: Type.UUID,
_description: 'UUID of concept to load from /obstree',
},
defaultOpen: {
_type: Type.Boolean,
_description: 'Set default behavior of filter accordion',
},
},
_default: [
{
conceptUuid: '5035a431-51de-40f0-8f25-4a98762eb796',
defaultOpen: true,
},
{
conceptUuid: '5566957d-9144-4fc5-8700-1882280002c1',
defaultOpen: false,
},
{
conceptUuid: '36d88354-1081-40af-b70a-2c4981b31367',
defaultOpen: false,
},
{
conceptUuid: 'acb5bab3-af2a-47c4-a985-934fd0113589',
defaultOpen: false,
},
],
},
};

export interface ObsTreeEntry {
conceptUuid: string;
defaultOpen: boolean;
}
export interface ConfigObject {
concepts: Array<ObsTreeEntry>;
}
74 changes: 62 additions & 12 deletions packages/esm-patient-test-results-app/src/filter/filter-context.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import React, { createContext, useReducer, useEffect, useMemo } from 'react';
import React, { createContext, useReducer, useEffect, useMemo, useState } from 'react';
import { parseTime } from '../timeline/useTimelineData';
import reducer from './filter-reducer';
import mockConceptTree from '../hiv/mock-concept-tree';
import { TreeNode } from './filter-set';

const initialState = {
checkboxes: {},
parents: {},
roots: [{ display: '', flatName: '' }],
tests: {},
lowestParents: [],
};

const initialContext = {
state: initialState,
checkboxes: {},
parents: {},
...initialState,
timelineData: {},
activeTests: [],
someChecked: false,
initialize: () => {},
Expand All @@ -21,11 +25,16 @@ const initialContext = {
interface StateProps {
checkboxes: { [key: string]: boolean };
parents: { [key: string]: string[] };
roots: { [key: string]: any }[];
}
interface FilterContextProps {
state: StateProps;
checkboxes: { [key: string]: boolean };
parents: { [key: string]: string[] };
roots: TreeNode[];
tests: { [key: string]: any };
lowestParents: { display: string; flatName: string }[];
timelineData: { [key: string]: any };
activeTests: string[];
someChecked: boolean;
initialize: any;
Expand All @@ -34,18 +43,22 @@ interface FilterContextProps {
}

interface FilterProviderProps {
sortedObs: any; // this data structure will change later
roots: any[];
children: React.ReactNode;
}

interface obsShape {
[key: string]: any;
}

const FilterContext = createContext<FilterContextProps>(initialContext);

const FilterProvider = ({ sortedObs, children }: FilterProviderProps) => {
const FilterProvider = ({ roots, children }: FilterProviderProps) => {
const [state, dispatch] = useReducer(reducer, initialState);

const actions = useMemo(
() => ({
initialize: (tree) => dispatch({ type: 'initialize', tree: tree }),
initialize: (trees) => dispatch({ type: 'initialize', trees: trees }),
toggleVal: (name) => {
dispatch({ type: 'toggleVal', name: name });
},
Expand All @@ -56,22 +69,59 @@ const FilterProvider = ({ sortedObs, children }: FilterProviderProps) => {
[dispatch],
);

const activeTests = Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || [];
const activeTests = useMemo(() => {
return Object.keys(state?.checkboxes)?.filter((key) => state.checkboxes[key]) || [];
}, [state.checkboxes]);

const someChecked = Boolean(activeTests.length);

const timelineData = useMemo(() => {
if (!state?.tests) {
return {
data: { parsedTime: {} as ReturnType<typeof parseTime>, rowData: [], panelName: '' },
loaded: false,
};
}
const tests: obsShape = activeTests?.length
? Object.fromEntries(Object.entries(state.tests).filter(([name, entry]) => activeTests.includes(name)))
: state.tests;

const allTimes = [
...new Set(
Object.values(tests)
.map((test: obsShape) => test?.obs?.map((entry) => entry.obsDatetime))
.flat(),
),
];
allTimes.sort((a, b) => (new Date(a) < new Date(b) ? 1 : -1));
const rows = [];
Object.keys(tests).forEach((test) => {
const newEntries = allTimes.map((time: string) => tests[test].obs.find((entry) => entry.obsDatetime === time));
rows.push({ ...tests[test], entries: newEntries });
});
const panelName = 'timeline';
return {
data: { parsedTime: parseTime(allTimes), rowData: rows, panelName },
loaded: true,
};
}, [activeTests, state.tests]);

useEffect(() => {
const tests = (sortedObs && Object.keys(sortedObs)) || [];
if (tests.length && !Object.keys(state?.checkboxes).length) {
actions.initialize(mockConceptTree);
if (roots?.length && !Object.keys(state?.checkboxes).length) {
actions.initialize(roots);
}
}, [sortedObs, actions, state]);
}, [actions, state, roots]);

return (
<FilterContext.Provider
value={{
state,
checkboxes: state.checkboxes,
parents: state.parents,
roots: state.roots,
tests: state.tests,
lowestParents: state.lowestParents,
timelineData,
activeTests,
someChecked,
initialize: actions.initialize,
Expand Down
69 changes: 58 additions & 11 deletions packages/esm-patient-test-results-app/src/filter/filter-reducer.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,74 @@
const computeParents = (node) => {
export const getName = (prefix, name) => {
return prefix ? `${prefix}-${name}` : name;
};

const computeParents = (prefix, node) => {
var parents = {};
const leaves = [];
if (node?.subSets?.length) {
var leaves = [];
var tests = [];
var lowestParents = [];
if (node?.subSets?.length && node.subSets[0].obs) {
// lowest parent
let activeLeaves = [];
node.subSets.forEach((leaf) => {
if (leaf.hasData) {
activeLeaves.push(leaf.flatName);
}
});
let activeTests = [];
node.subSets.forEach((leaf) => {
if (leaf.obs.length) {
activeTests.push([leaf.flatName, leaf]);
}
});
leaves.push(...activeLeaves);
tests.push(...activeTests);
lowestParents.push({ flatName: node.flatName, display: node.display });
} else if (node?.subSets?.length) {
node.subSets.map((subNode) => {
const { parents: newParents, leaves: newLeaves } = computeParents(subNode);
const {
parents: newParents,
leaves: newLeaves,
tests: newTests,
lowestParents: newLowestParents,
} = computeParents(getName(prefix, node.display), subNode);
parents = { ...parents, ...newParents };
leaves.push(...newLeaves);
tests.push(...newTests);
lowestParents.push(...newLowestParents);
});
}
if (node?.obs?.length) {
leaves.push(...node.obs.map((leaf) => leaf.display));
}
parents[node.display] = leaves;
return { parents: parents, leaves: leaves };
parents[node.flatName] = leaves;
return { parents, leaves, tests, lowestParents };
};

const reducer = (state, action) => {
switch (action.type) {
case 'initialize':
const { parents, leaves } = computeParents(action.tree);
let parents = {},
leaves = [],
tests = [],
lowestParents = [];
action.trees?.forEach((tree) => {
// if anyone knows a shorthand for this i'm stoked to learn it :)
const {
parents: newParents,
leaves: newLeaves,
tests: newTests,
lowestParents: newLP,
} = computeParents('', tree);
parents = { ...parents, ...newParents };
leaves = [...leaves, ...newLeaves];
tests = [...tests, ...newTests];
lowestParents = [...lowestParents, ...newLP];
});
const flatTests = Object.fromEntries(tests);
return {
checkboxes: Object.fromEntries(leaves.map((leaf) => [leaf, true])),
checkboxes: Object.fromEntries(leaves?.map((leaf) => [leaf, false])) || {},
parents: parents,
roots: action.trees,
tests: flatTests,
lowestParents: lowestParents,
};
case 'toggleVal':
return {
Expand Down
39 changes: 32 additions & 7 deletions packages/esm-patient-test-results-app/src/filter/filter-set.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,38 @@
@import "~carbon-components/src/globals/scss/vars";
@import "~carbon-components/src/globals/scss/mixins";

.floatingRightButton {
position: absolute;
top: 0px;
right: 0px;
z-index: 9;
}
.expandButton {
background-color: white;
border: 1px solid black;
}

.floatingRightButton > .expandButton > svg {
margin: 0;
}


.filterContainer {
background-color: $openmrs-background-grey;
margin: $spacing-02 0;
}

.filterContainerActive {
border-left: 0.3rem solid var(--brand-01);
}

.filterNode {
padding: 1rem;
}

.filterItem {
padding: 0.5rem 1rem 0.5rem 4rem;
border-bottom: 1px solid $ui-03;
}

.nestedAccordion > :global(.bx--accordion--start) > :global(.bx--accordion__item--active) {
border-left: 0.3rem solid var(--brand-01);
margin: 1.5rem 0;
}

.nestedAccordion :global {
Expand All @@ -36,6 +52,14 @@
display: block;
}

.bx--accordion__title {
margin: 0 0 0 0.8rem;
}

.bx--accordion__arrow {
margin: 0.4rem 0 0 1rem;
}

// Chevron transformations
.bx--accordion__item > button[aria-expanded="false"] > .bx--accordion__arrow {
transform: rotate(90deg);
Expand All @@ -47,7 +71,8 @@
fill: var(--brand-01);
}

.bx--accordion--start > .bx--accordion__item > button[aria-expanded="true"] {
background-color: $ui-03;
.bx--checkbox-label-text {
padding: 0 0 0 .75rem;
}
}

Loading