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

(fix)O3-3046: Allow sorting vitals, biometrics and conditions rows based on different sort functions #1779

Merged
merged 9 commits into from
Apr 12, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ import { Add } from '@carbon/react/icons';
import { formatDate, parseDate, useLayoutType } from '@openmrs/esm-framework';
import { CardHeader, EmptyState, ErrorState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib';
import { ConditionsActionMenu } from './conditions-action-menu.component';
import { compare } from './utils';
import { useConditions } from './conditions.resource';
import { useConditions, type ConditionTableHeader, useConditionsSorting } from './conditions.resource';
import styles from './conditions-detailed-summary.scss';

function ConditionsDetailedSummary({ patient }) {
Expand All @@ -47,19 +46,28 @@ function ConditionsDetailedSummary({ patient }) {
return conditions;
}, [filter, conditions]);

const headers = useMemo(
const headers: Array<ConditionTableHeader> = useMemo(
() => [
{
key: 'display',
header: t('condition', 'Condition'),
isSortable: true,
sortFunc: (valueA, valueB) => valueA.display?.localeCompare(valueB.display),
},
{
key: 'onsetDateTime',
key: 'onsetDateTimeRender',
header: t('dateOfOnset', 'Date of onset'),
isSortable: true,
sortFunc: (valueA, valueB) =>
valueA.onsetDateTime && valueB.onsetDateTime
? new Date(valueA.onsetDateTime).getTime() - new Date(valueB.onsetDateTime).getTime()
: 0,
},
{
key: 'clinicalStatus',
key: 'status',
header: t('status', 'Status'),
isSortable: true,
sortFunc: (valueA, valueB) => valueA.clinicalStatus?.localeCompare(valueB.clinicalStatus),
},
],
[t],
Expand All @@ -72,22 +80,15 @@ function ConditionsDetailedSummary({ patient }) {
id: condition.id,
condition: condition.display,
abatementDateTime: condition.abatementDateTime,
onsetDateTime: {
sortKey: condition.onsetDateTime ?? '',
content: condition.onsetDateTime
? formatDate(parseDate(condition.onsetDateTime), { time: false, day: false })
: '--',
},
onsetDateTimeRender: condition.onsetDateTime
? formatDate(parseDate(condition.onsetDateTime), { time: false, day: false })
: '--',
status: condition.clinicalStatus,
};
});
}, [filteredConditions]);

const sortRow = (cellA, cellB, { sortDirection, sortStates }) => {
return sortDirection === sortStates.DESC
? compare(cellB.sortKey, cellA.sortKey)
: compare(cellA.sortKey, cellB.sortKey);
};
const { sortedRows, sortRow } = useConditionsSorting(headers, tableRows);

const launchConditionsForm = useCallback(
() =>
Expand Down Expand Up @@ -131,7 +132,7 @@ function ConditionsDetailedSummary({ patient }) {
</div>
</CardHeader>
<DataTable
rows={tableRows}
rows={sortedRows}
sortRow={sortRow}
headers={headers}
isSortable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,23 @@ import {
} from '@openmrs/esm-patient-common-lib';
import type { ConfigObject } from '../config-schema';
import { ConditionsActionMenu } from './conditions-action-menu.component';
import { useConditions } from './conditions.resource';
import { type Condition, useConditions, useConditionsSorting } from './conditions.resource';
import styles from './conditions-overview.scss';

interface ConditionTableRow extends Condition {
id: string;
condition: string;
abatementDateTime: string;
onsetDateTimeRender: string;
}

interface ConditionTableHeader {
key: 'display' | 'onsetDateTimeRender' | 'status';
header: string;
isSortable: true;
sortFunc: (valueA: ConditionTableRow, valueB: ConditionTableRow) => number;
}

interface ConditionsOverviewProps {
patientUuid: string;
}
Expand Down Expand Up @@ -74,35 +88,51 @@ const ConditionsOverview: React.FC<ConditionsOverviewProps> = ({ patientUuid })
return conditions;
}, [filter, conditions]);

const {
results: paginatedConditions,
goTo,
currentPage,
} = usePagination(filteredConditions ?? [], conditionPageSize);

