From 8bc3f784134f78ba7df77a2f3f8e20ed8e32f798 Mon Sep 17 00:00:00 2001 From: William Lin Date: Wed, 9 Dec 2020 23:24:46 -0500 Subject: [PATCH 1/4] feat: filter and search field added Added filter dropdown and search textbox for imagings table view. No functionality yet. --- src/imagings/model/ImagingSearchRequest.ts | 4 +- src/imagings/search/ImagingRequestTable.tsx | 92 ++++++++++++++----- src/imagings/search/ViewImagings.tsx | 10 +- src/medications/ViewMedication.tsx | 6 +- .../enUs/translations/imagings/index.ts | 6 ++ 5 files changed, 82 insertions(+), 36 deletions(-) diff --git a/src/imagings/model/ImagingSearchRequest.ts b/src/imagings/model/ImagingSearchRequest.ts index 8f90ec2437..cafaee58cf 100644 --- a/src/imagings/model/ImagingSearchRequest.ts +++ b/src/imagings/model/ImagingSearchRequest.ts @@ -1,4 +1,6 @@ +export type ImagingFilter = 'completed' | 'requested' | 'canceled' | 'all' + export default interface ImagingSearchRequest { - status: 'completed' | 'requested' | 'canceled' | 'all' + status: ImagingFilter text: string } diff --git a/src/imagings/search/ImagingRequestTable.tsx b/src/imagings/search/ImagingRequestTable.tsx index efeef47676..4f260170ef 100644 --- a/src/imagings/search/ImagingRequestTable.tsx +++ b/src/imagings/search/ImagingRequestTable.tsx @@ -1,12 +1,16 @@ -import { Table } from '@hospitalrun/components' +import { Container, Row, Column, Table } from '@hospitalrun/components' import format from 'date-fns/format' -import React from 'react' +import React, { useState } from 'react' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import Loading from '../../shared/components/Loading' import useTranslator from '../../shared/hooks/useTranslator' import { extractUsername } from '../../shared/util/extractUsername' import useImagingSearch from '../hooks/useImagingSearch' -import ImagingSearchRequest from '../model/ImagingSearchRequest' +import ImagingSearchRequest, { ImagingFilter } from '../model/ImagingSearchRequest' interface Props { searchRequest: ImagingSearchRequest @@ -17,32 +21,72 @@ const ImagingRequestTable = (props: Props) => { const { t } = useTranslator() const { data, status } = useImagingSearch(searchRequest) + const [searchFilter, setSearchFilter] = useState('all') + const [searchText, setSearchText] = useState('') + if (data === undefined || status === 'loading') { return } + const onSearchBoxChange = (event: React.ChangeEvent) => { + setSearchText(event.target.value) + } + + const filterOptions: Option[] = [ + { label: t('imagings.status.requested'), value: 'requested' }, + { label: t('imagings.status.completed'), value: 'completed' }, + { label: t('imagings.status.canceled'), value: 'canceled' }, + { label: t('imagings.filter.all'), value: 'all' }, + ] + return ( - row.id} - columns={[ - { label: t('imagings.imaging.code'), key: 'code' }, - { label: t('imagings.imaging.type'), key: 'type' }, - { - label: t('imagings.imaging.requestedOn'), - key: 'requestedOn', - formatter: (row) => - row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('imagings.imaging.patient'), key: 'fullName' }, - { - label: t('imagings.imaging.requestedBy'), - key: 'requestedBy', - formatter: (row) => extractUsername(row.requestedBy), - }, - { label: t('imagings.imaging.status'), key: 'status' }, - ]} - data={data} - /> + + + + value === searchFilter)} + onChange={(values) => setSearchFilter(values[0] as ImagingFilter)} + isEditable + /> + + + + + + +
row.id} + columns={[ + { label: t('imagings.imaging.code'), key: 'code' }, + { label: t('imagings.imaging.type'), key: 'type' }, + { + label: t('imagings.imaging.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('imagings.imaging.patient'), key: 'fullName' }, + { + label: t('imagings.imaging.requestedBy'), + key: 'requestedBy', + formatter: (row) => extractUsername(row.requestedBy), + }, + { label: t('imagings.imaging.status'), key: 'status' }, + ]} + data={data} + /> + + ) } diff --git a/src/imagings/search/ViewImagings.tsx b/src/imagings/search/ViewImagings.tsx index 8577cbdc00..8eee600990 100644 --- a/src/imagings/search/ViewImagings.tsx +++ b/src/imagings/search/ViewImagings.tsx @@ -1,4 +1,4 @@ -import { Button, Container, Row } from '@hospitalrun/components' +import { Button } from '@hospitalrun/components' import React, { useState, useEffect, useCallback } from 'react' import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' @@ -56,13 +56,7 @@ const ViewImagings = () => { } }, [getButtons, setButtons]) - return ( - - - - - - ) + return } export default ViewImagings diff --git a/src/medications/ViewMedication.tsx b/src/medications/ViewMedication.tsx index c54b9a2c1c..ee4bdb366c 100644 --- a/src/medications/ViewMedication.tsx +++ b/src/medications/ViewMedication.tsx @@ -1,4 +1,4 @@ -import { Row, Column, Badge, Button, Alert } from '@hospitalrun/components' +import { Container, Row, Column, Badge, Button, Alert } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useEffect, useState } from 'react' import { useSelector, useDispatch } from 'react-redux' @@ -164,7 +164,7 @@ const ViewMedication = () => { } return ( - <> + {status === 'error' && ( )} @@ -311,7 +311,7 @@ const ViewMedication = () => { )} - + ) } return

