From dff2b3e44ee076f8154290fe9183106d6fe3f231 Mon Sep 17 00:00:00 2001 From: Akshay Patel Date: Tue, 28 Apr 2020 21:40:12 +0530 Subject: [PATCH] feat(viewpatients): add paging feature in ViewPatients component feat #1969 --- .../patients/list/ViewPatients.test.tsx | 53 +++++++++----- src/__tests__/patients/patients-slice.test.ts | 71 ++++++++++++++----- src/clients/Page.ts | 9 +++ src/clients/db/PageRequest.ts | 5 ++ src/clients/db/PatientRepository.ts | 44 ++++++++++++ src/clients/db/Repository.ts | 31 ++++++++ src/components/PageComponent.tsx | 42 +++++++++++ .../enUs/translations/actions/index.ts | 3 + src/patients/list/ViewPatients.tsx | 47 ++++++++++-- src/patients/patients-slice.ts | 39 +++++++--- 10 files changed, 297 insertions(+), 47 deletions(-) create mode 100644 src/clients/Page.ts create mode 100644 src/clients/db/PageRequest.ts create mode 100644 src/components/PageComponent.tsx diff --git a/src/__tests__/patients/list/ViewPatients.test.tsx b/src/__tests__/patients/list/ViewPatients.test.tsx index 4d13c1f516..1ef7fa6834 100644 --- a/src/__tests__/patients/list/ViewPatients.test.tsx +++ b/src/__tests__/patients/list/ViewPatients.test.tsx @@ -10,25 +10,39 @@ import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' import * as ButtonBarProvider from 'page-header/ButtonBarProvider' import format from 'date-fns/format' +import Page from 'clients/Page' +import { Unsorted } from 'clients/db/SortRequest' import ViewPatients from '../../../patients/list/ViewPatients' import PatientRepository from '../../../clients/db/PatientRepository' import * as patientSlice from '../../../patients/patients-slice' +import Patient from '../../../model/Patient' +import { UnpagedRequest } from '../../../clients/db/PageRequest' const middlewares = [thunk] const mockStore = configureStore(middlewares) describe('Patients', () => { - const patients = [ - { - id: '123', - fullName: 'test test', - givenName: 'test', - familyName: 'test', - code: 'P12345', - sex: 'male', - dateOfBirth: new Date().toISOString(), - }, - ] + const patients: Page = { + content: [ + { + id: '123', + fullName: 'test test', + isApproximateDateOfBirth: false, + givenName: 'test', + familyName: 'test', + code: 'P12345', + sex: 'male', + dateOfBirth: new Date().toISOString(), + phoneNumber: '99999999', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + rev: '', + }, + ], + hasNext: false, + hasPrevious: false, + pageRequest: UnpagedRequest, + } const mockedPatientRepository = mocked(PatientRepository, true) const setup = (isLoading?: boolean) => { @@ -36,6 +50,7 @@ describe('Patients', () => { patients: { patients, isLoading, + pageRequest: UnpagedRequest, }, }) return mount( @@ -80,12 +95,12 @@ describe('Patients', () => { expect(tableHeaders.at(3).text()).toEqual('patient.sex') expect(tableHeaders.at(4).text()).toEqual('patient.dateOfBirth') - expect(tableColumns.at(0).text()).toEqual(patients[0].code) - expect(tableColumns.at(1).text()).toEqual(patients[0].givenName) - expect(tableColumns.at(2).text()).toEqual(patients[0].familyName) - expect(tableColumns.at(3).text()).toEqual(patients[0].sex) + expect(tableColumns.at(0).text()).toEqual(patients.content[0].code) + expect(tableColumns.at(1).text()).toEqual(patients.content[0].givenName) + expect(tableColumns.at(2).text()).toEqual(patients.content[0].familyName) + expect(tableColumns.at(3).text()).toEqual(patients.content[0].sex) expect(tableColumns.at(4).text()).toEqual( - format(new Date(patients[0].dateOfBirth), 'yyyy-MM-dd'), + format(new Date(patients.content[0].dateOfBirth), 'yyyy-MM-dd'), ) }) @@ -130,7 +145,11 @@ describe('Patients', () => { wrapper.update() expect(searchPatientsSpy).toHaveBeenCalledTimes(1) - expect(searchPatientsSpy).toHaveBeenLastCalledWith(expectedSearchText) + expect(searchPatientsSpy).toHaveBeenLastCalledWith( + expectedSearchText, + Unsorted, + UnpagedRequest, + ) }) }) }) diff --git a/src/__tests__/patients/patients-slice.test.ts b/src/__tests__/patients/patients-slice.test.ts index b0f32778a5..e658a0dabe 100644 --- a/src/__tests__/patients/patients-slice.test.ts +++ b/src/__tests__/patients/patients-slice.test.ts @@ -1,12 +1,12 @@ import '../../__mocks__/matchMediaMock' import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' +import { UnpagedRequest } from 'clients/db/PageRequest' import patients, { fetchPatientsStart, fetchPatientsSuccess, searchPatients, } from '../../patients/patients-slice' -import Patient from '../../model/Patient' import PatientRepository from '../../clients/db/PatientRepository' describe('patients slice', () => { @@ -18,14 +18,34 @@ describe('patients slice', () => { it('should create the proper initial state with empty patients array', () => { const patientsStore = patients(undefined, {} as AnyAction) expect(patientsStore.isLoading).toBeFalsy() - expect(patientsStore.patients).toHaveLength(0) + expect(patientsStore.patients.content).toHaveLength(0) }) it('should handle the FETCH_PATIENTS_SUCCESS action', () => { - const expectedPatients = [{ id: '1234' }] + const expectedPatients = { + content: [ + { + id: '123', + fullName: 'test test', + isApproximateDateOfBirth: false, + givenName: 'test', + familyName: 'test', + code: 'P12345', + sex: 'male', + dateOfBirth: new Date().toISOString(), + phoneNumber: '99999999', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + rev: '', + }, + ], + hasNext: false, + hasPrevious: false, + pageRequest: UnpagedRequest, + } const patientsStore = patients(undefined, { type: fetchPatientsSuccess.type, - payload: [{ id: '1234' }], + payload: expectedPatients, }) expect(patientsStore.isLoading).toBeFalsy() @@ -43,39 +63,58 @@ describe('patients slice', () => { expect(dispatch).toHaveBeenCalledWith({ type: fetchPatientsStart.type }) }) - it('should call the PatientRepository search method with the correct search criteria', async () => { + it('should call the PatientRepository searchPaged method with the correct search criteria', async () => { const dispatch = jest.fn() const getState = jest.fn() - jest.spyOn(PatientRepository, 'search') + jest.spyOn(PatientRepository, 'searchPaged') const expectedSearchString = 'search string' await searchPatients(expectedSearchString)(dispatch, getState, null) - expect(PatientRepository.search).toHaveBeenCalledWith(expectedSearchString) + expect(PatientRepository.searchPaged).toHaveBeenCalledWith( + expectedSearchString, + UnpagedRequest, + ) }) - it('should call the PatientRepository findAll method if there is no string text', async () => { + it('should call the PatientRepository findAllPaged method if there is no string text', async () => { const dispatch = jest.fn() const getState = jest.fn() - jest.spyOn(PatientRepository, 'findAll') + jest.spyOn(PatientRepository, 'findAllPaged') await searchPatients('')(dispatch, getState, null) - expect(PatientRepository.findAll).toHaveBeenCalledTimes(1) + expect(PatientRepository.findAllPaged).toHaveBeenCalledTimes(1) }) it('should dispatch the FETCH_PATIENTS_SUCCESS action', async () => { const dispatch = jest.fn() const getState = jest.fn() - const expectedPatients = [ - { - id: '1234', - }, - ] as Patient[] + const expectedPatients = { + content: [ + { + id: '123', + fullName: 'test test', + isApproximateDateOfBirth: false, + givenName: 'test', + familyName: 'test', + code: 'P12345', + sex: 'male', + dateOfBirth: new Date().toISOString(), + phoneNumber: '99999999', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + rev: '', + }, + ], + hasNext: false, + hasPrevious: false, + pageRequest: UnpagedRequest, + } const mockedPatientRepository = mocked(PatientRepository, true) - mockedPatientRepository.search.mockResolvedValue(expectedPatients) + mockedPatientRepository.searchPaged.mockResolvedValue(expectedPatients) await searchPatients('search string')(dispatch, getState, null) diff --git a/src/clients/Page.ts b/src/clients/Page.ts new file mode 100644 index 0000000000..d9a4ce9e41 --- /dev/null +++ b/src/clients/Page.ts @@ -0,0 +1,9 @@ +import AbstractDBModel from '../model/AbstractDBModel' +import PageRequest from './db/PageRequest' + +export default interface Page { + content: T[] + hasNext?: boolean + hasPrevious?: boolean + pageRequest?: PageRequest +} diff --git a/src/clients/db/PageRequest.ts b/src/clients/db/PageRequest.ts new file mode 100644 index 0000000000..d63098d262 --- /dev/null +++ b/src/clients/db/PageRequest.ts @@ -0,0 +1,5 @@ +export default interface PageRequest { + limit: number | undefined + skip: number +} +export const UnpagedRequest: PageRequest = { limit: undefined, skip: 0 } diff --git a/src/clients/db/PatientRepository.ts b/src/clients/db/PatientRepository.ts index 5fb5937ed0..7ef4cad02b 100644 --- a/src/clients/db/PatientRepository.ts +++ b/src/clients/db/PatientRepository.ts @@ -1,7 +1,9 @@ import shortid from 'shortid' +import Page from 'clients/Page' import Patient from '../../model/Patient' import Repository from './Repository' import { patients } from '../../config/pouchdb' +import PageRequest, { UnpagedRequest } from './PageRequest' const formatPatientCode = (prefix: string, sequenceNumber: string) => `${prefix}${sequenceNumber}` @@ -10,6 +12,9 @@ const getPatientCode = (): string => formatPatientCode('P-', shortid.generate()) export class PatientRepository extends Repository { constructor() { super(patients) + patients.createIndex({ + index: { fields: ['code', 'fullName'] }, + }) } async search(text: string): Promise { @@ -29,6 +34,45 @@ export class PatientRepository extends Repository { }) } + async searchPaged( + text: string, + pageRequest: PageRequest = UnpagedRequest, + ): Promise> { + return super + .search({ + selector: { + $or: [ + { + fullName: { + $regex: RegExp(text, 'i'), + }, + }, + { + code: text, + }, + ], + }, + skip: pageRequest.skip, + limit: pageRequest.limit, + }) + .then( + (searchedData) => + new Promise>((resolve) => { + const pagedResult: Page = { + content: searchedData, + pageRequest, + hasNext: pageRequest.limit !== undefined && searchedData.length === pageRequest.limit, + hasPrevious: pageRequest.skip > 0, + } + resolve(pagedResult) + }), + ) + .catch((err) => { + console.log(err) + return err + }) + } + async save(entity: Patient): Promise { const patientCode = getPatientCode() entity.code = patientCode diff --git a/src/clients/db/Repository.ts b/src/clients/db/Repository.ts index a1d6972403..67266172ad 100644 --- a/src/clients/db/Repository.ts +++ b/src/clients/db/Repository.ts @@ -1,7 +1,9 @@ /* eslint "@typescript-eslint/camelcase": "off" */ import { v4 as uuidv4 } from 'uuid' +import Page from 'clients/Page' import AbstractDBModel from '../../model/AbstractDBModel' import { Unsorted } from './SortRequest' +import PageRequest, { UnpagedRequest } from './PageRequest' function mapDocument(document: any): any { const { _id, _rev, ...values } = document @@ -41,6 +43,35 @@ export default class Repository { return result.docs.map(mapDocument) } + async findAllPaged(sort = Unsorted, pageRequest: PageRequest = UnpagedRequest): Promise> { + const selector: any = { + _id: { $gt: null }, + } + + sort.sorts.forEach((s) => { + selector[s.field] = { $gt: null } + }) + + const result = await this.db.find({ + selector, + sort: sort.sorts.length > 0 ? sort.sorts.map((s) => ({ [s.field]: s.direction })) : undefined, + limit: pageRequest.limit, + skip: pageRequest.skip, + }) + const mappedResult = result.docs.map(mapDocument) + + const pagedResult: Page = { + content: mappedResult, + hasNext: pageRequest.limit !== undefined && mappedResult.length === pageRequest.limit, + hasPrevious: pageRequest.skip > 0, + pageRequest: { + skip: pageRequest.skip, + limit: pageRequest.limit, + }, + } + return pagedResult + } + async search(criteria: any): Promise { const response = await this.db.find(criteria) return response.docs.map(mapDocument) diff --git a/src/components/PageComponent.tsx b/src/components/PageComponent.tsx new file mode 100644 index 0000000000..9a2ed59ac1 --- /dev/null +++ b/src/components/PageComponent.tsx @@ -0,0 +1,42 @@ +import React from 'react' +import { Button } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' + +const PageComponent = ({ + hasNext, + hasPrevious, + pageNumber, + setPreviousPageRequest, + setNextPageRequest, +}: any) => { + const { t } = useTranslation() + + return ( +
+ +
+ {t('actions.page')} {pageNumber} +
+ +
+ ) +} +export default PageComponent diff --git a/src/locales/enUs/translations/actions/index.ts b/src/locales/enUs/translations/actions/index.ts index 3def1af777..9a02a66f0e 100644 --- a/src/locales/enUs/translations/actions/index.ts +++ b/src/locales/enUs/translations/actions/index.ts @@ -11,5 +11,8 @@ export default { list: 'List', search: 'Search', confirmDelete: 'Delete Confirmation', + next: 'Next', + previous: 'Previous', + page: 'Page', }, } diff --git a/src/patients/list/ViewPatients.tsx b/src/patients/list/ViewPatients.tsx index 7682463e46..60a70e06a7 100644 --- a/src/patients/list/ViewPatients.tsx +++ b/src/patients/list/ViewPatients.tsx @@ -5,6 +5,9 @@ import { useTranslation } from 'react-i18next' import { Spinner, Button, Container, Row, TextInput, Column } from '@hospitalrun/components' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import format from 'date-fns/format' +import { Unsorted } from 'clients/db/SortRequest' +import PageRequest from 'clients/db/PageRequest' +import PageComponent from 'components/PageComponent' import { RootState } from '../../store' import { fetchPatients, searchPatients } from '../patients-slice' import useTitle from '../../page-header/useTitle' @@ -19,20 +22,45 @@ const ViewPatients = () => { useTitle(t('patients.label')) useAddBreadcrumbs(breadcrumbs, true) const dispatch = useDispatch() - const { patients, isLoading } = useSelector((state: RootState) => state.patients) + const { patients, isLoading, pageRequest } = useSelector((state: RootState) => state.patients) const setButtonToolBar = useButtonToolbarSetter() + const [userPageRequest, setUserPageRequest] = useState(pageRequest) + const setNextPageRequest = () => { + setUserPageRequest((p) => { + if (p.limit) { + const newPageRequest: PageRequest = { + limit: p.limit, + skip: p.skip + p.limit, + } + return newPageRequest + } + return p + }) + } + + const setPreviousPageRequest = () => { + setUserPageRequest((p) => { + if (p.limit) { + return { + limit: p.limit, + skip: p.skip - p.limit, + } + } + return p + }) + } const [searchText, setSearchText] = useState('') const debouncedSearchText = useDebounce(searchText, 500) useEffect(() => { - dispatch(searchPatients(debouncedSearchText)) - }, [dispatch, debouncedSearchText]) + dispatch(searchPatients(debouncedSearchText, Unsorted, userPageRequest)) + }, [dispatch, debouncedSearchText, userPageRequest]) useEffect(() => { - dispatch(fetchPatients()) + dispatch(fetchPatients(Unsorted, userPageRequest)) setButtonToolBar([