From f500d955afdee67d42b0b7db2f32c1adab085639 Mon Sep 17 00:00:00 2001 From: Akshay Patel Date: Mon, 27 Apr 2020 19:58:53 +0530 Subject: [PATCH] feat: add pagination in viewing Patients list feat #1969 --- src/__tests__/patients/list/Patients.test.tsx | 62 +++++++++++++------ src/__tests__/patients/patients-slice.test.ts | 27 +++++++- 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/Patients.tsx | 57 ++++++++++++++--- src/patients/patients-slice.ts | 39 +++++++++--- 10 files changed, 283 insertions(+), 36 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/Patients.test.tsx b/src/__tests__/patients/list/Patients.test.tsx index 56cb7a3e21..970e81bc6f 100644 --- a/src/__tests__/patients/list/Patients.test.tsx +++ b/src/__tests__/patients/list/Patients.test.tsx @@ -10,6 +10,10 @@ 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 Patient from 'model/Patient' +import { UnpagedRequest } from 'clients/db/PageRequest' +import SortRequest from 'clients/db/SortRequest' import Patients from '../../../patients/list/Patients' import PatientRepository from '../../../clients/db/PatientRepository' import * as patientSlice from '../../../patients/patients-slice' @@ -18,17 +22,27 @@ 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'), ) }) @@ -111,7 +126,14 @@ describe('Patients', () => { const wrapper = setup() searchPatientsSpy.mockClear() const expectedSearchText = 'search text' - + const sortRequest: SortRequest = { + sorts: [ + { + field: 'code', + direction: 'desc', + }, + ], + } act(() => { ;(wrapper.find(TextInput).prop('onChange') as any)({ target: { @@ -130,7 +152,11 @@ describe('Patients', () => { wrapper.update() expect(searchPatientsSpy).toHaveBeenCalledTimes(1) - expect(searchPatientsSpy).toHaveBeenLastCalledWith(expectedSearchText) + expect(searchPatientsSpy).toHaveBeenLastCalledWith( + expectedSearchText, + sortRequest, + UnpagedRequest, + ) }) }) }) diff --git a/src/__tests__/patients/patients-slice.test.ts b/src/__tests__/patients/patients-slice.test.ts index b0f32778a5..f82b4fc787 100644 --- a/src/__tests__/patients/patients-slice.test.ts +++ b/src/__tests__/patients/patients-slice.test.ts @@ -1,6 +1,7 @@ import '../../__mocks__/matchMediaMock' import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' +import { UnpagedRequest } from 'clients/db/PageRequest' import patients, { fetchPatientsStart, fetchPatientsSuccess, @@ -18,14 +19,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() 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..43c08fa65f 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'] }, + }) } 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/Patients.tsx b/src/patients/list/Patients.tsx index a56f374321..b29a3eba54 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.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 SortRequest 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,8 +22,15 @@ const Patients = () => { 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 sortRequest: SortRequest = { + sorts: [ + { + field: 'code', + direction: 'desc', + }, + ], + } const setButtonToolBar = useButtonToolbarSetter() setButtonToolBar([ , ]) + 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, sortRequest, userPageRequest)) + }, [dispatch, debouncedSearchText, userPageRequest]) useEffect(() => { - dispatch(fetchPatients()) + dispatch(fetchPatients(sortRequest, userPageRequest)) return () => { setButtonToolBar([]) @@ -54,7 +91,7 @@ const Patients = () => { const listBody = ( - {patients.map((p) => ( + {patients.content.map((p) => ( history.push(`/patients/${p.id}`)}> {p.code} {p.givenName} @@ -98,8 +135,14 @@ const Patients = () => { /> - {list} + ) } diff --git a/src/patients/patients-slice.ts b/src/patients/patients-slice.ts index 73cb93f3e6..27b9d3ce1d 100644 --- a/src/patients/patients-slice.ts +++ b/src/patients/patients-slice.ts @@ -1,16 +1,32 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import SortRequest, { Unsorted } from 'clients/db/SortRequest' +import PageRequest, { UnpagedRequest } from 'clients/db/PageRequest' +import Page from 'clients/Page' import Patient from '../model/Patient' import PatientRepository from '../clients/db/PatientRepository' import { AppThunk } from '../store' interface PatientsState { isLoading: boolean - patients: Patient[] + patients: Page + pageRequest: PageRequest } const initialState: PatientsState = { isLoading: false, - patients: [], + patients: { + content: [], + hasNext: false, + hasPrevious: false, + pageRequest: { + skip: 0, + limit: 20, + }, + }, + pageRequest: { + skip: 0, + limit: 20, + }, } function startLoading(state: PatientsState) { @@ -22,7 +38,7 @@ const patientsSlice = createSlice({ initialState, reducers: { fetchPatientsStart: startLoading, - fetchPatientsSuccess(state, { payload }: PayloadAction) { + fetchPatientsSuccess(state, { payload }: PayloadAction>) { state.isLoading = false state.patients = payload }, @@ -30,20 +46,27 @@ const patientsSlice = createSlice({ }) export const { fetchPatientsStart, fetchPatientsSuccess } = patientsSlice.actions -export const fetchPatients = (): AppThunk => async (dispatch) => { +export const fetchPatients = ( + sortRequest: SortRequest, + pageRequest: PageRequest, +): AppThunk => async (dispatch) => { dispatch(fetchPatientsStart()) - const patients = await PatientRepository.findAll() + const patients = await PatientRepository.findAllPaged(sortRequest, pageRequest) dispatch(fetchPatientsSuccess(patients)) } -export const searchPatients = (searchString: string): AppThunk => async (dispatch) => { +export const searchPatients = ( + searchString: string, + sortRequest: SortRequest = Unsorted, + pageRequest: PageRequest = UnpagedRequest, +): AppThunk => async (dispatch) => { dispatch(fetchPatientsStart()) let patients if (searchString.trim() === '') { - patients = await PatientRepository.findAll() + patients = await PatientRepository.findAllPaged(sortRequest, pageRequest) } else { - patients = await PatientRepository.search(searchString) + patients = await PatientRepository.searchPaged(searchString, pageRequest) } dispatch(fetchPatientsSuccess(patients))