Loading...

diff --git a/src/shared/locales/enUs/translations/imagings/index.ts b/src/shared/locales/enUs/translations/imagings/index.ts index b98e7db19a..7c170d0a56 100644 --- a/src/shared/locales/enUs/translations/imagings/index.ts +++ b/src/shared/locales/enUs/translations/imagings/index.ts @@ -1,11 +1,17 @@ export default { imagings: { label: 'Imagings', + filterTitle: 'Filter by status', + search: 'Search imaging', + searchPlaceholder: 'X-ray, CT, PET, etc.', status: { requested: 'Requested', completed: 'Completed', canceled: 'Canceled', }, + filter: { + all: 'All Statuses', + }, requests: { label: 'Imaging Requests', new: 'New Imaging Request', From 7168f5e46e20eb024edb84511baa0a47c64537bd Mon Sep 17 00:00:00 2001 From: William Lin Date: Wed, 16 Dec 2020 01:25:25 -0500 Subject: [PATCH 2/4] feat: added filter and search functionality --- src/imagings/search/ImagingRequestTable.tsx | 47 +--------------- src/imagings/search/ViewImagings.tsx | 62 ++++++++++++++++++--- 2 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/imagings/search/ImagingRequestTable.tsx b/src/imagings/search/ImagingRequestTable.tsx index 4f260170ef..88e6c9e9ea 100644 --- a/src/imagings/search/ImagingRequestTable.tsx +++ b/src/imagings/search/ImagingRequestTable.tsx @@ -1,16 +1,12 @@ -import { Container, Row, Column, Table } from '@hospitalrun/components' +import { Container, Row, Table } from '@hospitalrun/components' import format from 'date-fns/format' -import React, { useState } from 'react' +import React from 'react' -import SelectWithLabelFormGroup, { - Option, -} from '../../shared/components/input/SelectWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import Loading from '../../shared/components/Loading' import useTranslator from '../../shared/hooks/useTranslator' import { extractUsername } from '../../shared/util/extractUsername' import useImagingSearch from '../hooks/useImagingSearch' -import ImagingSearchRequest, { ImagingFilter } from '../model/ImagingSearchRequest' +import ImagingSearchRequest from '../model/ImagingSearchRequest' interface Props { searchRequest: ImagingSearchRequest @@ -20,49 +16,12 @@ const ImagingRequestTable = (props: Props) => { const { searchRequest } = props const { t } = useTranslator() const { data, status } = useImagingSearch(searchRequest) - - const [searchFilter, setSearchFilter] = useState('all') - const [searchText, setSearchText] = useState('') - if (data === undefined || status === 'loading') { return } - const onSearchBoxChange = (event: React.ChangeEvent) => { - setSearchText(event.target.value) - } - - const filterOptions: Option[] = [ - { label: t('imagings.status.requested'), value: 'requested' }, - { label: t('imagings.status.completed'), value: 'completed' }, - { label: t('imagings.status.canceled'), value: 'canceled' }, - { label: t('imagings.filter.all'), value: 'all' }, - ] - return ( - - - value === searchFilter)} - onChange={(values) => setSearchFilter(values[0] as ImagingFilter)} - isEditable - /> - - - - -
row.id} diff --git a/src/imagings/search/ViewImagings.tsx b/src/imagings/search/ViewImagings.tsx index 8eee600990..1499b20011 100644 --- a/src/imagings/search/ViewImagings.tsx +++ b/src/imagings/search/ViewImagings.tsx @@ -1,17 +1,25 @@ -import { Button } from '@hospitalrun/components' +import { Button, Container, Row, Column } from '@hospitalrun/components' import React, { useState, useEffect, useCallback } from 'react' import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' import { useUpdateTitle } from '../../page-header/title/TitleContext' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import useDebounce from '../../shared/hooks/useDebounce' import useTranslator from '../../shared/hooks/useTranslator' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' -import ImagingSearchRequest from '../model/ImagingSearchRequest' +import ImagingSearchRequest, { ImagingFilter } from '../model/ImagingSearchRequest' import ImagingRequestTable from './ImagingRequestTable' const ViewImagings = () => { + const [searchFilter, setSearchFilter] = useState('all') + const [searchText, setSearchText] = useState('') + const debouncedSearchText = useDebounce(searchText, 500) const { t } = useTranslator() const { permissions } = useSelector((state: RootState) => state.user) const history = useHistory() @@ -20,9 +28,17 @@ const ViewImagings = () => { useEffect(() => { updateTitle(t('imagings.label')) }) + + const filterOptions: Option[] = [ + { label: t('imagings.status.requested'), value: 'requested' }, + { label: t('imagings.status.completed'), value: 'completed' }, + { label: t('imagings.status.canceled'), value: 'canceled' }, + { label: t('imagings.filter.all'), value: 'all' }, + ] + const [searchRequest, setSearchRequest] = useState({ - status: 'all', - text: '', + status: searchFilter, + text: debouncedSearchText, }) const getButtons = useCallback(() => { @@ -46,8 +62,8 @@ const ViewImagings = () => { }, [permissions, history, t]) useEffect(() => { - setSearchRequest((previousRequest) => ({ ...previousRequest, status: 'all' })) - }, []) + setSearchRequest(() => ({ text: debouncedSearchText, status: searchFilter })) + }, [searchFilter, debouncedSearchText]) useEffect(() => { setButtons(getButtons()) @@ -56,7 +72,39 @@ const ViewImagings = () => { } }, [getButtons, setButtons]) - return + const onSearchBoxChange = (event: React.ChangeEvent) => { + setSearchText(event.target.value) + } + + return ( + + + + value === searchFilter)} + onChange={(values) => setSearchFilter(values[0] as ImagingFilter)} + isEditable + /> + + + + + + + + + + ) } export default ViewImagings From e8bd29fc51cf33c579a045efbf884d4798947f28 Mon Sep 17 00:00:00 2001 From: William Lin Date: Wed, 23 Dec 2020 21:13:27 -0500 Subject: [PATCH 3/4] feat: added test for component rendering --- .../search/ImagingRequestTable.test.tsx | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx index 0f821a7708..741520864c 100644 --- a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx +++ b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx @@ -1,14 +1,26 @@ import { Table } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' import ImagingRequestTable from '../../../imagings/search/ImagingRequestTable' +import ViewImagings from '../../../imagings/search/ViewImagings' +import * as titleUtil from '../../../page-header/title/TitleContext' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import ImagingRepository from '../../../shared/db/ImagingRepository' import SortRequest from '../../../shared/db/SortRequest' import Imaging from '../../../shared/model/Imaging' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const defaultSortRequest: SortRequest = { sorts: [ { @@ -18,6 +30,8 @@ const defaultSortRequest: SortRequest = { ], } +const mockStore = createMockStore([thunk]) + describe('Imaging Request Table', () => { const expectedImaging = { code: 'I-1234', @@ -80,3 +94,56 @@ describe('Imaging Request Table', () => { expect(table.prop('data')).toEqual([expectedImaging]) }) }) + +describe('View Imagings Search', () => { + const expectedImaging = { + code: 'I-1234', + id: '1234', + type: 'imaging type', + patient: 'patient', + fullName: 'full name', + status: 'requested', + requestedOn: new Date().toISOString(), + requestedBy: 'some user', + } as Imaging + const expectedImagings = [expectedImaging] + + const setup = async (permissions: Permissions[] = []) => { + jest.resetAllMocks() + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(expectedImagings) + + const history = createMemoryHistory() + const store = mockStore({ + title: '', + user: { + permissions, + }, + } as any) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + , + ) + }) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('Should render imaging filter field', async () => { + const { wrapper } = await setup() + expect(wrapper.find(SelectWithLabelFormGroup)).toHaveLength(1) + }) + + it('Should render imaging search text field', async () => { + const { wrapper } = await setup() + expect(wrapper.find(TextInputWithLabelFormGroup)).toHaveLength(1) + }) +}) From 589b37607e947e7cc7424167591410a6f9159c50 Mon Sep 17 00:00:00 2001 From: William Lin Date: Fri, 25 Dec 2020 23:17:41 -0500 Subject: [PATCH 4/4] feat: added tests for imaging filter and search fields --- .../search/ImagingRequestTable.test.tsx | 70 +++++++++++++++++-- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx index 741520864c..dfa1dbb019 100644 --- a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx +++ b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx @@ -1,4 +1,4 @@ -import { Table } from '@hospitalrun/components' +import { Select, Table, TextInput } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -137,13 +137,69 @@ describe('View Imagings Search', () => { return { wrapper: wrapper as ReactWrapper } } - it('Should render imaging filter field', async () => { - const { wrapper } = await setup() - expect(wrapper.find(SelectWithLabelFormGroup)).toHaveLength(1) + describe('imagings dropdown filter', () => { + const searchImagingSpy = jest.spyOn(ImagingRepository, 'search') + + beforeEach(() => { + searchImagingSpy.mockClear() + }) + + it('should render imaging filter field', async () => { + const { wrapper } = await setup([Permissions.ViewImagings]) + expect(wrapper.find(SelectWithLabelFormGroup)).toHaveLength(1) + }) + + it('should filter and cause correct search results', async () => { + const { wrapper } = await setup([Permissions.ViewImagings]) + const expectedStatus = 'requested' + + await act(async () => { + const onChange = wrapper.find(Select).prop('onChange') as any + await onChange([expectedStatus]) + }) + + expect(searchImagingSpy).toHaveBeenCalledTimes(2) + expect(searchImagingSpy).toHaveBeenCalledWith( + expect.objectContaining({ status: expectedStatus }), + ) + }) }) - it('Should render imaging search text field', async () => { - const { wrapper } = await setup() - expect(wrapper.find(TextInputWithLabelFormGroup)).toHaveLength(1) + describe('imagings search field', () => { + const searchImagingSpy = jest.spyOn(ImagingRepository, 'search') + + beforeEach(() => { + searchImagingSpy.mockClear() + }) + + it('should render imaging search text field', async () => { + const { wrapper } = await setup([Permissions.ViewImagings]) + expect(wrapper.find(TextInputWithLabelFormGroup)).toHaveLength(1) + }) + + it('should search after 500 debounced typing', async () => { + const { wrapper } = await setup([Permissions.ViewImagings]) + const expectedSearchText = '1234' + + jest.useFakeTimers() + act(() => { + const onChange = wrapper.find(TextInput).prop('onChange') as any + onChange({ + target: { + value: expectedSearchText, + }, + preventDefault: jest.fn(), + }) + }) + + act(() => { + jest.advanceTimersByTime(500) + }) + + expect(searchImagingSpy).toHaveBeenCalledTimes(2) + expect(searchImagingSpy).toHaveBeenCalledWith( + expect.objectContaining({ text: expectedSearchText }), + ) + }) }) })