const tableHeaders = [
{
key: 'display',
header: t('condition', 'Condition'),
},
{
key: 'onsetDateTime',
header: t('dateOfOnset', 'Date of onset'),
},
{
key: 'clinicalStatus',
header: t('status', 'Status'),
},
];
const headers: Array<ConditionTableHeader> = useMemo(
() => [
{
key: 'display',
header: t('condition', 'Condition'),
isSortable: true,
sortFunc: (valueA, valueB) => valueA.display?.localeCompare(valueB.display),
},
{
key: 'onsetDateTimeRender',
header: t('dateOfOnset', 'Date of onset'),
isSortable: true,
sortFunc: (valueA, valueB) =>
valueA.onsetDateTime && valueB.onsetDateTime
? new Date(valueA.onsetDateTime).getTime() - new Date(valueB.onsetDateTime).getTime()
: 0,
},
{
key: 'status',
header: t('status', 'Status'),
isSortable: true,
sortFunc: (valueA, valueB) => valueA.clinicalStatus?.localeCompare(valueB.clinicalStatus),
},
],
[t],
);

const tableRows = useMemo(() => {
return paginatedConditions?.map((condition) => ({
...condition,
onsetDateTime: condition.onsetDateTime
? formatDate(parseDate(condition.onsetDateTime), { time: false, day: false })
: '--',
}));
}, [paginatedConditions]);
return filteredConditions?.map((condition) => {
return {
...condition,
id: condition.id,
condition: condition.display,
abatementDateTime: condition.abatementDateTime,
onsetDateTimeRender: condition.onsetDateTime
? formatDate(parseDate(condition.onsetDateTime), { time: false, day: false })
: '--',
status: condition.clinicalStatus,
};
});
}, [filteredConditions]);

const { sortedRows, sortRow } = useConditionsSorting(headers, tableRows);

const { results: paginatedConditions, goTo, currentPage } = usePagination(sortedRows, conditionPageSize);

const handleConditionStatusChange = ({ selectedItem }) => setFilter(selectedItem);

