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

(refactor) O3-3654: Results filter refinement #1943

Merged
merged 1 commit into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 16 additions & 14 deletions e2e/specs/results-viewer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,21 +264,23 @@ test('Record and edit test results', async ({ page }) => {
await resultsViewerPage.goTo(patient.uuid);
});

await test.step('And I click on the `Panel` tab', async () => {
await page.getByRole('tab', { name: /panel/i }).click();
await test.step('And I click on the `Individual tests` tab', async () => {
await page.getByRole('tab', { name: /individual tests/i }).click();
});

await test.step('Then I should see the newly entered test results reflect in the results viewer', async () => {
for (const { resultsPageReference, value } of completeBloodCountData) {
await test.step(resultsPageReference, async () => {
const row = page.locator(`tr:has-text("${resultsPageReference}")`);
const valueCell = row.locator('td:nth-child(2)');
await expect(valueCell).toContainText(value);
});
}
await test.step('Then I should see the newly entered test results reflect in the results viewer', async () => {
for (const { resultsPageReference, value } of completeBloodCountData) {
await test.step(resultsPageReference, async () => {
const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${value}"))`).first();
const valueCell = row.locator('td:nth-child(2)');
await expect(valueCell).toContainText(value);
});
}
});
for (const { resultsPageReference, value } of chemistryResultsData) {
await test.step(resultsPageReference, async () => {
const row = page.locator(`tr:has-text("${resultsPageReference}")`);
const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${value}"))`).first();
const valueCell = row.locator('td:nth-child(2)');
await expect(valueCell).toContainText(value);
});
Expand Down Expand Up @@ -338,21 +340,21 @@ test('Record and edit test results', async ({ page }) => {
await resultsViewerPage.goTo(patient.uuid);
});

await test.step('And I click on the `Panel` tab', async () => {
await page.getByRole('tab', { name: /panel/i }).click();
await test.step('And I click on the `Individual tests` tab', async () => {
await page.getByRole('tab', { name: /individual tests/i }).click();
});

await test.step('Then I should see the updated results reflect in the results viewer', async () => {
for (const { resultsPageReference, updatedValue } of completeBloodCountData) {
await test.step(resultsPageReference, async () => {
const row = page.locator(`tr:has-text("${resultsPageReference}")`);
const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${updatedValue}"))`).first();
const valueCell = row.locator('td:nth-child(2)');
await expect(valueCell).toContainText(updatedValue);
});
}
for (const { resultsPageReference, updatedValue } of chemistryResultsData) {
await test.step(resultsPageReference, async () => {
const row = page.locator(`tr:has-text("${resultsPageReference}")`);
const row = page.locator(`tr:has-text("${resultsPageReference}"):has(td:has-text("${updatedValue}"))`).first();
const valueCell = row.locator('td:nth-child(2)');
await expect(valueCell).toContainText(updatedValue);
});
Expand Down
7 changes: 0 additions & 7 deletions packages/esm-patient-labs-app/src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,6 @@ export const configSchema = {
},
],
},
showPrintButton: {
_type: Type.Boolean,
_default: true,
_description:
'Whether or not to display the print button in the Test Results dashboard. When set to `true`, a print button is shown alongside the panel and tree view content switcher. When clicked, a modal pops up showing a datatable with the available test results. Once the user selects an appropriate date range, they can click on the print button in the modal to print the data',
},
orders: {
labOrderTypeUuid: {
_type: Type.UUID,
Expand Down Expand Up @@ -97,7 +91,6 @@ export interface OrderReason {
}
export interface ConfigObject {
resultsViewerConcepts: Array<ObsTreeEntry>;
showPrintButton: boolean;
orders: {
labOrderTypeUuid: string;
labOrderableConcepts: Array<string>;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import React, { useContext, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Accordion, AccordionItem, Button, Checkbox, Search } from '@carbon/react';
import { TreeViewAlt, Close, Search as SearchIcon } from '@carbon/react/icons';
import { Accordion, AccordionItem, Button, Checkbox } from '@carbon/react';
import { useConfig, useLayoutType } from '@openmrs/esm-framework';
import { type ConfigObject } from '../../config-schema';
import type { FilterNodeProps, FilterLeafProps } from './filter-types';
Expand All @@ -17,6 +16,10 @@ interface FilterSetProps {
hideFilterSetHeader?: boolean;
}

interface filterNodeParentProps extends Pick<FilterNodeProps, 'root'> {
itemNumber: number;
}

function filterTreeNode(inputValue, treeNode) {
// If the tree node's display value contains the user input, or any of its children's display contains the user input, return true
if (
Expand All @@ -31,56 +34,23 @@ function filterTreeNode(inputValue, treeNode) {
return false;
}

const FilterSet: React.FC<FilterSetProps> = ({ hideFilterSetHeader = false }) => {
const FilterSet: React.FC<FilterSetProps> = () => {
const { roots } = useContext(FilterContext);
const config = useConfig<ConfigObject>();
const tablet = useLayoutType() === 'tablet';
const { t } = useTranslation();
const { resetTree } = useContext(FilterContext);
const [searchTerm, setSearchTerm] = useState('');
const [treeDataFiltered, setTreeDataFiltered] = useState(roots);
const [showSearchInput, setShowSearchInput] = useState(false);

useEffect(() => {
const filteredData = roots.filter((node) => filterTreeNode(searchTerm, node));
setTreeDataFiltered(filteredData);
}, [searchTerm, roots]);

return (
<div className={!tablet ? styles.stickyFilterSet : ''}>
{!hideFilterSetHeader &&
(!showSearchInput ? (
<div className={styles.filterSetHeader}>
<h4>{t('tree', 'Tree')}</h4>
<div className={styles.filterSetActions}>
<Button
kind="ghost"
size="sm"
onClick={resetTree}
renderIcon={(props) => <TreeViewAlt size={16} {...props} />}
>
{t('resetTreeText', 'Reset tree')}
</Button>

<Button kind="ghost" size="sm" renderIcon={SearchIcon} onClick={() => setShowSearchInput(true)}>
{t('search', 'Search')}
</Button>
</div>
</div>
) : (
<div className={styles.filterTreeSearchHeader}>
<Search autoFocus size="sm" value={searchTerm} onChange={(evt) => setSearchTerm(evt.target.value)} />
<Button kind="secondary" size="sm" onClick={() => {}}>
{t('search', 'Search')}
</Button>
<Button hasIconOnly renderIcon={Close} size="sm" kind="ghost" onClick={() => setShowSearchInput(false)} />
</div>
))}
<div>
<div className={styles.filterSetContent}>
{treeDataFiltered?.length > 0 ? (
treeDataFiltered?.map((root, index) => (
<div className={styles.nestedAccordion} key={`filter-node-${index}`}>
<FilterNode root={root} level={0} open={config.resultsViewerConcepts[index].defaultOpen} />
<div className={`${styles.nestedAccordion} ${styles.nestedAccordionTablet}`} key={`filter-node-${index}`}>
<FilterNodeParent root={root} itemNumber={index} />
</div>
))
) : (
Expand All @@ -91,11 +61,48 @@ const FilterSet: React.FC<FilterSetProps> = ({ hideFilterSetHeader = false }) =>
);
};

const FilterNodeParent = ({ root, itemNumber }: filterNodeParentProps) => {
const config = useConfig<ConfigObject>();
const { t } = useTranslation();
const tablet = useLayoutType() === 'tablet';
const [expandAll, setExpandAll] = useState<boolean | undefined>(undefined);

if (!root.subSets) return;

const filterParent = root.subSets.map((node) => {
return (
<FilterNode
root={node}
level={0}
open={expandAll === undefined ? config.resultsViewerConcepts[itemNumber].defaultOpen : expandAll}
/>
);
});

return (
<div>
<div className={`${styles.treeNodeHeader} ${tablet ? styles.treeNodeHeaderTablet : ''}`}>
<h5>{t(root.display)}</h5>
<Button
className={styles.button}
kind="ghost"
size="sm"
onClick={() => setExpandAll((prevValue) => !prevValue)}
>
<span>{t(!expandAll ? `Expand all` : `Collapse all`)}</span>
</Button>
</div>
{filterParent}
</div>
);
};

const FilterNode = ({ root, level, open }: FilterNodeProps) => {
const tablet = useLayoutType() === 'tablet';
const { checkboxes, parents, updateParent } = useContext(FilterContext);
const indeterminate = isIndeterminate(parents[root.flatName], checkboxes);
const allChildrenChecked = parents[root.flatName]?.every((kid) => checkboxes[kid]);

return (
<Accordion align="start" size={tablet ? 'md' : 'sm'}>
<AccordionItem
Expand All @@ -104,7 +111,7 @@ const FilterNode = ({ root, level, open }: FilterNodeProps) => {
id={root?.flatName}
checked={root.hasData && allChildrenChecked}
indeterminate={indeterminate}
labelText={`${root?.display} (${parents?.[root?.flatName]?.length})`}
labelText={`${root?.display} (${parents?.[root?.flatName]?.length ?? 0})`}
onChange={() => updateParent(root.flatName)}
disabled={!root.hasData}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,61 +1,53 @@
@use '@carbon/layout';
@use '@carbon/type';
@use '@openmrs/esm-styleguide/src/vars' as *;
@use '@carbon/styles/scss/type';
@use '@carbon/colors';
@import '@openmrs/esm-styleguide/src/vars';

.stickyFilterSet {
position: sticky;
top: 6.5rem;
overflow-y: hidden;
.filterSetContent {
max-height: calc(100vh - 9.5rem);
overflow-y: auto;
}

.filterSetHeader {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: layout.$spacing-03;
// background of filter, and spacing between containers
.nestedAccordion {
background-color: $openmrs-background-grey;
position: sticky;
top: 0;
z-index: 1;
margin: layout.$spacing-02 0;

@media (min-width: $breakpoint-small-desktop-min) {
margin: layout.$spacing-02 0;
}

h4 {
@include type.type-style('heading-compact-02');
color: $text-02;
:global(.cds--accordion__item) {
border: none;
}

.filterSetActions {
display: flex;
justify-content: flex-end;
align-items: center;
:global(.cds--accordion__item:last-child) {
border: none;
}
}

.filterTreeSearchHeader {
.treeNodeHeader {
display: flex;
justify-content: space-between;
border-bottom: 1px solid colors.$gray-20;
padding: layout.$spacing-03 0;
margin-bottom: layout.$spacing-03;
background-color: $openmrs-background-grey;
position: sticky;
top: 0;
z-index: 1;
align-items: center;
}

.filterSetContent {
max-height: calc(100vh - 9.5rem);
overflow-y: auto;
.treeNodeHeaderTablet {
padding-left: layout.$spacing-05;
margin-bottom: 0;
}

// background of filter, and spacing between containers
.nestedAccordion {
background-color: $openmrs-background-grey;
margin: layout.$spacing-02 0;
@media (min-width: $breakpoint-small-desktop-min) {
background-color: $ui-background;
}
.nestedAccordionTablet {
margin-bottom: layout.$spacing-05;
}

// our special accordion rules
.nestedAccordion > :global(.cds--accordion--start) > :global(.cds--accordion__item--active) {
border-left: 0.375rem solid var(--brand-01);

@media (max-width: $breakpoint-tablet-max) {
margin: layout.$spacing-06 0;
}
Expand All @@ -75,6 +67,7 @@

.cds--accordion__item--active > .cds--accordion__content {
display: block;
padding-right: layout.$spacing-05;
}

.cds--accordion__title {
Expand Down
Loading