Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

feat(patients): added live search to the patients search #1970

Merged
merged 6 commits into from
Apr 7, 2020
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
46 changes: 46 additions & 0 deletions src/__tests__/hooks/debounce.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { renderHook, act } from '@testing-library/react-hooks'
import useDebounce from 'hooks/debounce'

describe('useDebounce', () => {
beforeAll(() => jest.useFakeTimers())

afterAll(() => jest.useRealTimers())

it('should set the next value after the input value has not changed for the specified amount of time', () => {
const initialValue = 'initialValue'
const expectedValue = 'someValue'
const debounceDelay = 500

let currentValue = initialValue

const { rerender, result } = renderHook(() => useDebounce(currentValue, debounceDelay))

currentValue = expectedValue

act(() => {
rerender()
jest.advanceTimersByTime(debounceDelay)
})

expect(result.current).toBe(expectedValue)
})

it('should not set a new value before the specified delay has elapsed', () => {
const initialValue = 'initialValue'
const nextValue = 'someValue'
const debounceDelay = 500

let currentValue = initialValue

const { rerender, result } = renderHook(() => useDebounce(currentValue, debounceDelay))

currentValue = nextValue

act(() => {
rerender()
jest.advanceTimersByTime(debounceDelay - 1)
})

expect(result.current).toBe(initialValue)
})
})
28 changes: 9 additions & 19 deletions src/__tests__/patients/list/Patients.test.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import '../../../__mocks__/matchMediaMock'
import React from 'react'
import { mount } from 'enzyme'
import { TextInput, Button, Spinner } from '@hospitalrun/components'
import { TextInput, Spinner } from '@hospitalrun/components'
import { MemoryRouter } from 'react-router-dom'
import { Provider } from 'react-redux'
import thunk from 'redux-thunk'
Expand Down Expand Up @@ -58,15 +58,6 @@ describe('Patients', () => {
jest.restoreAllMocks()
})

it('should render a search input with button', () => {
const wrapper = setup()
const searchInput = wrapper.find(TextInput)
const searchButton = wrapper.find(Button)
expect(searchInput).toHaveLength(1)
expect(searchInput.prop('placeholder')).toEqual('actions.search')
expect(searchButton.text().trim()).toEqual('actions.search')
})

it('should render a loading bar if it is loading', () => {
const wrapper = setup(true)

Expand Down Expand Up @@ -111,10 +102,15 @@ describe('Patients', () => {
})

describe('search functionality', () => {
it('should call the searchPatients() action with the correct data', () => {
beforeEach(() => jest.useFakeTimers())

afterEach(() => jest.useRealTimers())

it('should search for patients after the search text has not changed for 500 milliseconds', () => {
const searchPatientsSpy = jest.spyOn(patientSlice, 'searchPatients')
const expectedSearchText = 'search text'
const wrapper = setup()
searchPatientsSpy.mockClear()
const expectedSearchText = 'search text'

act(() => {
;(wrapper.find(TextInput).prop('onChange') as any)({
Expand All @@ -127,14 +123,8 @@ describe('Patients', () => {
} as React.ChangeEvent<HTMLInputElement>)
})

wrapper.update()

act(() => {
;(wrapper.find(Button).prop('onClick') as any)({
preventDefault(): void {
// noop
},
} as React.MouseEvent<HTMLButtonElement>)
jest.advanceTimersByTime(500)
})

wrapper.update()
Expand Down
13 changes: 13 additions & 0 deletions src/hooks/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useState, useEffect } from 'react'

export default function <T>(value: T, delayInMilliseconds: number): T {
const [debouncedValue, setDebouncedValue] = useState(value)

useEffect(() => {
const debounceHandler = setTimeout(() => setDebouncedValue(value), delayInMilliseconds)

return () => clearTimeout(debounceHandler)
}, [value, delayInMilliseconds])

return debouncedValue
}
71 changes: 34 additions & 37 deletions src/patients/list/Patients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RootState } from '../../store'
import { fetchPatients, searchPatients } from '../patients-slice'
import useTitle from '../../page-header/useTitle'
import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs'
import useDebounce from '../../hooks/debounce'

const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }]

Expand All @@ -35,6 +36,12 @@ const Patients = () => {

const [searchText, setSearchText] = useState<string>('')

const debouncedSearchText = useDebounce(searchText, 500)

useEffect(() => {
dispatch(searchPatients(debouncedSearchText))
}, [dispatch, debouncedSearchText])

useEffect(() => {
dispatch(fetchPatients())

Expand All @@ -43,9 +50,21 @@ const Patients = () => {
}
}, [dispatch, setButtonToolBar])

if (isLoading) {
return <Spinner color="blue" loading size={[10, 25]} type="ScaleLoader" />
}
const loadingIndicator = <Spinner color="blue" loading size={[10, 25]} type="ScaleLoader" />

const listBody = (
<tbody>
{patients.map((p) => (
<tr key={p.id} onClick={() => history.push(`/patients/${p.id}`)}>
<td>{p.code}</td>
<td>{p.givenName}</td>
<td>{p.familyName}</td>
<td>{p.sex}</td>
<td>{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'yyyy-MM-dd') : ''}</td>
</tr>
))}
</tbody>
)

const list = (
<table className="table table-hover">
Expand All @@ -58,49 +77,27 @@ const Patients = () => {
<th>{t('patient.dateOfBirth')}</th>
</tr>
</thead>
<tbody>
{patients.map((p) => (
<tr key={p.id} onClick={() => history.push(`/patients/${p.id}`)}>
<td>{p.code}</td>
<td>{p.givenName}</td>
<td>{p.familyName}</td>
<td>{p.sex}</td>
<td>{p.dateOfBirth ? format(new Date(p.dateOfBirth), 'yyyy-MM-dd') : ''}</td>
</tr>
))}
</tbody>
{isLoading ? loadingIndicator : listBody}
</table>
)

const onSearchBoxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchText(event.target.value)
}

const onSearchFormSubmit = (event: React.FormEvent | React.MouseEvent) => {
event.preventDefault()
dispatch(searchPatients(searchText))
}

return (
<Container>
<form className="form" onSubmit={onSearchFormSubmit}>
<Row>
<Column md={10}>
<TextInput
size="lg"
type="text"
onChange={onSearchBoxChange}
value={searchText}
placeholder={t('actions.search')}
/>
</Column>
<Column md={2}>
<Button size="large" onClick={onSearchFormSubmit}>
{t('actions.search')}
</Button>
</Column>
</Row>
</form>
<Row>
<Column md={12}>
<TextInput
size="lg"
type="text"
onChange={onSearchBoxChange}
value={searchText}
placeholder={t('actions.search')}
/>
</Column>
</Row>

<Row>{list}</Row>
</Container>
Expand Down