From 72c37ccfb9a2f80dd89f855676b710fc3bce45f0 Mon Sep 17 00:00:00 2001 From: kyampeire Hadijah <30952856+hadijahkyampeire@users.noreply.github.com> Date: Mon, 23 Sep 2024 13:14:47 +0300 Subject: [PATCH] (Fix) Fix the Encounters pagination by leveraging the backend (#1926) * paginate encounters on the backend * fix CI * fix the tests * remove unused import --- packages/esm-commons-lib/src/api/types.ts | 2 +- .../encounter-list.component.tsx | 98 +++++++++---------- .../encounter-list/encounter-list.test.tsx | 5 +- .../src/hooks/useEncounterRows.ts | 24 +++-- .../tab-list/hts-overview-list.component.tsx | 2 +- 5 files changed, 68 insertions(+), 63 deletions(-) diff --git a/packages/esm-commons-lib/src/api/types.ts b/packages/esm-commons-lib/src/api/types.ts index 6f495f9d0..b9488ca8d 100644 --- a/packages/esm-commons-lib/src/api/types.ts +++ b/packages/esm-commons-lib/src/api/types.ts @@ -154,7 +154,7 @@ export interface Location { export interface OpenmrsEncounter extends OpenmrsResource { encounterDatetime: Date; - encounterType: string; + encounterType: { name: string; uuid: string }; patient: string; location: string; encounterProviders?: Array<{ encounterRole: string; provider: string }>; diff --git a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx index 4c4352a33..08309f6be 100644 --- a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx +++ b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.component.tsx @@ -3,7 +3,6 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { EmptyState } from '../empty-state/empty-state.component'; import { FormLauncherWithIntent } from '../ohri-form-launcher/ohri-form-launcher.component'; -import styles from './encounter-list.scss'; import { OTable } from '../data-table/o-table.component'; import { Button, Link, OverflowMenu, OverflowMenuItem, Pagination, DataTableSkeleton } from '@carbon/react'; import { Add } from '@carbon/react/icons'; @@ -14,6 +13,8 @@ import { OpenmrsEncounter } from '../../api/types'; import { useFormsJson } from '../../hooks/useFormsJson'; import { usePatientDeathStatus } from '../../hooks/usePatientDeathStatus'; +import styles from './encounter-list.scss'; + export interface EncounterListColumn { key: string; header: string; @@ -64,12 +65,15 @@ export const EncounterList: React.FC = ({ const { isDead } = usePatientDeathStatus(patientUuid); const formNames = useMemo(() => formList.map((form) => form.name), []); const { formsJson, isLoading: isLoadingFormsJson } = useFormsJson(formNames); - const { encounters, isLoading, onFormSave } = useEncounterRows( + const { encounters, total, isLoading, onFormSave } = useEncounterRows( patientUuid, encounterType, filter, afterFormSaveAction, + pageSize, + currentPage, ); + const { moduleName, workspaceWindowSize, displayText, hideFormLauncher } = launchOptions; const defaultActions = useMemo( @@ -102,38 +106,41 @@ export const EncounterList: React.FC = ({ [forms, t], ); - const handleDeleteEncounter = useCallback((encounterUuid, encounterTypeName) => { - const close = showModal('delete-encounter-modal', { - close: () => close(), - encounterTypeName: encounterTypeName || '', - onConfirmation: () => { - const abortController = new AbortController(); - deleteEncounter(encounterUuid, abortController) - .then(() => { - onFormSave(); - showSnackbar({ - isLowContrast: true, - title: t('encounterDeleted', 'Encounter deleted'), - subtitle: `Encounter ${t('successfullyDeleted', 'successfully deleted')}`, - kind: 'success', + const handleDeleteEncounter = useCallback( + (encounterUuid, encounterTypeName) => { + const close = showModal('delete-encounter-modal', { + close: () => close(), + encounterTypeName: encounterTypeName || '', + onConfirmation: () => { + const abortController = new AbortController(); + deleteEncounter(encounterUuid, abortController) + .then(() => { + onFormSave(); + showSnackbar({ + isLowContrast: true, + title: t('encounterDeleted', 'Encounter deleted'), + subtitle: `Encounter ${t('successfullyDeleted', 'successfully deleted')}`, + kind: 'success', + }); + }) + .catch(() => { + showSnackbar({ + isLowContrast: false, + title: t('error', 'Error'), + subtitle: `Encounter ${t('failedDeleting', "couldn't be deleted")}`, + kind: 'error', + }); }); - }) - .catch(() => { - showSnackbar({ - isLowContrast: false, - title: t('error', 'Error'), - subtitle: `Encounter ${t('failedDeleting', "couldn't be deleted")}`, - kind: 'error', - }); - }); - // Update encounters after deletion - const updatedEncounters = encounters.filter((enc) => enc.uuid !== encounterUuid); - constructPaginatedTableRows(updatedEncounters, currentPage, pageSize); - close(); - }, - }); - }, []); + // Update encounters after deletion + const updatedEncounters = encounters.filter((enc) => enc.uuid !== encounterUuid); + constructTableRows(updatedEncounters); + close(); + }, + }); + }, + [t, onFormSave, encounters], + ); useEffect(() => { if (!isLoadingFormsJson) { @@ -167,18 +174,11 @@ export const EncounterList: React.FC = ({ return []; }, [columns]); - const constructPaginatedTableRows = useCallback( - (encounters: OpenmrsEncounter[], currentPage: number, pageSize: number) => { - const startIndex = (currentPage - 1) * pageSize; - const paginatedEncounters = []; - for (let i = startIndex; i < startIndex + pageSize; i++) { - if (i < encounters.length) { - paginatedEncounters.push(encounters[i]); - } - } - const rows = paginatedEncounters.map((encounter) => { + const constructTableRows = useCallback( + (encounters: OpenmrsEncounter[]) => { + const rows = encounters.map((encounter) => { const tableRow: { id: string; actions: any } = { id: encounter.uuid, actions: null }; - // inject launch actions + encounter['launchFormActions'] = { editEncounter: () => launchEncounterForm( @@ -205,7 +205,7 @@ export const EncounterList: React.FC = ({ patientUuid, ), }; - // process columns + columns.forEach((column) => { let val = column?.getValue(encounter); if (column.link) { @@ -225,7 +225,7 @@ export const EncounterList: React.FC = ({ } tableRow[column.key] = val; }); - // If custom config is available, generate actions accordingly; otherwise, fallback to the default actions. + const actions = tableRow.actions?.length ? tableRow.actions : defaultActions; tableRow['actions'] = ( @@ -262,9 +262,9 @@ export const EncounterList: React.FC = ({ useEffect(() => { if (encounters?.length) { - constructPaginatedTableRows(encounters, currentPage, pageSize); + constructTableRows(encounters); } - }, [encounters, pageSize, constructPaginatedTableRows, currentPage]); + }, [encounters, pageSize, constructTableRows, currentPage]); const formLauncher = useMemo(() => { if (forms.length == 1 && !forms[0]['availableIntents']?.length) { @@ -317,7 +317,7 @@ export const EncounterList: React.FC = ({ }, [forms, hideFormLauncher, isDead, displayText, moduleName, workspaceWindowSize, onFormSave, patientUuid]); if (isLoading === true || isLoadingForms === true || isLoadingFormsJson === true) { - return ; + return ; } return ( @@ -335,7 +335,7 @@ export const EncounterList: React.FC = ({ page={currentPage} pageSize={pageSize} pageSizes={[10, 20, 30, 40, 50]} - totalItems={encounters.length} + totalItems={total} onChange={({ page, pageSize }) => { setCurrentPage(page); setPageSize(pageSize); diff --git a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx index 6c776c1c7..cf97e8122 100644 --- a/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx +++ b/packages/esm-commons-lib/src/components/encounter-list/encounter-list.test.tsx @@ -61,7 +61,7 @@ describe('EncounterList', () => { test('renders an loading state if data is loading', () => { jest .spyOn(encounterRowsHook, 'useEncounterRows') - .mockReturnValue({ encounters: [], isLoading: true, error: null, onFormSave: () => {} }); + .mockReturnValue({ encounters: [], total: 0, isLoading: true, error: null, onFormSave: () => {} }); jest.spyOn(formsJsonHook, 'useFormsJson').mockReturnValue({ formsJson: [], isLoading: true }); @@ -75,7 +75,7 @@ describe('EncounterList', () => { test('renders an empty state if data is null', () => { jest .spyOn(encounterRowsHook, 'useEncounterRows') - .mockReturnValue({ encounters: [], isLoading: false, error: null, onFormSave: () => {} }); + .mockReturnValue({ encounters: [], total: 0, isLoading: false, error: null, onFormSave: () => {} }); jest.spyOn(formsJsonHook, 'useFormsJson').mockReturnValue({ formsJson: [], isLoading: false }); @@ -90,6 +90,7 @@ describe('EncounterList', () => { test('should render the encounter list component', () => { jest.spyOn(encounterRowsHook, 'useEncounterRows').mockReturnValue({ encounters: mockEncounter, + total: 1, isLoading: false, error: null, onFormSave: () => {}, diff --git a/packages/esm-commons-lib/src/hooks/useEncounterRows.ts b/packages/esm-commons-lib/src/hooks/useEncounterRows.ts index 14240d80e..ec5d338e9 100644 --- a/packages/esm-commons-lib/src/hooks/useEncounterRows.ts +++ b/packages/esm-commons-lib/src/hooks/useEncounterRows.ts @@ -1,32 +1,35 @@ import useSWR from 'swr'; import { OpenmrsEncounter } from '../api/types'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { openmrsFetch } from '@openmrs/esm-framework'; import { encounterRepresentation } from '../constants'; +interface EncounterResponse { + results: OpenmrsEncounter[]; + totalCount?: number; +} + export function useEncounterRows( patientUuid: string, encounterType: string, encounterFilter: (encounter) => boolean, afterFormSaveAction: () => void, + pageSize?: number, + pageNumber?: number, ) { const [encounters, setEncounters] = useState([]); - const url = `/ws/rest/v1/encounter?encounterType=${encounterType}&patient=${patientUuid}&v=${encounterRepresentation}`; + const startIndex = (pageNumber - 1) * pageSize; - const { - data: response, - error, - isLoading, - mutate, - } = useSWR<{ data: { results: OpenmrsEncounter[] } }, Error>(url, openmrsFetch); + const url = `/ws/rest/v1/encounter?encounterType=${encounterType}&patient=${patientUuid}&v=${encounterRepresentation}&totalCount=true&limit=${pageSize}&startIndex=${startIndex}`; + + const { data: response, error, isLoading, mutate } = useSWR<{ data: EncounterResponse }, Error>(url, openmrsFetch); useEffect(() => { if (response) { - // sort the encounters response.data.results.sort( (a, b) => new Date(b.encounterDatetime).getTime() - new Date(a.encounterDatetime).getTime(), ); - // apply filter + if (encounterFilter) { setEncounters(response.data.results.filter((encounter) => encounterFilter(encounter))); } else { @@ -42,6 +45,7 @@ export function useEncounterRows( return { encounters, + total: response?.data?.totalCount, isLoading, error, onFormSave, diff --git a/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx b/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx index f093916fe..b15ae9c00 100644 --- a/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx +++ b/packages/esm-hiv-app/src/views/hiv-testing-services/tab-list/hts-overview-list.component.tsx @@ -29,7 +29,7 @@ const HtsOverviewList: React.FC = ({ patientUuid }) => { key: 'location', header: t('location', 'Location'), getValue: (encounter) => { - return encounter.location.name; + return encounter?.location?.name; }, }, {