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

Commit

Permalink
feat(patients): added live search to the patients search (#1970)
Browse files Browse the repository at this point in the history
  • Loading branch information
hd-genius committed Apr 7, 2020
1 parent 6e08bab commit 3ba741c
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 56 deletions.
46 changes: 46 additions & 0 deletions src/__tests__/hooks/debounce.spec.ts
@@ -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
@@ -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
@@ -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
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

1 comment on commit 3ba741c

@vercel
Copy link

@vercel vercel bot commented on 3ba741c Apr 7, 2020

Choose a reason for hiding this comment

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

Failed to assign a domain to your deployment due to the following error:

We could not create a certificate for staging.hospitalrun.io because HTTP pretest failed. Please ensure the CNAME for staging.hospitalrun.io points to "alias.zeit.co". You can find more information at https://err.sh/now-cli/cant-solve-challenge.

(Learn more or visit the non-aliased deployment)

Please sign in to comment.