Expand Down Expand Up @@ -139,12 +169,13 @@ const ConditionsOverview: React.FC<ConditionsOverviewProps> = ({ patientUuid })
</CardHeader>
<DataTable
aria-label="conditions overview"
rows={tableRows}
headers={tableHeaders}
rows={paginatedConditions}
headers={headers}
isSortable
size={isTablet ? 'lg' : 'sm'}
useZebraStyles
overflowMenuOnHover={isDesktop}
sortRow={sortRow}
>
{({ rows, headers, getHeaderProps, getTableProps }) => (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import useSWR from 'swr';
import { fhirBaseUrl, openmrsFetch, restBaseUrl, useConfig } from '@openmrs/esm-framework';
import { type FHIRCondition, type FHIRConditionResponse } from '../types';
import { useMemo, useState } from 'react';

export type Condition = {
clinicalStatus: string;
Expand Down Expand Up @@ -233,3 +234,45 @@ export async function deleteCondition(conditionId: string) {

return res;
}

export interface ConditionTableRow extends Condition {
id: string;
condition: string;
abatementDateTime: string;
onsetDateTimeRender: string;
}

export interface ConditionTableHeader {
key: 'display' | 'onsetDateTimeRender' | 'status';
header: string;
isSortable: true;
sortFunc: (valueA: ConditionTableRow, valueB: ConditionTableRow) => number;
}

export function useConditionsSorting(tableHeaders: Array<ConditionTableHeader>, tableRows: Array<ConditionTableRow>) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like useSortedConditionTableRows describe this hook better?

const [sortParams, setSortParams] = useState<{
key: ConditionTableHeader['key'] | '';
sortDirection: 'ASC' | 'DESC' | 'NONE';
}>({ key: '', sortDirection: 'NONE' });
const sortRow = (cellA, cellB, { key, sortDirection }) => {
setSortParams({ key, sortDirection });
};
const sortedRows = useMemo(() => {
if (sortParams.sortDirection === 'NONE') {
return tableRows;
}

const { key, sortDirection } = sortParams;
const tableHeader = tableHeaders.find((h) => h.key === key);

return tableRows?.slice().sort((a, b) => {
const sortingNum = tableHeader.sortFunc(a, b);
return sortDirection === 'DESC' ? sortingNum : -sortingNum;
});
}, [sortParams, tableRows, tableHeaders]);

return {
sortedRows,
sortRow,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { type ConfigObject } from '../config-schema';
import BiometricsChart from './biometrics-chart.component';
import PaginatedBiometrics from './paginated-biometrics.component';
import styles from './biometrics-base.scss';
import type { BiometricsTableHeader, BiometricsTableRow } from './types';

interface BiometricsBaseProps {
pageSize: number;
Expand All @@ -36,28 +37,50 @@ const BiometricsBase: React.FC<BiometricsBaseProps> = ({ patientUuid, pageSize,
[config, currentVisit],
);

const tableHeaders = [
{ key: 'date', header: t('dateAndTime', 'Date and time'), isSortable: true },
{ key: 'weight', header: withUnit(t('weight', 'Weight'), conceptUnits.get(config.concepts.weightUuid) ?? '') },
{ key: 'height', header: withUnit(t('height', 'Height'), conceptUnits.get(config.concepts.heightUuid) ?? '') },
{ key: 'bmi', header: `${t('bmi', 'BMI')} (${bmiUnit})` },
const tableHeaders: Array<BiometricsTableHeader> = [
{
key: 'muac',
key: 'dateRender',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the rationale behind the Render suffix?

Copy link
Member Author

@vasharma05 vasharma05 Apr 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are 2 kinds of data for a column (say date column):

  1. date, which is the ISO string
  2. dateRender, which is the formatted version of the pt.1 date.

Now, if we see, the user/ datatable sees the dateRender on the screen, but we want to sort the column based on the date value, hence I added the "Render" suffix to differentiate and keep the original value intact.

The sortFunc of the header will be using the date for comparison, and the DataTable will be using dateRender for rendering data in the column.

header: t('dateAndTime', 'Date and time'),
isSortable: true,
sortFunc: (valueA, valueB) => new Date(valueA.date).getTime() - new Date(valueB.date).getTime(),
},
{
key: 'weightRender',
header: withUnit(t('weight', 'Weight'), conceptUnits.get(config.concepts.weightUuid) ?? ''),
isSortable: true,
sortFunc: (valueA, valueB) => (valueA.weight && valueB.weight ? valueA.weight - valueB.weight : 0),
},
{
key: 'heightRender',
header: withUnit(t('height', 'Height'), conceptUnits.get(config.concepts.heightUuid) ?? ''),
isSortable: true,
sortFunc: (valueA, valueB) => (valueA.height && valueB.height ? valueA.height - valueB.height : 0),
},
{
key: 'bmiRender',
header: `${t('bmi', 'BMI')} (${bmiUnit})`,
isSortable: true,
sortFunc: (valueA, valueB) => (valueA.bmi && valueB.bmi ? valueA.bmi - valueB.bmi : 0),
},
{
key: 'muacRender',
header: withUnit(t('muac', 'MUAC'), conceptUnits.get(config.concepts.midUpperArmCircumferenceUuid) ?? ''),
isSortable: true,
sortFunc: (valueA, valueB) => (valueA.muac && valueB.muac ? valueA.muac - valueB.muac : 0),
},
];

const tableRows = useMemo(
const tableRows: Array<BiometricsTableRow> = useMemo(
() =>
biometrics?.map((biometricsData, index) => {
return {
...biometricsData,
id: `${index}`,
date: formatDatetime(parseDate(biometricsData.date.toString()), { mode: 'wide' }),
weight: biometricsData.weight ?? '--',
height: biometricsData.height ?? '--',
bmi: biometricsData.bmi ?? '--',
muac: biometricsData.muac ?? '--',
dateRender: formatDatetime(parseDate(biometricsData.date.toString()), { mode: 'wide' }),
weightRender: biometricsData.weight ?? '--',
heightRender: biometricsData.height ?? '--',
bmiRender: biometricsData.bmi ?? '--',
muacRender: biometricsData.muac ?? '--',
};
}),
[biometrics],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,17 @@ describe('BiometricsOverview: ', () => {

const sortRowsButton = screen.getByRole('button', { name: /date and time/i });

// Sorting in descending order
// Since the date order is already in descending order, the rows should be the same
await user.click(sortRowsButton);
// Sorting in ascending order
await user.click(sortRowsButton);

expect(screen.getAllByRole('row')).not.toEqual(initialRowElements);

// Sorting order = NONE, hence it is still in the ascending order
await user.click(sortRowsButton);
// Sorting in descending order
await user.click(sortRowsButton);

expect(screen.getAllByRole('row')).toEqual(initialRowElements);
Expand Down
Loading
Loading