From 68d6297735b13783212a1c3787d8b3a00b8aa446 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 13:22:13 -0400 Subject: [PATCH 01/86] feat: added permission for viewing widgets to Permissions model --- src/shared/model/Permissions.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/model/Permissions.ts b/src/shared/model/Permissions.ts index e11b06cf8f..e61eb33155 100644 --- a/src/shared/model/Permissions.ts +++ b/src/shared/model/Permissions.ts @@ -21,6 +21,7 @@ enum Permissions { ReadVisits = 'read:visit', RequestImaging = 'write:imaging', ViewImagings = 'read:imagings', + ViewIncidentWidgets = 'read:incident_widgets', } export default Permissions From e23c913ab0a6f68aaa86e74142a25c5448878694 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 13:27:20 -0400 Subject: [PATCH 02/86] feat: added an entry in pageMap for incident infographic --- src/shared/components/navbar/pageMap.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/shared/components/navbar/pageMap.tsx b/src/shared/components/navbar/pageMap.tsx index bab446729b..fdae7d3388 100644 --- a/src/shared/components/navbar/pageMap.tsx +++ b/src/shared/components/navbar/pageMap.tsx @@ -59,6 +59,12 @@ const pageMap: { path: '/incidents', icon: 'incident', }, + viewIncidentWidgets: { + permission: Permissions.ViewIncidentWidgets, + label: 'incidents.visualize.label', + path: '/incidents/visualize', + icon: 'incident', + }, newVisit: { permission: Permissions.AddVisit, label: 'visits.visit.new', From d73a3e7109a5923eccebc79f768fc09958e2746c Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 13:39:23 -0400 Subject: [PATCH 03/86] feat: added ListItem component to incidents in Sidebar for visualize tab --- src/shared/components/Sidebar.tsx | 11 +++++++++++ src/user/user-slice.ts | 1 + 2 files changed, 12 insertions(+) diff --git a/src/shared/components/Sidebar.tsx b/src/shared/components/Sidebar.tsx index 204fc28961..2e6fb0b389 100644 --- a/src/shared/components/Sidebar.tsx +++ b/src/shared/components/Sidebar.tsx @@ -294,6 +294,17 @@ const Sidebar = () => { {!sidebarCollapsed && t('incidents.reports.label')} )} + {permissions.includes(Permissions.ViewIncidentWidgets) && ( + navigateTo('/incidents/visualize')} + active={splittedPath[1].includes('incidents') && splittedPath.length < 3} + > + + {!sidebarCollapsed && t('incidents.visualize.label')} + + )} )} diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 84af456645..738f25119c 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -36,6 +36,7 @@ const initialState: UserState = { Permissions.ViewIncidents, Permissions.ReportIncident, Permissions.ResolveIncident, + Permissions.ViewIncidentWidgets, Permissions.AddCarePlan, Permissions.ReadCarePlan, Permissions.AddVisit, From a10574874fa673fd2414234d61079e16a13a7050 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 13:51:33 -0400 Subject: [PATCH 04/86] test: implemented unit tests for visualize tab --- .../shared/components/Sidebar.test.tsx | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 5a141f838e..fa89becfaa 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -31,6 +31,7 @@ describe('Sidebar', () => { Permissions.ViewLabs, Permissions.ViewIncidents, Permissions.ViewIncident, + Permissions.ViewIncidentWidgets, Permissions.ReportIncident, Permissions.ReadVisits, Permissions.AddVisit, @@ -473,6 +474,24 @@ describe('Sidebar', () => { }) }) + it('should render the incidents visualize link', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(8).text().trim()).toEqual('incidents.visualize.label') + }) + + it('should not render the incidents visualize link when user does not have the view incident widgets privileges', () => { + const wrapper = setupNoPermissions('/incidents') + + const listItems = wrapper.find(ListItem) + + listItems.forEach((_, i) => { + expect(listItems.at(i).text().trim()).not.toEqual('incidents.visualize.label') + }) + }) + it('main incidents link should be active when the current path is /incidents', () => { const wrapper = setup('/incidents') @@ -515,6 +534,19 @@ describe('Sidebar', () => { expect(history.location.pathname).toEqual('/incidents/new') }) + it('should navigate to /incidents/visualize when the incidents visualize link is clicked', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(8).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/incidents/visualize') + }) + it('incidents list link should be active when the current path is /incidents', () => { const wrapper = setup('/incidents') From 9dc78558affd1419801a91e060d0b412b4c09072 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 14:11:27 -0400 Subject: [PATCH 05/86] feat: added route for visualize component and performed sanity check --- src/incidents/Incidents.tsx | 7 +++++ .../visualize/VisualizeIncidents.tsx | 31 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 src/incidents/visualize/VisualizeIncidents.tsx diff --git a/src/incidents/Incidents.tsx b/src/incidents/Incidents.tsx index 7b19355a56..2dcd39be90 100644 --- a/src/incidents/Incidents.tsx +++ b/src/incidents/Incidents.tsx @@ -9,6 +9,7 @@ import { RootState } from '../shared/store' import ViewIncidents from './list/ViewIncidents' import ReportIncident from './report/ReportIncident' import ViewIncident from './view/ViewIncident' +import VisualizeIncidents from './visualize/VisualizeIncidents' const Incidents = () => { const { permissions } = useSelector((state: RootState) => state.user) @@ -34,6 +35,12 @@ const Incidents = () => { path="/incidents/new" component={ReportIncident} /> + ( + // const dispatch = useDispatch() + // const { t } = useTranslator() + // const history = useHistory() + // const { id } = useParams() + // const { incident } = useSelector((state: RootState) => state.incident) + // useTitle(incident ? incident.code : '') + // const breadcrumbs = [ + // { + // i18nKey: incident ? incident.code : '', + // location: `/incidents/${id}`, + // }, + // ] + // useAddBreadcrumbs(breadcrumbs) + + <> +

Hello from VisualizeIncidents.tsx!

+ +) + +export default VisualizeIncidents From c6f17b3b1b98b80aae7eb805fb2f2571c2b850b9 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 14:30:28 -0400 Subject: [PATCH 06/86] feat: added test cases for visualize route --- src/__tests__/incidents/Incidents.test.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx index 41d551c003..43a32abfbe 100644 --- a/src/__tests__/incidents/Incidents.test.tsx +++ b/src/__tests__/incidents/Incidents.test.tsx @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Incidents from '../../incidents/Incidents' import ReportIncident from '../../incidents/report/ReportIncident' import ViewIncident from '../../incidents/view/ViewIncident' +import VisualizeIncidents from '../../incidents/visualize/VisualizeIncidents' import IncidentRepository from '../../shared/db/IncidentRepository' import Incident from '../../shared/model/Incident' import Permissions from '../../shared/model/Permissions' @@ -63,6 +64,20 @@ describe('Incidents', () => { }) }) + describe('/incidents/visualize', () => { + it('should render the incident visualize screen when /incidents/visualize is accessed', async () => { + const { wrapper } = await setup([Permissions.ViewIncidentWidgets], '/incidents/visualize') + + expect(wrapper.find(VisualizeIncidents)).toHaveLength(1) + }) + + it('should not navigate to /incidents/visualize if the user does not have ViewIncidentWidgets permissions', async () => { + const { wrapper } = await setup([], '/incidents/visualize') + + expect(wrapper.find(VisualizeIncidents)).toHaveLength(0) + }) + }) + describe('/incidents/:id', () => { it('should render the view incident screen when /incidents/:id is accessed', async () => { const { wrapper } = await setup([Permissions.ViewIncident], '/incidents/1234') From 8d04353bfe0ef3a89d6d93e8dfedcbd928376a8e Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 12 Aug 2020 15:58:35 -0400 Subject: [PATCH 07/86] feat: implemented use of hook to retrieve reported incidents --- .../visualize/VisualizeIncidents.tsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 62d4b1aea1..3ef5892c75 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,31 +1,25 @@ -import {} from '@hospitalrun/components' +import { Spinner } from '@hospitalrun/components' import React from 'react' -// import { useDispatch, useSelector } from 'react-redux' -// import { useParams, useHistory } from 'react-router-dom' -// import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -// import useTitle from '../../page-header/title/useTitle' -// import useTranslator from '../../shared/hooks/useTranslator' -// import { RootState } from '../../shared/store' +import useIncidents from '../hooks/useIncidents' +import IncidentFilter from '../IncidentFilter' +import IncidentSearchRequest from '../model/IncidentSearchRequest' -const VisualizeIncidents = () => ( - // const dispatch = useDispatch() - // const { t } = useTranslator() - // const history = useHistory() - // const { id } = useParams() - // const { incident } = useSelector((state: RootState) => state.incident) - // useTitle(incident ? incident.code : '') - // const breadcrumbs = [ - // { - // i18nKey: incident ? incident.code : '', - // location: `/incidents/${id}`, - // }, - // ] - // useAddBreadcrumbs(breadcrumbs) +const VisualizeIncidents = () => { + const searchFilter = IncidentFilter.reported + const searchRequest: IncidentSearchRequest = { status: searchFilter } + const { data, isLoading } = useIncidents(searchRequest) - <> -

Hello from VisualizeIncidents.tsx!

- -) + if (data === undefined || isLoading) { + return + } + + console.log('data: ', data) + return ( + <> +

Hello from Visualize Incidents

+ + ) +} export default VisualizeIncidents From 6575a7403bcc920b56ae6903232cf3ee8d09952b Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 16 Aug 2020 14:01:01 +0800 Subject: [PATCH 08/86] feat(download csv of incident table): uses json2csv. filters data updates package.json re #2292 --- package.json | 2 + src/incidents/list/ViewIncidentsTable.tsx | 120 +++++++++++++++++----- 2 files changed, 95 insertions(+), 27 deletions(-) diff --git a/package.json b/package.json index 5df817faf6..abf0c57f4e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@hospitalrun/components": "~1.16.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", + "@types/json2csv": "~5.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", "date-fns": "~2.15.0", @@ -15,6 +16,7 @@ "i18next": "~19.6.0", "i18next-browser-languagedetector": "~6.0.0", "i18next-xhr-backend": "~3.2.2", + "json2csv": "~5.0.1", "lodash": "^4.17.15", "node-sass": "~4.14.0", "pouchdb": "~7.2.1", diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index d57532e3f4..b4a57cc58b 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,5 +1,6 @@ -import { Spinner, Table } from '@hospitalrun/components' +import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' +import { Parser } from 'json2csv' import React from 'react' import { useHistory } from 'react-router' @@ -22,33 +23,98 @@ function ViewIncidentsTable(props: Props) { return } + // filter data + const exportData = [{}] + let first = true + if (data != null) { + data.forEach((elm) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + exportData[0] = entry + first = false + } else { + exportData.push(entry) + } + }) + } + + function downloadCSV() { + const fields = Object.keys(exportData[0]) + const opts = { fields } + const parser = new Parser(opts) + const csv = parser.parse(exportData) + console.log(csv) + + const filename = 'IncidenntsCSV.csv' + const text = csv + const element = document.createElement('a') + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', filename) + + element.style.display = 'none' + document.body.appendChild(element) + + element.click() + + document.body.removeChild(element) + } + + const dropdownItems = [ + { + onClick: function runfun() { + downloadCSV() + }, + text: 'CSV', + }, + ] + return ( - row.id} - data={data} - columns={[ - { label: t('incidents.reports.code'), key: 'code' }, - { - label: t('incidents.reports.dateOfIncident'), - key: 'date', - formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), - }, - { - label: t('incidents.reports.reportedBy'), - key: 'reportedBy', - formatter: (row) => extractUsername(row.reportedBy), - }, - { - label: t('incidents.reports.reportedOn'), - key: 'reportedOn', - formatter: (row) => - row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('incidents.reports.status'), key: 'status' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => history.push(`incidents/${row.id}`) }]} - /> + <> +
row.id} + data={data} + columns={[ + { + label: t('incidents.reports.code'), + key: 'code', + }, + { + label: t('incidents.reports.dateOfIncident'), + key: 'date', + formatter: (row) => (row.date ? format(new Date(row.date), 'yyyy-MM-dd hh:mm a') : ''), + }, + { + label: t('incidents.reports.reportedBy'), + key: 'reportedBy', + formatter: (row) => extractUsername(row.reportedBy), + }, + { + label: t('incidents.reports.reportedOn'), + key: 'reportedOn', + formatter: (row) => + row.reportedOn ? format(new Date(row.reportedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { + label: t('incidents.reports.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`incidents/${row.id}`), + }, + ]} + /> + + ) } From 7cc66ebe530e2449e6ae6e733a600d5b6881d2a7 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Sun, 16 Aug 2020 12:44:33 -0400 Subject: [PATCH 09/86] feat: imported LineGraph component and rendered it with dumby data --- .../visualize/VisualizeIncidents.tsx | 42 ++++++++++++++++++- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 3ef5892c75..5ee8b32097 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,4 +1,4 @@ -import { Spinner } from '@hospitalrun/components' +import { Spinner, LineGraph } from '@hospitalrun/components' import React from 'react' import useIncidents from '../hooks/useIncidents' @@ -14,10 +14,48 @@ const VisualizeIncidents = () => { return } + // reportedOn: "2020-08-12T19:53:30.153Z" + // we can use a function that splices the string at position 6-7 to get the month + console.log('data: ', data) return ( <> -

Hello from Visualize Incidents

+ ) } From 88bf19ba01edd47b0de7f60a8125e378645f258f Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Sun, 16 Aug 2020 12:51:03 -0400 Subject: [PATCH 10/86] style: made the graph look better --- src/incidents/visualize/VisualizeIncidents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 5ee8b32097..6ab624aaae 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -24,7 +24,7 @@ const VisualizeIncidents = () => { datasets={[ { backgroundColor: 'blue', - borderColor: 'red', + borderColor: 'black', data: [ { x: 'January', From fd2ec6961f51dc7259b2c8872e60461a4eb1ae38 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Mon, 17 Aug 2020 19:06:26 +0800 Subject: [PATCH 11/86] fix(typo): fixed typo and used locale re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index b4a57cc58b..c52f3f1de0 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -51,7 +51,9 @@ function ViewIncidentsTable(props: Props) { const csv = parser.parse(exportData) console.log(csv) - const filename = 'IncidenntsCSV.csv' + const incidentsText = t('incidents.label') + const filename = incidentsText.concat('.csv') + const text = csv const element = document.createElement('a') element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) From a8d34e14ab89447bd2753248038907c89801f327 Mon Sep 17 00:00:00 2001 From: blestab Date: Mon, 17 Aug 2020 15:20:58 +0200 Subject: [PATCH 12/86] fix(shared): pouchdb auth - add skip_setup flag fix #2256 --- src/shared/config/pouchdb.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/config/pouchdb.ts b/src/shared/config/pouchdb.ts index cb4c3fdb90..f45dd8252c 100644 --- a/src/shared/config/pouchdb.ts +++ b/src/shared/config/pouchdb.ts @@ -18,14 +18,14 @@ let serverDb let localDb if (process.env.NODE_ENV === 'test') { - serverDb = new PouchDB('hospitalrun', { adapter: 'memory' }) - localDb = new PouchDB('local_hospitalrun', { adapter: 'memory' }) + serverDb = new PouchDB('hospitalrun', { skip_setup: true, adapter: 'memory' }) + localDb = new PouchDB('local_hospitalrun', { skip_setup: true, adapter: 'memory' }) } else { serverDb = new PouchDB(`${process.env.REACT_APP_HOSPITALRUN_API}/hospitalrun`, { skip_setup: true, }) - localDb = new PouchDB('local_hospitalrun') + localDb = new PouchDB('local_hospitalrun', { skip_setup: true }) localDb .sync(serverDb, { live: true, retry: true }) .on('change', (info) => { From 84af4b31de1b7de628ba425a9545c06c3aec7ffc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 15:46:21 +0000 Subject: [PATCH 13/86] build(deps-dev): bump @types/node from 14.0.27 to 14.6.0 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.0.27 to 14.6.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5df817faf6..57786a070b 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", "@types/lodash": "^4.14.150", - "@types/node": "~14.0.0", + "@types/node": "~14.6.0", "@types/pouchdb": "~6.4.0", "@types/react": "~16.9.17", "@types/react-dom": "~16.9.4", From 29a603e0db5df1a94aece929a2314cc812bf6dca Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Mon, 17 Aug 2020 22:42:07 -0400 Subject: [PATCH 14/86] feat: partial implementation of useEffect hook --- .../visualize/VisualizeIncidents.tsx | 61 ++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 6ab624aaae..e9b4f16de9 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,5 +1,5 @@ -import { Spinner, LineGraph } from '@hospitalrun/components' -import React from 'react' +import { LineGraph } from '@hospitalrun/components' +import React, { useEffect, useState } from 'react' import useIncidents from '../hooks/useIncidents' import IncidentFilter from '../IncidentFilter' @@ -9,15 +9,62 @@ const VisualizeIncidents = () => { const searchFilter = IncidentFilter.reported const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) + const [monthlyIncidents, setMonthlyIncidents] = useState({ + January: 0, + February: 0, + March: 0, + April: 0, + May: 0, + June: 0, + July: 0, + August: 0, + September: 0, + November: 0, + December: 0, + }) - if (data === undefined || isLoading) { - return + const getIncidentMonth = (reportedOn: string) => { + // reportedOn: "2020-08-12T19:53:30.153Z" + // splices the data.reportedOn string at position 5-6 to get the month + const months = [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'November', + 'December', + ] + return months[Number(reportedOn.slice(5, 7)) - 1] } - // reportedOn: "2020-08-12T19:53:30.153Z" - // we can use a function that splices the string at position 6-7 to get the month + useEffect(() => { + if (data === undefined || isLoading) { + console.log('data is undefined') + } else { + let incidentMonth: string + const totalIncidents: number = data.length + for (let incident = 0; incident < totalIncidents; incident += 1) { + incidentMonth = getIncidentMonth(data[incident].reportedOn) + setMonthlyIncidents((state) => ({ + ...state, + // incidentMonth: incidentMonth + 1, + })) + console.log('incidentMonth: ', incidentMonth) + } + } + }, []) + + // if (data === undefined || isLoading) { + // return + // } + + console.log('August: ', monthlyIncidents.August) - console.log('data: ', data) return ( <> Date: Mon, 17 Aug 2020 23:15:40 -0400 Subject: [PATCH 15/86] feat: triggers a re-render on useEffect when data loads --- src/incidents/visualize/VisualizeIncidents.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index e9b4f16de9..a401eaca7e 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -46,6 +46,7 @@ const VisualizeIncidents = () => { if (data === undefined || isLoading) { console.log('data is undefined') } else { + console.log('data:', data) let incidentMonth: string const totalIncidents: number = data.length for (let incident = 0; incident < totalIncidents; incident += 1) { @@ -57,7 +58,7 @@ const VisualizeIncidents = () => { console.log('incidentMonth: ', incidentMonth) } } - }, []) + }, [data]) // if (data === undefined || isLoading) { // return From a5f60b228ceccc7030b36c4498ec1c227897092a Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Tue, 18 Aug 2020 20:55:03 +0800 Subject: [PATCH 16/86] feat(incidentscsv): fixes re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 59 ++++++++++++------- .../enUs/translations/incidents/index.ts | 1 + 2 files changed, 40 insertions(+), 20 deletions(-) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index c52f3f1de0..eecad38e6e 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -25,34 +25,42 @@ function ViewIncidentsTable(props: Props) { // filter data const exportData = [{}] - let first = true - if (data != null) { - data.forEach((elm) => { - const entry = { - code: elm.code, - date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), - reportedBy: elm.reportedBy, - reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), - status: elm.status, - } - if (first) { - exportData[0] = entry - first = false - } else { - exportData.push(entry) - } - }) + + function populateExportData() { + let first = true + if (data != null) { + data.forEach((elm) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + exportData[0] = entry + first = false + } else { + exportData.push(entry) + } + }) + } } function downloadCSV() { + populateExportData() + const fields = Object.keys(exportData[0]) const opts = { fields } const parser = new Parser(opts) const csv = parser.parse(exportData) - console.log(csv) const incidentsText = t('incidents.label') - const filename = incidentsText.concat('.csv') + + const filename = incidentsText + .concat('-') + .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) + .concat('.csv') const text = csv const element = document.createElement('a') @@ -76,8 +84,20 @@ function ViewIncidentsTable(props: Props) { }, ] + const dropStyle = { + marginLeft: 'auto', // note the capital 'W' here + marginBottom: '4px', // 'ms' is the only lowercase vendor prefix + } + return ( <> +
row.id} data={data} @@ -115,7 +135,6 @@ function ViewIncidentsTable(props: Props) { }, ]} /> - ) } diff --git a/src/shared/locales/enUs/translations/incidents/index.ts b/src/shared/locales/enUs/translations/incidents/index.ts index 6223bafb29..f39d87cb59 100644 --- a/src/shared/locales/enUs/translations/incidents/index.ts +++ b/src/shared/locales/enUs/translations/incidents/index.ts @@ -17,6 +17,7 @@ export default { resolve: 'Resolve Incident', dateOfIncident: 'Date of Incident', department: 'Department', + download: 'Download', category: 'Category', categoryItem: 'Category Item', description: 'Description of Incident', From f2bc985fd90878d9b084598693afd817e77f855c Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Wed, 19 Aug 2020 23:43:01 -0400 Subject: [PATCH 17/86] feat: incidents per month are now managed in state --- .../visualize/VisualizeIncidents.tsx | 72 ++++++++----------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index a401eaca7e..425c8b55d8 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -9,62 +9,48 @@ const VisualizeIncidents = () => { const searchFilter = IncidentFilter.reported const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) - const [monthlyIncidents, setMonthlyIncidents] = useState({ - January: 0, - February: 0, - March: 0, - April: 0, - May: 0, - June: 0, - July: 0, - August: 0, - September: 0, - November: 0, - December: 0, - }) + const [monthlyIncidents, setMonthlyIncidents] = useState([ + // monthlyIncidents[0] -> January ... monthlyIncidents[11] -> December + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + ]) - const getIncidentMonth = (reportedOn: string) => { - // reportedOn: "2020-08-12T19:53:30.153Z" - // splices the data.reportedOn string at position 5-6 to get the month - const months = [ - 'January', - 'February', - 'March', - 'April', - 'May', - 'June', - 'July', - 'August', - 'September', - 'November', - 'December', - ] - return months[Number(reportedOn.slice(5, 7)) - 1] + const handleUpdate = (incidentMonth: number) => { + console.log('monthlyIncidents:', monthlyIncidents) + const newMonthlyIncidents = [...monthlyIncidents] + newMonthlyIncidents[incidentMonth] += 1 + console.log('newMonthlyIncidents: ', newMonthlyIncidents) + setMonthlyIncidents(newMonthlyIncidents) } + const getIncidentMonth = (reportedOn: string) => + // reportedOn: "2020-08-12T19:53:30.153Z" + Number(reportedOn.slice(5, 7)) - 1 + useEffect(() => { if (data === undefined || isLoading) { console.log('data is undefined') } else { - console.log('data:', data) - let incidentMonth: string const totalIncidents: number = data.length for (let incident = 0; incident < totalIncidents; incident += 1) { - incidentMonth = getIncidentMonth(data[incident].reportedOn) - setMonthlyIncidents((state) => ({ - ...state, - // incidentMonth: incidentMonth + 1, - })) - console.log('incidentMonth: ', incidentMonth) + const incidentMonth = getIncidentMonth(data[incident].reportedOn) + console.log('iteration number ', incident) + handleUpdate(incidentMonth) } } }, [data]) - // if (data === undefined || isLoading) { - // return - // } - - console.log('August: ', monthlyIncidents.August) + // console.log("after updating: ", monthlyIncidents) return ( <> From d3cdfa2ef370257ace3f8a0c495129b97594e9a8 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Thu, 20 Aug 2020 01:19:33 -0400 Subject: [PATCH 18/86] style: cleaned up the code --- src/incidents/visualize/VisualizeIncidents.tsx | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 425c8b55d8..8aa4786bf7 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -25,33 +25,28 @@ const VisualizeIncidents = () => { 0, ]) + const getIncidentMonth = (reportedOn: string) => + // reportedOn: "2020-08-12T19:53:30.153Z" + Number(reportedOn.slice(5, 7)) - 1 + const handleUpdate = (incidentMonth: number) => { - console.log('monthlyIncidents:', monthlyIncidents) const newMonthlyIncidents = [...monthlyIncidents] newMonthlyIncidents[incidentMonth] += 1 - console.log('newMonthlyIncidents: ', newMonthlyIncidents) setMonthlyIncidents(newMonthlyIncidents) } - const getIncidentMonth = (reportedOn: string) => - // reportedOn: "2020-08-12T19:53:30.153Z" - Number(reportedOn.slice(5, 7)) - 1 - useEffect(() => { if (data === undefined || isLoading) { - console.log('data is undefined') + // const spinner = } else { const totalIncidents: number = data.length for (let incident = 0; incident < totalIncidents; incident += 1) { const incidentMonth = getIncidentMonth(data[incident].reportedOn) - console.log('iteration number ', incident) handleUpdate(incidentMonth) } } }, [data]) - // console.log("after updating: ", monthlyIncidents) - return ( <> Date: Thu, 20 Aug 2020 03:26:12 -0400 Subject: [PATCH 19/86] feat: updated useEffect dependency and implemented map state of monthlyIncidents is accurately reflecting incident per month --- src/incidents/visualize/VisualizeIncidents.tsx | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 8aa4786bf7..a650262e02 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -9,6 +9,7 @@ const VisualizeIncidents = () => { const searchFilter = IncidentFilter.reported const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) + const [incident, setIncident] = useState(0) const [monthlyIncidents, setMonthlyIncidents] = useState([ // monthlyIncidents[0] -> January ... monthlyIncidents[11] -> December 0, @@ -29,23 +30,20 @@ const VisualizeIncidents = () => { // reportedOn: "2020-08-12T19:53:30.153Z" Number(reportedOn.slice(5, 7)) - 1 - const handleUpdate = (incidentMonth: number) => { - const newMonthlyIncidents = [...monthlyIncidents] - newMonthlyIncidents[incidentMonth] += 1 - setMonthlyIncidents(newMonthlyIncidents) - } - useEffect(() => { if (data === undefined || isLoading) { // const spinner = } else { const totalIncidents: number = data.length - for (let incident = 0; incident < totalIncidents; incident += 1) { + if (totalIncidents > incident) { const incidentMonth = getIncidentMonth(data[incident].reportedOn) - handleUpdate(incidentMonth) + setMonthlyIncidents((prevIncidents) => + prevIncidents.map((value, index) => (index === incidentMonth ? value + 1 : value)), + ) + setIncident(incident + 1) } } - }, [data]) + }, [data, monthlyIncidents]) return ( <> From 56ae1b844fb881bf1c29cc826cb2a80099cde545 Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Thu, 20 Aug 2020 12:32:39 -0400 Subject: [PATCH 20/86] feat: linegraph component now renders dynamic data stored in state --- .../visualize/VisualizeIncidents.tsx | 54 ++++++++++++++++--- 1 file changed, 48 insertions(+), 6 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index a650262e02..9b4d7ef6bd 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,4 +1,4 @@ -import { LineGraph } from '@hospitalrun/components' +import { LineGraph, Spinner } from '@hospitalrun/components' import React, { useEffect, useState } from 'react' import useIncidents from '../hooks/useIncidents' @@ -10,6 +10,7 @@ const VisualizeIncidents = () => { const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) const [incident, setIncident] = useState(0) + const [showGraph, setShowGraph] = useState(false) const [monthlyIncidents, setMonthlyIncidents] = useState([ // monthlyIncidents[0] -> January ... monthlyIncidents[11] -> December 0, @@ -32,7 +33,7 @@ const VisualizeIncidents = () => { useEffect(() => { if (data === undefined || isLoading) { - // const spinner = + // incidents data not loaded yet, do nothing } else { const totalIncidents: number = data.length if (totalIncidents > incident) { @@ -41,11 +42,16 @@ const VisualizeIncidents = () => { prevIncidents.map((value, index) => (index === incidentMonth ? value + 1 : value)), ) setIncident(incident + 1) + } else if (totalIncidents === incident) { + // incidents data finished processing + setShowGraph(true) } } }, [data, monthlyIncidents]) - return ( + return !showGraph ? ( + + ) : ( <> { data: [ { x: 'January', - y: 12, + y: monthlyIncidents[0], }, { x: 'February', - y: 11, + y: monthlyIncidents[1], }, { x: 'March', - y: 10, + y: monthlyIncidents[2], + }, + { + x: 'April', + y: monthlyIncidents[3], + }, + { + x: 'May', + y: monthlyIncidents[4], + }, + { + x: 'June', + y: monthlyIncidents[5], + }, + { + x: 'July', + y: monthlyIncidents[6], + }, + { + x: 'August', + y: monthlyIncidents[7], + }, + { + x: 'September', + y: monthlyIncidents[8], + }, + { + x: 'October', + y: monthlyIncidents[9], + }, + { + x: 'November', + y: monthlyIncidents[10], + }, + { + x: 'December', + y: monthlyIncidents[11], }, ], label: 'Incidents', From bb4b9fb682b0d1bac90094acbf32795708bcf97e Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Thu, 20 Aug 2020 15:21:09 -0400 Subject: [PATCH 21/86] feat: use of Array.fill in monthlyIncidents useState --- src/incidents/visualize/VisualizeIncidents.tsx | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 9b4d7ef6bd..92718f366d 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -11,21 +11,7 @@ const VisualizeIncidents = () => { const { data, isLoading } = useIncidents(searchRequest) const [incident, setIncident] = useState(0) const [showGraph, setShowGraph] = useState(false) - const [monthlyIncidents, setMonthlyIncidents] = useState([ - // monthlyIncidents[0] -> January ... monthlyIncidents[11] -> December - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - 0, - ]) + const [monthlyIncidents, setMonthlyIncidents] = useState(Array(12).fill(0)) const getIncidentMonth = (reportedOn: string) => // reportedOn: "2020-08-12T19:53:30.153Z" From 1fb36c97f16760ba8867e61d3413f34b3611a7be Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 23 Aug 2020 20:03:42 +0800 Subject: [PATCH 22/86] feat(incidentscsv): made functions more abstract made getcsv and downloadlink functions re #2292 --- src/incidents/list/ViewIncidentsTable.tsx | 19 +++---------------- src/shared/util/DataHelpers.tsx | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+), 16 deletions(-) create mode 100644 src/shared/util/DataHelpers.tsx diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index eecad38e6e..4c77371a48 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -1,10 +1,10 @@ import { Spinner, Table, Dropdown } from '@hospitalrun/components' import format from 'date-fns/format' -import { Parser } from 'json2csv' import React from 'react' import { useHistory } from 'react-router' import useTranslator from '../../shared/hooks/useTranslator' +import { DownloadLink, getCSV } from '../../shared/util/DataHelpers' import { extractUsername } from '../../shared/util/extractUsername' import useIncidents from '../hooks/useIncidents' import IncidentSearchRequest from '../model/IncidentSearchRequest' @@ -50,10 +50,7 @@ function ViewIncidentsTable(props: Props) { function downloadCSV() { populateExportData() - const fields = Object.keys(exportData[0]) - const opts = { fields } - const parser = new Parser(opts) - const csv = parser.parse(exportData) + const csv = getCSV(exportData) const incidentsText = t('incidents.label') @@ -62,17 +59,7 @@ function ViewIncidentsTable(props: Props) { .concat(format(new Date(Date.now()), 'yyyy-MM-dd--hh-mma')) .concat('.csv') - const text = csv - const element = document.createElement('a') - element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) - element.setAttribute('download', filename) - - element.style.display = 'none' - document.body.appendChild(element) - - element.click() - - document.body.removeChild(element) + DownloadLink(csv, filename) } const dropdownItems = [ diff --git a/src/shared/util/DataHelpers.tsx b/src/shared/util/DataHelpers.tsx new file mode 100644 index 0000000000..c8797f3235 --- /dev/null +++ b/src/shared/util/DataHelpers.tsx @@ -0,0 +1,22 @@ +import { Parser } from 'json2csv' + +export function getCSV(data: T[]): string { + const fields = Object.keys(data[0]) + const opts = { fields } + const parser = new Parser(opts) + const csv = parser.parse(data) + return csv +} + +export function DownloadLink(data: string, fileName: string) { + const text = data + const element = document.createElement('a') + element.setAttribute('href', `data:text/plain;charset=utf-8,${encodeURIComponent(text)}`) + element.setAttribute('download', fileName) + + element.style.display = 'none' + document.body.appendChild(element) + element.click() + + return document.body.removeChild(element) +} From e51463b26e32d4d70a6aa38f9a3fb0f357e60320 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2020 06:17:45 +0000 Subject: [PATCH 23/86] build(deps-dev): bump ts-jest from 26.2.0 to 26.3.0 Bumps [ts-jest](https://github.com/kulshekhar/ts-jest) from 26.2.0 to 26.3.0. - [Release notes](https://github.com/kulshekhar/ts-jest/releases) - [Changelog](https://github.com/kulshekhar/ts-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/kulshekhar/ts-jest/compare/v26.2.0...v26.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8f1b944b9d..4272adb220 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "rimraf": "~3.0.2", "source-map-explorer": "^2.2.2", "standard-version": "~9.0.0", - "ts-jest": "~26.2.0" + "ts-jest": "~26.3.0" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", From 3c728cc06b9e07c2e638c215914ecd537a276a18 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 27 Aug 2020 15:46:13 +0000 Subject: [PATCH 24/86] build(deps-dev): bump cz-conventional-changelog from 3.2.1 to 3.3.0 Bumps [cz-conventional-changelog](https://github.com/commitizen/cz-conventional-changelog) from 3.2.1 to 3.3.0. - [Release notes](https://github.com/commitizen/cz-conventional-changelog/releases) - [Commits](https://github.com/commitizen/cz-conventional-changelog/compare/v3.2.1...v3.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4272adb220..7803a6330c 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,7 @@ "commitizen": "~4.2.0", "commitlint-config-cz": "~0.13.0", "cross-env": "~7.0.0", - "cz-conventional-changelog": "~3.2.0", + "cz-conventional-changelog": "~3.3.0", "dateformat": "~3.0.3", "enzyme": "~3.11.0", "enzyme-adapter-react-16": "~1.15.2", From 7a4c52dad22c47777fecd1b425e32800442d9785 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2020 06:46:28 +0000 Subject: [PATCH 25/86] build(deps): bump date-fns from 2.15.0 to 2.16.0 Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.15.0 to 2.16.0. - [Release notes](https://github.com/date-fns/date-fns/releases) - [Changelog](https://github.com/date-fns/date-fns/blob/master/CHANGELOG.md) - [Commits](https://github.com/date-fns/date-fns/compare/v2.15.0...v2.16.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7803a6330c..74c49878a8 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", - "date-fns": "~2.15.0", + "date-fns": "~2.16.0", "escape-string-regexp": "~4.0.0", "i18next": "~19.7.0", "i18next-browser-languagedetector": "~6.0.0", From d939bb2a69adb58262696f62dcf2c0ee453c6815 Mon Sep 17 00:00:00 2001 From: blestab Date: Sat, 29 Aug 2020 14:24:19 +0200 Subject: [PATCH 26/86] fix(shared): update couchdb auth local.ini httpd section --- couchdb/local.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/couchdb/local.ini b/couchdb/local.ini index 70c17e9c0f..35be728c69 100644 --- a/couchdb/local.ini +++ b/couchdb/local.ini @@ -4,6 +4,7 @@ users_db_security_editable = true [httpd] enable_cors = true +WWW-Authenticate = Other realm="app" [cors] origins = * From 937f78ef1d1a4e0ffb79582f27b3e60603f76249 Mon Sep 17 00:00:00 2001 From: blestab Date: Sat, 29 Aug 2020 14:35:19 +0200 Subject: [PATCH 27/86] fix(shared): couchdb auth popup local.ini httpd section update fix #2256 --- couchdb/local.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/couchdb/local.ini b/couchdb/local.ini index 35be728c69..9775ad68cb 100644 --- a/couchdb/local.ini +++ b/couchdb/local.ini @@ -4,6 +4,7 @@ users_db_security_editable = true [httpd] enable_cors = true +; Replace default WWW-Authenticate = Basic realm="administrator" WWW-Authenticate = Other realm="app" [cors] From e06fbc3e1746ea36d6de1f3d4837cc92f2c40bdb Mon Sep 17 00:00:00 2001 From: Yoseph Ahmed Date: Mon, 31 Aug 2020 00:44:29 -0400 Subject: [PATCH 28/86] feat: added label for visualize and fixed title --- src/incidents/visualize/VisualizeIncidents.tsx | 4 ++++ src/shared/locales/enUs/translations/incidents/index.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 92718f366d..0071cacbcf 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,11 +1,15 @@ import { LineGraph, Spinner } from '@hospitalrun/components' import React, { useEffect, useState } from 'react' +import useTitle from '../../page-header/title/useTitle' +import useTranslator from '../../shared/hooks/useTranslator' import useIncidents from '../hooks/useIncidents' import IncidentFilter from '../IncidentFilter' import IncidentSearchRequest from '../model/IncidentSearchRequest' const VisualizeIncidents = () => { + const { t } = useTranslator() + useTitle(t('incidents.visualize.view')) const searchFilter = IncidentFilter.reported const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) diff --git a/src/shared/locales/enUs/translations/incidents/index.ts b/src/shared/locales/enUs/translations/incidents/index.ts index 6223bafb29..94029c1580 100644 --- a/src/shared/locales/enUs/translations/incidents/index.ts +++ b/src/shared/locales/enUs/translations/incidents/index.ts @@ -34,5 +34,9 @@ export default { descriptionRequired: 'Description is required', }, }, + visualize: { + label: 'Visualize', + view: 'Visualize Incidents', + }, }, } From 9c2baf3ee0652d6cad71c55eea296bf822c6990c Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 31 Aug 2020 22:20:28 -0500 Subject: [PATCH 29/86] refactor(imaging): add useImagingRequest hook --- .../imagings/hooks/useImagingRequest.test.tsx | 32 +++++++++++++++++++ src/imagings/hooks/useImagingRequest.tsx | 12 +++++++ 2 files changed, 44 insertions(+) create mode 100644 src/__tests__/imagings/hooks/useImagingRequest.test.tsx create mode 100644 src/imagings/hooks/useImagingRequest.tsx diff --git a/src/__tests__/imagings/hooks/useImagingRequest.test.tsx b/src/__tests__/imagings/hooks/useImagingRequest.test.tsx new file mode 100644 index 0000000000..98b8b46d7d --- /dev/null +++ b/src/__tests__/imagings/hooks/useImagingRequest.test.tsx @@ -0,0 +1,32 @@ +import { renderHook, act } from '@testing-library/react-hooks' + +import useImagingRequest from '../../../imagings/hooks/useImagingRequest' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' + +describe('useImagingRequest', () => { + it('should get an imaging request by id', async () => { + const expectedImagingId = 'some id' + const expectedImagingRequest = { + id: expectedImagingId, + patient: 'some patient', + visitId: 'visit id', + status: 'requested', + type: 'some type', + } as Imaging + jest.spyOn(ImagingRepository, 'find').mockResolvedValue(expectedImagingRequest) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useImagingRequest(expectedImagingId)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(ImagingRepository.find).toHaveBeenCalledTimes(1) + expect(ImagingRepository.find).toBeCalledWith(expectedImagingId) + expect(actualData).toEqual(expectedImagingRequest) + }) +}) diff --git a/src/imagings/hooks/useImagingRequest.tsx b/src/imagings/hooks/useImagingRequest.tsx new file mode 100644 index 0000000000..758736888c --- /dev/null +++ b/src/imagings/hooks/useImagingRequest.tsx @@ -0,0 +1,12 @@ +import { QueryKey, useQuery } from 'react-query' + +import ImagingRepository from '../../shared/db/ImagingRepository' +import Imaging from '../../shared/model/Imaging' + +function getImagingRequestById(_: QueryKey, imagingRequestId: string): Promise { + return ImagingRepository.find(imagingRequestId) +} + +export default function useImagingRequest(imagingRequestId: string) { + return useQuery(['imaging', imagingRequestId], getImagingRequestById) +} From 7fe842fc39d538d718088710896b37e66b6077ee Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 31 Aug 2020 22:29:07 -0500 Subject: [PATCH 30/86] refactor(imaging): add useImagingSearch hook --- .../imagings/hooks/useImagingSearch.test.tsx | 43 +++++++++++++++++++ src/imagings/hooks/useImagingSearch.tsx | 26 +++++++++++ src/imagings/model/ImagingSearchRequest.ts | 4 ++ 3 files changed, 73 insertions(+) create mode 100644 src/__tests__/imagings/hooks/useImagingSearch.test.tsx create mode 100644 src/imagings/hooks/useImagingSearch.tsx create mode 100644 src/imagings/model/ImagingSearchRequest.ts diff --git a/src/__tests__/imagings/hooks/useImagingSearch.test.tsx b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx new file mode 100644 index 0000000000..9b107ff1a8 --- /dev/null +++ b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx @@ -0,0 +1,43 @@ +import { act, renderHook } from '@testing-library/react-hooks' + +import useImagingSearch from '../../../imagings/hooks/useImagingSearch' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Imaging from '../../../shared/model/Imaging' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' +import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('useImagingSearch', () => { + it('it should search imaging requests', async () => { + const expectedSearchRequest: ImagingSearchRequest = { + status: 'completed', + text: 'some search request', + } + const expectedImagingRequests = [{ id: 'some id' }] as Imaging[] + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(expectedImagingRequests) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useImagingSearch(expectedSearchRequest)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(ImagingRepository.search).toHaveBeenCalledTimes(1) + expect(ImagingRepository.search).toBeCalledWith({ + ...expectedSearchRequest, + defaultSortRequest, + }) + expect(actualData).toEqual(expectedImagingRequests) + }) +}) diff --git a/src/imagings/hooks/useImagingSearch.tsx b/src/imagings/hooks/useImagingSearch.tsx new file mode 100644 index 0000000000..7c80f18536 --- /dev/null +++ b/src/imagings/hooks/useImagingSearch.tsx @@ -0,0 +1,26 @@ +import { QueryKey, useQuery } from 'react-query' + +import ImagingSearchRequest from '../model/ImagingSearchRequest' +import ImagingRepository from '../../shared/db/ImagingRepository' +import SortRequest from '../../shared/db/SortRequest' +import Imaging from '../../shared/model/Imaging' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +function searchImagingRequests( + _: QueryKey, + searchRequest: ImagingSearchRequest, +): Promise { + return ImagingRepository.search({ ...searchRequest, defaultSortRequest }) +} + +export default function useImagingSearch(searchRequest: ImagingSearchRequest) { + return useQuery(['imagings', searchRequest], searchImagingRequests) +} diff --git a/src/imagings/model/ImagingSearchRequest.ts b/src/imagings/model/ImagingSearchRequest.ts new file mode 100644 index 0000000000..030e4dfc8e --- /dev/null +++ b/src/imagings/model/ImagingSearchRequest.ts @@ -0,0 +1,4 @@ +export default interface ImagingSearchRequest { + status: 'completed' | 'requested' | 'canceled' + text: string +} From 502a113e4c92cb8a721158f5f4ce75d239720d72 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 31 Aug 2020 22:55:47 -0500 Subject: [PATCH 31/86] refactor(imaging): add useRequestImaging hook --- .../imagings/hooks/useRequestImaging.test.tsx | 55 +++++++++++++++++++ .../util/validate-imaging-request.test.ts | 17 ++++++ src/imagings/hooks/useRequestImaging.tsx | 32 +++++++++++ src/imagings/util/validate-imaging-request.ts | 38 +++++++++++++ 4 files changed, 142 insertions(+) create mode 100644 src/__tests__/imagings/hooks/useRequestImaging.test.tsx create mode 100644 src/__tests__/imagings/util/validate-imaging-request.test.ts create mode 100644 src/imagings/hooks/useRequestImaging.tsx create mode 100644 src/imagings/util/validate-imaging-request.ts diff --git a/src/__tests__/imagings/hooks/useRequestImaging.test.tsx b/src/__tests__/imagings/hooks/useRequestImaging.test.tsx new file mode 100644 index 0000000000..ad7385bd9e --- /dev/null +++ b/src/__tests__/imagings/hooks/useRequestImaging.test.tsx @@ -0,0 +1,55 @@ +/* eslint-disable no-console */ + +import useRequestImaging from '../../../imagings/hooks/useRequestImaging' +import { ImagingRequestError } from '../../../imagings/util/validate-imaging-request' +import * as imagingRequestValidator from '../../../imagings/util/validate-imaging-request' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('useReportIncident', () => { + beforeEach(() => { + jest.restoreAllMocks() + console.error = jest.fn() + }) + + it('should save the imaging request with correct data', async () => { + const expectedDate = new Date(Date.now()) + Date.now = jest.fn().mockReturnValue(expectedDate) + const givenImagingRequest = { + patient: 'some patient', + fullName: 'some full name', + status: 'requested', + type: 'some type', + notes: 'some notes', + visitId: 'some visit id', + } as Imaging + + const expectedImagingRequest = { + ...givenImagingRequest, + requestedOn: expectedDate.toISOString(), + requestedBy: 'test', + } as Imaging + jest.spyOn(ImagingRepository, 'save').mockResolvedValue(expectedImagingRequest) + + await executeMutation(() => useRequestImaging(), givenImagingRequest) + expect(ImagingRepository.save).toHaveBeenCalledTimes(1) + expect(ImagingRepository.save).toBeCalledWith(expectedImagingRequest) + }) + + it('should throw an error if validation fails', async () => { + const expectedImagingRequestError = { + patient: 'some patient error', + } as ImagingRequestError + + jest.spyOn(imagingRequestValidator, 'default').mockReturnValue(expectedImagingRequestError) + jest.spyOn(ImagingRepository, 'save').mockResolvedValue({} as Imaging) + + try { + await executeMutation(() => useRequestImaging(), {}) + } catch (e) { + expect(e).toEqual(expectedImagingRequestError) + expect(ImagingRepository.save).not.toHaveBeenCalled() + } + }) +}) diff --git a/src/__tests__/imagings/util/validate-imaging-request.test.ts b/src/__tests__/imagings/util/validate-imaging-request.test.ts new file mode 100644 index 0000000000..af3f23e659 --- /dev/null +++ b/src/__tests__/imagings/util/validate-imaging-request.test.ts @@ -0,0 +1,17 @@ +import validateImagingRequest from '../../../imagings/util/validate-imaging-request' +import Imaging from '../../../shared/model/Imaging' + +describe('imaging request validator', () => { + it('should validate the required fields apart of an incident', async () => { + const newImagingRequest = {} as Imaging + const expectedError = { + patient: 'imagings.requests.error.patientRequired', + status: 'imagings.requests.error.statusRequired', + type: 'imagings.requests.error.typeRequired', + } + + const actualError = validateImagingRequest(newImagingRequest) + + expect(actualError).toEqual(expectedError) + }) +}) diff --git a/src/imagings/hooks/useRequestImaging.tsx b/src/imagings/hooks/useRequestImaging.tsx new file mode 100644 index 0000000000..09633edbf9 --- /dev/null +++ b/src/imagings/hooks/useRequestImaging.tsx @@ -0,0 +1,32 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import ImagingRepository from '../../shared/db/ImagingRepository' +import AbstractDBModel from '../../shared/model/AbstractDBModel' +import Imaging from '../../shared/model/Imaging' +import validateImagingRequest from '../util/validate-imaging-request' + +type ImagingRequest = Omit + +async function requestImaging(request: ImagingRequest): Promise { + const error = validateImagingRequest(request) + + if (!isEmpty(error)) { + throw error + } + + await ImagingRepository.save({ + ...request, + requestedBy: 'test', + requestedOn: new Date(Date.now()).toISOString(), + } as Imaging) +} + +export default function useRequestImaging() { + return useMutation(requestImaging, { + onSuccess: async () => { + await queryCache.invalidateQueries('imagings') + }, + throwOnError: true, + }) +} diff --git a/src/imagings/util/validate-imaging-request.ts b/src/imagings/util/validate-imaging-request.ts new file mode 100644 index 0000000000..d077ecc0cc --- /dev/null +++ b/src/imagings/util/validate-imaging-request.ts @@ -0,0 +1,38 @@ +import Imaging from '../../shared/model/Imaging' + +const statusType: string[] = ['requested', 'completed', 'canceled'] + +export class ImagingRequestError extends Error { + patient?: string + + type?: string + + status?: string + + constructor(message: string, patient?: string, type?: string, status?: string) { + super(message) + this.patient = patient + this.type = type + this.status = status + Object.setPrototypeOf(this, ImagingRequestError.prototype) + } +} + +export default function validateImagingRequest(request: Partial) { + const imagingRequestError = {} as any + if (!request.patient) { + imagingRequestError.patient = 'imagings.requests.error.patientRequired' + } + + if (!request.type) { + imagingRequestError.type = 'imagings.requests.error.typeRequired' + } + + if (!request.status) { + imagingRequestError.status = 'imagings.requests.error.statusRequired' + } else if (!statusType.includes(request.status)) { + imagingRequestError.status = 'imagings.requests.error.incorrectStatus' + } + + return imagingRequestError +} From ecc5b53de8dda7a56bb0671f56563b5bd61c3d4d Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 31 Aug 2020 22:56:08 -0500 Subject: [PATCH 32/86] style: fix lint --- src/__tests__/imagings/hooks/useImagingSearch.test.tsx | 2 +- src/imagings/hooks/useImagingSearch.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/imagings/hooks/useImagingSearch.test.tsx b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx index 9b107ff1a8..8a50eeccee 100644 --- a/src/__tests__/imagings/hooks/useImagingSearch.test.tsx +++ b/src/__tests__/imagings/hooks/useImagingSearch.test.tsx @@ -1,11 +1,11 @@ import { act, renderHook } from '@testing-library/react-hooks' import useImagingSearch from '../../../imagings/hooks/useImagingSearch' +import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' import ImagingRepository from '../../../shared/db/ImagingRepository' import SortRequest from '../../../shared/db/SortRequest' import Imaging from '../../../shared/model/Imaging' import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' -import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' const defaultSortRequest: SortRequest = { sorts: [ diff --git a/src/imagings/hooks/useImagingSearch.tsx b/src/imagings/hooks/useImagingSearch.tsx index 7c80f18536..77e067d05b 100644 --- a/src/imagings/hooks/useImagingSearch.tsx +++ b/src/imagings/hooks/useImagingSearch.tsx @@ -1,9 +1,9 @@ import { QueryKey, useQuery } from 'react-query' -import ImagingSearchRequest from '../model/ImagingSearchRequest' import ImagingRepository from '../../shared/db/ImagingRepository' import SortRequest from '../../shared/db/SortRequest' import Imaging from '../../shared/model/Imaging' +import ImagingSearchRequest from '../model/ImagingSearchRequest' const defaultSortRequest: SortRequest = { sorts: [ From 2baa7f7f40f8d9ccbeca1f1a0e19110db57ab535 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 31 Aug 2020 23:50:35 -0500 Subject: [PATCH 33/86] refactor(imaging): use hooks instead of redux to get imaging requests --- src/__tests__/HospitalRun.test.tsx | 5 +- src/__tests__/imagings/imaging-slice.test.ts | 122 --------------- src/__tests__/imagings/imagings-slice.test.ts | 146 ------------------ .../requests/NewImagingRequest.test.tsx | 54 ++----- .../search/ImagingRequestTable.test.tsx | 82 ++++++++++ .../{ => search}/ViewImagings.test.tsx | 47 ++---- src/imagings/Imagings.tsx | 2 +- src/imagings/ViewImagings.tsx | 92 ----------- src/imagings/hooks/useRequestImaging.tsx | 10 +- src/imagings/imaging-slice.ts | 104 ------------- src/imagings/imagings-slice.ts | 66 -------- src/imagings/model/ImagingSearchRequest.ts | 2 +- src/imagings/requests/NewImagingRequest.tsx | 38 +++-- src/imagings/search/ImagingRequestTable.tsx | 49 ++++++ src/imagings/search/ViewImagings.tsx | 64 ++++++++ src/shared/store/index.ts | 4 - 16 files changed, 250 insertions(+), 637 deletions(-) delete mode 100644 src/__tests__/imagings/imaging-slice.test.ts delete mode 100644 src/__tests__/imagings/imagings-slice.test.ts create mode 100644 src/__tests__/imagings/search/ImagingRequestTable.test.tsx rename src/__tests__/imagings/{ => search}/ViewImagings.test.tsx (62%) delete mode 100644 src/imagings/ViewImagings.tsx delete mode 100644 src/imagings/imaging-slice.ts delete mode 100644 src/imagings/imagings-slice.ts create mode 100644 src/imagings/search/ImagingRequestTable.tsx create mode 100644 src/imagings/search/ViewImagings.tsx diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 072611fc1f..82616b535b 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -9,7 +9,7 @@ import thunk from 'redux-thunk' import Dashboard from '../dashboard/Dashboard' import HospitalRun from '../HospitalRun' -import ViewImagings from '../imagings/ViewImagings' +import ViewImagings from '../imagings/search/ViewImagings' import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' import ViewMedications from '../medications/ViewMedications' @@ -224,11 +224,10 @@ describe('HospitalRun', () => { describe('/imaging', () => { it('should render the Imagings component when /imaging is accessed', async () => { - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) + jest.spyOn(ImagingRepository, 'search').mockResolvedValue([]) const store = mockStore({ title: 'test', user: { user: { id: '123' }, permissions: [Permissions.ViewImagings] }, - imagings: { imagings: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) diff --git a/src/__tests__/imagings/imaging-slice.test.ts b/src/__tests__/imagings/imaging-slice.test.ts deleted file mode 100644 index 9a674ae194..0000000000 --- a/src/__tests__/imagings/imaging-slice.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' - -import imagingSlice, { - requestImaging, - requestImagingStart, - requestImagingSuccess, - requestImagingError, -} from '../../imagings/imaging-slice' -import ImagingRepository from '../../shared/db/ImagingRepository' -import Imaging from '../../shared/model/Imaging' -import { RootState } from '../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('imaging slice', () => { - describe('reducers', () => { - describe('requestImagingStart', () => { - it('should set status to loading', async () => { - const imagingStore = imagingSlice(undefined, requestImagingStart()) - - expect(imagingStore.status).toEqual('loading') - }) - }) - - describe('requestImagingSuccess', () => { - it('should set the imaging and status to success', () => { - const expectedImaging = { id: 'imagingId' } as Imaging - - const imagingStore = imagingSlice(undefined, requestImagingSuccess(expectedImaging)) - - expect(imagingStore.status).toEqual('completed') - expect(imagingStore.imaging).toEqual(expectedImaging) - }) - }) - - describe('requestImagingError', () => { - const expectedError = { message: 'some message', result: 'some result error' } - - const imagingStore = imagingSlice(undefined, requestImagingError(expectedError)) - - expect(imagingStore.status).toEqual('error') - expect(imagingStore.error).toEqual(expectedError) - }) - - describe('request imaging', () => { - const mockImaging = { - id: 'imagingId', - type: 'imagingType', - patient: 'patient', - status: 'requested', - } as Imaging - let imagingRepositorySaveSpy: any - - beforeEach(() => { - jest.restoreAllMocks() - Date.now = jest.fn().mockReturnValue(new Date().valueOf()) - imagingRepositorySaveSpy = jest - .spyOn(ImagingRepository, 'save') - .mockResolvedValue(mockImaging) - }) - - it('should request a new imaging', async () => { - const store = mockStore({ - user: { - user: { - id: '1234', - }, - }, - } as any) - - const expectedRequestedImaging = { - ...mockImaging, - requestedOn: new Date(Date.now()).toISOString(), - requestedBy: store.getState().user.user.id, - } as Imaging - - await store.dispatch(requestImaging(mockImaging)) - - const actions = store.getActions() - - expect(actions[0]).toEqual(requestImagingStart()) - expect(imagingRepositorySaveSpy).toHaveBeenCalledWith(expectedRequestedImaging) - expect(actions[1]).toEqual(requestImagingSuccess(expectedRequestedImaging)) - }) - - it('should execute the onSuccess callback if provided', async () => { - const store = mockStore({ - user: { - user: { - id: 'fake id', - }, - }, - } as any) - const onSuccessSpy = jest.fn() - - await store.dispatch(requestImaging(mockImaging, onSuccessSpy)) - expect(onSuccessSpy).toHaveBeenCalledWith(mockImaging) - }) - - it('should validate that the imaging can be requested', async () => { - const store = mockStore() - const onSuccessSpy = jest.fn() - await store.dispatch(requestImaging({} as Imaging, onSuccessSpy)) - - const actions = store.getActions() - - expect(actions[0]).toEqual(requestImagingStart()) - expect(actions[1]).toEqual( - requestImagingError({ - patient: 'imagings.requests.error.patientRequired', - type: 'imagings.requests.error.typeRequired', - status: 'imagings.requests.error.statusRequired', - message: 'imagings.requests.error.unableToRequest', - }), - ) - expect(imagingRepositorySaveSpy).not.toHaveBeenCalled() - expect(onSuccessSpy).not.toHaveBeenCalled() - }) - }) - }) -}) diff --git a/src/__tests__/imagings/imagings-slice.test.ts b/src/__tests__/imagings/imagings-slice.test.ts deleted file mode 100644 index e4b5447d59..0000000000 --- a/src/__tests__/imagings/imagings-slice.test.ts +++ /dev/null @@ -1,146 +0,0 @@ -import { AnyAction } from 'redux' -import { mocked } from 'ts-jest/utils' - -import imagings, { - fetchImagingsStart, - fetchImagingsSuccess, - searchImagings, -} from '../../imagings/imagings-slice' -import ImagingRepository from '../../shared/db/ImagingRepository' -import SortRequest from '../../shared/db/SortRequest' -import Imaging from '../../shared/model/Imaging' - -interface SearchContainer { - text: string - status: 'requested' | 'completed' | 'canceled' | 'all' - defaultSortRequest: SortRequest -} - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const expectedSearchObject: SearchContainer = { - text: 'search string', - status: 'all', - defaultSortRequest, -} - -describe('imagings slice', () => { - beforeEach(() => { - jest.resetAllMocks() - }) - - describe('imagings reducer', () => { - it('should create the proper intial state with empty imagings array', () => { - const imagingsStore = imagings(undefined, {} as AnyAction) - expect(imagingsStore.isLoading).toBeFalsy() - expect(imagingsStore.imagings).toHaveLength(0) - expect(imagingsStore.statusFilter).toEqual('all') - }) - - it('it should handle the FETCH_IMAGINGS_SUCCESS action', () => { - const expectedImagings = [{ id: '1234' }] - const imagingsStore = imagings(undefined, { - type: fetchImagingsSuccess.type, - payload: expectedImagings, - }) - - expect(imagingsStore.isLoading).toBeFalsy() - expect(imagingsStore.imagings).toEqual(expectedImagings) - }) - }) - - describe('searchImagings', () => { - it('should dispatch the FETCH_IMAGINGS_START action', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - - await searchImagings('search string', 'all')(dispatch, getState, null) - - expect(dispatch).toHaveBeenCalledWith({ type: fetchImagingsStart.type }) - }) - - it('should call the ImagingRepository search method with the correct search criteria', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(ImagingRepository, 'search') - - await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - - it('should call the ImagingRepository findAll method if there is no string text and status is set to all', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(ImagingRepository, 'findAll') - - await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) - - expect(ImagingRepository.findAll).toHaveBeenCalledTimes(1) - }) - - it('should dispatch the FETCH_IMAGINGS_SUCCESS action', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - - const expectedImagings = [ - { - type: 'text', - }, - ] as Imaging[] - - const mockedImagingRepository = mocked(ImagingRepository, true) - mockedImagingRepository.search.mockResolvedValue(expectedImagings) - - await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(dispatch).toHaveBeenLastCalledWith({ - type: fetchImagingsSuccess.type, - payload: expectedImagings, - }) - }) - }) - - describe('sort Request', () => { - it('should have called findAll with sort request in searchImagings method', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(ImagingRepository, 'findAll') - - await searchImagings('', expectedSearchObject.status)(dispatch, getState, null) - - expect(ImagingRepository.findAll).toHaveBeenCalledWith( - expectedSearchObject.defaultSortRequest, - ) - }) - - it('should include sorts in the search criteria', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(ImagingRepository, 'search') - - await searchImagings(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(ImagingRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - }) -}) diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx index 6d21d587f5..21aacc5d8b 100644 --- a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -1,4 +1,4 @@ -import { Button, Typeahead, Label, Alert } from '@hospitalrun/components' +import { Button, Typeahead, Label } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -26,7 +26,7 @@ describe('New Imaging Request', () => { let history: any let setButtonToolBarSpy: any - const setup = async (status: string, error: any = {}) => { + const setup = async () => { jest.resetAllMocks() jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() @@ -37,11 +37,6 @@ describe('New Imaging Request', () => { history.push(`/imaging/new`) const store = mockStore({ title: '', - user: { user: { id: '1234' } }, - imaging: { - status, - error, - }, } as any) let wrapper: any @@ -64,14 +59,14 @@ describe('New Imaging Request', () => { describe('title and breadcrumbs', () => { it('should have New Imaging Request as the title', async () => { - await setup('loading', {}) + await setup() expect(titleUtil.default).toHaveBeenCalledWith('imagings.requests.new') }) }) describe('form layout', () => { it('should render a patient typeahead', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -87,7 +82,7 @@ describe('New Imaging Request', () => { }) it('should render a dropdown list of visits', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const visitsTypeSelect = wrapper.find('.visits').find(SelectWithLabelFormGroup) expect(visitsTypeSelect).toBeDefined() expect(visitsTypeSelect.prop('label')).toEqual('patient.visits.label') @@ -95,7 +90,7 @@ describe('New Imaging Request', () => { }) it('should render a type input box', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) expect(typeInputBox).toBeDefined() @@ -105,7 +100,7 @@ describe('New Imaging Request', () => { }) it('should render a status types select', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const statusTypesSelect = wrapper.find('.imaging-status').find(SelectWithLabelFormGroup) expect(statusTypesSelect).toBeDefined() @@ -122,7 +117,7 @@ describe('New Imaging Request', () => { }) it('should render a notes text field', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -132,48 +127,23 @@ describe('New Imaging Request', () => { }) it('should render a save button', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('actions.save') }) it('should render a cancel button', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') }) }) - describe('errors', () => { - const error = { - message: 'some message', - patient: 'some patient message', - type: 'some type error', - status: 'status type error', - } - - it('should display errors', async () => { - const wrapper = await setup('error', error) - const alert = wrapper.find(Alert) - const typeInput = wrapper.find(TextInputWithLabelFormGroup) - const patientTypeahead = wrapper.find(Typeahead) - - expect(alert.prop('message')).toEqual(error.message) - expect(alert.prop('title')).toEqual('states.error') - expect(alert.prop('color')).toEqual('danger') - - expect(patientTypeahead.prop('isInvalid')).toBeTruthy() - - expect(typeInput.prop('feedback')).toEqual(error.type) - expect(typeInput.prop('isInvalid')).toBeTruthy() - }) - }) - describe('on cancel', () => { it('should navigate back to /imaging', async () => { - const wrapper = await setup('loading', {}) + const wrapper = await setup() const cancelButton = wrapper.find(Button).at(1) act(() => { @@ -198,7 +168,7 @@ describe('New Imaging Request', () => { requestedOn: expectedDate.toISOString(), } as Imaging - const wrapper = await setup('loading', {}) + const wrapper = await setup() jest.spyOn(ImagingRepository, 'save').mockResolvedValue({ ...expectedImaging }) const patientTypeahead = wrapper.find(Typeahead) diff --git a/src/__tests__/imagings/search/ImagingRequestTable.test.tsx b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx new file mode 100644 index 0000000000..0f821a7708 --- /dev/null +++ b/src/__tests__/imagings/search/ImagingRequestTable.test.tsx @@ -0,0 +1,82 @@ +import { Table } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import ImagingSearchRequest from '../../../imagings/model/ImagingSearchRequest' +import ImagingRequestTable from '../../../imagings/search/ImagingRequestTable' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Imaging from '../../../shared/model/Imaging' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('Imaging Request Table', () => { + 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 (searchRequest: ImagingSearchRequest) => { + jest.resetAllMocks() + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(expectedImagings) + let wrapper: any + + await act(async () => { + wrapper = await mount() + }) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should search for imaging requests ', async () => { + const expectedSearch: ImagingSearchRequest = { status: 'all', text: 'text' } + await setup(expectedSearch) + + expect(ImagingRepository.search).toHaveBeenCalledTimes(1) + expect(ImagingRepository.search).toHaveBeenCalledWith({ ...expectedSearch, defaultSortRequest }) + }) + + it('should render a table of imaging requests', async () => { + const expectedSearch: ImagingSearchRequest = { status: 'all', text: 'text' } + const { wrapper } = await setup(expectedSearch) + + const table = wrapper.find(Table) + const columns = table.prop('columns') + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.code', key: 'code' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.type', key: 'type' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedOn', key: 'requestedOn' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.patient', key: 'fullName' }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.requestedBy', key: 'requestedBy' }), + ) + expect(columns[5]).toEqual( + expect.objectContaining({ label: 'imagings.imaging.status', key: 'status' }), + ) + + expect(table.prop('data')).toEqual([expectedImaging]) + }) +}) diff --git a/src/__tests__/imagings/ViewImagings.test.tsx b/src/__tests__/imagings/search/ViewImagings.test.tsx similarity index 62% rename from src/__tests__/imagings/ViewImagings.test.tsx rename to src/__tests__/imagings/search/ViewImagings.test.tsx index f06f32e3cb..51a02b753d 100644 --- a/src/__tests__/imagings/ViewImagings.test.tsx +++ b/src/__tests__/imagings/search/ViewImagings.test.tsx @@ -1,21 +1,21 @@ -import { Table } from '@hospitalrun/components' -import { act } from '@testing-library/react' 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 { Route, Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import ViewImagings from '../../imagings/ViewImagings' -import * as breadcrumbUtil from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' -import ImagingRepository from '../../shared/db/ImagingRepository' -import Imaging from '../../shared/model/Imaging' -import Permissions from '../../shared/model/Permissions' -import { RootState } from '../../shared/store' +import ImagingRequestTable from '../../../imagings/search/ImagingRequestTable' +import ViewImagings from '../../../imagings/search/ViewImagings' +import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import ImagingRepository from '../../../shared/db/ImagingRepository' +import Imaging from '../../../shared/model/Imaging' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) @@ -39,7 +39,7 @@ describe('View Imagings', () => { jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() jest.spyOn(titleUtil, 'default') - jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue(mockImagings) + jest.spyOn(ImagingRepository, 'search').mockResolvedValue(mockImagings) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) history = createMemoryHistory() @@ -48,7 +48,6 @@ describe('View Imagings', () => { const store = mockStore({ title: '', user: { permissions }, - imagings: { imagings: mockImagings }, } as any) let wrapper: any @@ -65,8 +64,8 @@ describe('View Imagings', () => { , ) }) - wrapper.update() + return wrapper as ReactWrapper } @@ -99,28 +98,8 @@ describe('View Imagings', () => { [Permissions.ViewIncident, Permissions.RequestImaging], [expectedImaging], ) - const table = wrapper.find(Table) - const columns = table.prop('columns') - expect(columns[0]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.code', key: 'code' }), - ) - expect(columns[1]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.type', key: 'type' }), - ) - expect(columns[2]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.requestedOn', key: 'requestedOn' }), - ) - expect(columns[3]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.patient', key: 'fullName' }), - ) - expect(columns[4]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.requestedBy', key: 'requestedBy' }), - ) - expect(columns[5]).toEqual( - expect.objectContaining({ label: 'imagings.imaging.status', key: 'status' }), - ) - expect(table.prop('data')).toEqual([expectedImaging]) + expect(wrapper.exists(ImagingRequestTable)) }) }) }) diff --git a/src/imagings/Imagings.tsx b/src/imagings/Imagings.tsx index ce9bc893e3..4b13fa1563 100644 --- a/src/imagings/Imagings.tsx +++ b/src/imagings/Imagings.tsx @@ -7,7 +7,7 @@ import PrivateRoute from '../shared/components/PrivateRoute' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' import NewImagingRequest from './requests/NewImagingRequest' -import ImagingRequests from './ViewImagings' +import ImagingRequests from './search/ViewImagings' const Imagings = () => { const { permissions } = useSelector((state: RootState) => state.user) diff --git a/src/imagings/ViewImagings.tsx b/src/imagings/ViewImagings.tsx deleted file mode 100644 index 96b2bd4b9f..0000000000 --- a/src/imagings/ViewImagings.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { Button, Table } from '@hospitalrun/components' -import format from 'date-fns/format' -import React, { useState, useEffect, useCallback } from 'react' -import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' - -import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../page-header/title/useTitle' -import useTranslator from '../shared/hooks/useTranslator' -import Permissions from '../shared/model/Permissions' -import { RootState } from '../shared/store' -import { extractUsername } from '../shared/util/extractUsername' -import { searchImagings } from './imagings-slice' - -type ImagingFilter = 'requested' | 'completed' | 'canceled' | 'all' - -const ViewImagings = () => { - const { t } = useTranslator() - const history = useHistory() - const setButtons = useButtonToolbarSetter() - useTitle(t('imagings.label')) - - const { permissions } = useSelector((state: RootState) => state.user) - const dispatch = useDispatch() - const { imagings } = useSelector((state: RootState) => state.imagings) - const [searchFilter, setSearchFilter] = useState('all') - - const getButtons = useCallback(() => { - const buttons: React.ReactNode[] = [] - - if (permissions.includes(Permissions.RequestImaging)) { - buttons.push( - , - ) - } - - return buttons - }, [permissions, history, t]) - - useEffect(() => { - setSearchFilter('all' as ImagingFilter) - }, []) - - useEffect(() => { - dispatch(searchImagings(' ', searchFilter)) - }, [dispatch, searchFilter]) - - useEffect(() => { - setButtons(getButtons()) - return () => { - setButtons([]) - } - }, [dispatch, getButtons, setButtons]) - - 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={imagings} - /> - - - ) -} - -export default ViewImagings diff --git a/src/imagings/hooks/useRequestImaging.tsx b/src/imagings/hooks/useRequestImaging.tsx index 09633edbf9..eba9c52bbc 100644 --- a/src/imagings/hooks/useRequestImaging.tsx +++ b/src/imagings/hooks/useRequestImaging.tsx @@ -2,11 +2,17 @@ import { isEmpty } from 'lodash' import { queryCache, useMutation } from 'react-query' import ImagingRepository from '../../shared/db/ImagingRepository' -import AbstractDBModel from '../../shared/model/AbstractDBModel' import Imaging from '../../shared/model/Imaging' import validateImagingRequest from '../util/validate-imaging-request' -type ImagingRequest = Omit +export interface ImagingRequest { + status: 'completed' | 'requested' | 'canceled' + patient: string + visitId: string + fullName: string + notes?: string + type: string +} async function requestImaging(request: ImagingRequest): Promise { const error = validateImagingRequest(request) diff --git a/src/imagings/imaging-slice.ts b/src/imagings/imaging-slice.ts deleted file mode 100644 index 70bd442e96..0000000000 --- a/src/imagings/imaging-slice.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import ImagingRepository from '../shared/db/ImagingRepository' -import Imaging from '../shared/model/Imaging' -import Patient from '../shared/model/Patient' -import { AppThunk } from '../shared/store' - -interface Error { - patient?: string - type?: string - status?: string - message?: string - visitId?: string -} - -interface ImagingState { - error: Error - imaging?: Imaging - patient?: Patient - status: 'loading' | 'error' | 'completed' -} - -const statusType: string[] = ['requested', 'completed', 'canceled'] - -const initialState: ImagingState = { - error: {}, - imaging: undefined, - patient: undefined, - status: 'loading', -} - -function start(state: ImagingState) { - state.status = 'loading' -} - -function finish(state: ImagingState, { payload }: PayloadAction) { - state.status = 'completed' - state.imaging = payload - state.error = {} -} - -function error(state: ImagingState, { payload }: PayloadAction) { - state.status = 'error' - state.error = payload -} - -const imagingSlice = createSlice({ - name: 'imaging', - initialState, - reducers: { - requestImagingStart: start, - requestImagingSuccess: finish, - requestImagingError: error, - }, -}) - -export const { - requestImagingStart, - requestImagingSuccess, - requestImagingError, -} = imagingSlice.actions - -const validateImagingRequest = (newImaging: Imaging): Error => { - const imagingRequestError: Error = {} - if (!newImaging.patient) { - imagingRequestError.patient = 'imagings.requests.error.patientRequired' - } - - if (!newImaging.type) { - imagingRequestError.type = 'imagings.requests.error.typeRequired' - } - - if (!newImaging.status) { - imagingRequestError.status = 'imagings.requests.error.statusRequired' - } else if (!statusType.includes(newImaging.status)) { - imagingRequestError.status = 'imagings.requests.error.incorrectStatus' - } - - return imagingRequestError -} - -export const requestImaging = ( - newImaging: Imaging, - onSuccess?: (imaging: Imaging) => void, -): AppThunk => async (dispatch, getState) => { - dispatch(requestImagingStart()) - - const imagingRequestError = validateImagingRequest(newImaging) - if (Object.keys(imagingRequestError).length > 0) { - imagingRequestError.message = 'imagings.requests.error.unableToRequest' - dispatch(requestImagingError(imagingRequestError)) - } else { - newImaging.requestedOn = new Date(Date.now().valueOf()).toISOString() - newImaging.requestedBy = getState().user.user?.id || '' - const requestedImaging = await ImagingRepository.save(newImaging) - dispatch(requestImagingSuccess(requestedImaging)) - - if (onSuccess) { - onSuccess(requestedImaging) - } - } -} - -export default imagingSlice.reducer diff --git a/src/imagings/imagings-slice.ts b/src/imagings/imagings-slice.ts deleted file mode 100644 index e58fd2fc0d..0000000000 --- a/src/imagings/imagings-slice.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import ImagingRepository from '../shared/db/ImagingRepository' -import SortRequest from '../shared/db/SortRequest' -import Imaging from '../shared/model/Imaging' -import { AppThunk } from '../shared/store' - -interface ImagingsState { - isLoading: boolean - imagings: Imaging[] - statusFilter: status -} - -type status = 'requested' | 'completed' | 'canceled' | 'all' - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const initialState: ImagingsState = { - isLoading: false, - imagings: [], - statusFilter: 'all', -} - -const startLoading = (state: ImagingsState) => { - state.isLoading = true -} - -const imagingsSlice = createSlice({ - name: 'imagings', - initialState, - reducers: { - fetchImagingsStart: startLoading, - fetchImagingsSuccess(state, { payload }: PayloadAction) { - state.isLoading = false - state.imagings = payload - }, - }, -}) -export const { fetchImagingsStart, fetchImagingsSuccess } = imagingsSlice.actions - -export const searchImagings = (text: string, status: status): AppThunk => async (dispatch) => { - dispatch(fetchImagingsStart()) - - let imagings - - if (text.trim() === '' && status === initialState.statusFilter) { - imagings = await ImagingRepository.findAll(defaultSortRequest) - } else { - imagings = await ImagingRepository.search({ - text, - status, - defaultSortRequest, - }) - } - - dispatch(fetchImagingsSuccess(imagings)) -} - -export default imagingsSlice.reducer diff --git a/src/imagings/model/ImagingSearchRequest.ts b/src/imagings/model/ImagingSearchRequest.ts index 030e4dfc8e..8f90ec2437 100644 --- a/src/imagings/model/ImagingSearchRequest.ts +++ b/src/imagings/model/ImagingSearchRequest.ts @@ -1,4 +1,4 @@ export default interface ImagingSearchRequest { - status: 'completed' | 'requested' | 'canceled' + status: 'completed' | 'requested' | 'canceled' | 'all' text: string } diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx index f6432f7e47..f032647601 100644 --- a/src/imagings/requests/NewImagingRequest.tsx +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -1,7 +1,6 @@ import { Typeahead, Label, Button, Alert, Column, Row } from '@hospitalrun/components' import format from 'date-fns/format' import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' @@ -13,17 +12,16 @@ import TextFieldWithLabelFormGroup from '../../shared/components/input/TextField import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' import useTranslator from '../../shared/hooks/useTranslator' -import Imaging from '../../shared/model/Imaging' import Patient from '../../shared/model/Patient' -import { RootState } from '../../shared/store' -import { requestImaging } from '../imaging-slice' +import useRequestImaging, { ImagingRequest } from '../hooks/useRequestImaging' +import { ImagingRequestError } from '../util/validate-imaging-request' const NewImagingRequest = () => { const { t } = useTranslator() - const dispatch = useDispatch() const history = useHistory() + const [mutate] = useRequestImaging() useTitle(t('imagings.requests.new')) - const { status, error } = useSelector((state: RootState) => state.imaging) + const [error, setError] = useState() const [visitOption, setVisitOption] = useState([] as Option[]) const statusOptions: Option[] = [ @@ -32,12 +30,12 @@ const NewImagingRequest = () => { { label: t('imagings.status.canceled'), value: 'canceled' }, ] - const [newImagingRequest, setNewImagingRequest] = useState({ + const [newImagingRequest, setNewImagingRequest] = useState({ patient: '', fullName: '', - type: '', + status: 'requested', notes: '', - status: '', + type: '', visitId: '', }) @@ -85,7 +83,7 @@ const NewImagingRequest = () => { const onStatusChange = (value: string) => { setNewImagingRequest((previousNewImagingRequest) => ({ ...previousNewImagingRequest, - status: value, + status: value as 'completed' | 'canceled' | 'requested', })) } @@ -105,12 +103,12 @@ const NewImagingRequest = () => { } const onSave = async () => { - const newImaging = newImagingRequest as Imaging - const onSuccess = () => { + try { + await mutate(newImagingRequest) history.push(`/imaging`) + } catch (e) { + setError(e) } - - dispatch(requestImaging(newImaging, onSuccess)) } const onCancel = () => { @@ -126,8 +124,8 @@ const NewImagingRequest = () => { return ( <> - {status === 'error' && ( - + {error !== undefined && ( + )}
@@ -143,8 +141,8 @@ const NewImagingRequest = () => { onSearch={async (query: string) => PatientRepository.search(query)} searchAccessor="fullName" renderMenuItemChildren={(p: Patient) =>
{`${p.fullName} (${p.code})`}
} - isInvalid={!!error.patient} - feedback={t(error.patient as string)} + isInvalid={!!error?.patient} + feedback={t(error?.patient)} /> @@ -170,8 +168,8 @@ const NewImagingRequest = () => { label={t('imagings.imaging.type')} isRequired isEditable - isInvalid={!!error.type} - feedback={t(error.type as string)} + isInvalid={!!error?.type} + feedback={t(error?.type)} value={newImagingRequest.type} onChange={onImagingTypeChange} /> diff --git a/src/imagings/search/ImagingRequestTable.tsx b/src/imagings/search/ImagingRequestTable.tsx new file mode 100644 index 0000000000..efeef47676 --- /dev/null +++ b/src/imagings/search/ImagingRequestTable.tsx @@ -0,0 +1,49 @@ +import { Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' + +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' + +interface Props { + searchRequest: ImagingSearchRequest +} + +const ImagingRequestTable = (props: Props) => { + const { searchRequest } = props + const { t } = useTranslator() + const { data, status } = useImagingSearch(searchRequest) + + if (data === undefined || status === 'loading') { + return + } + + 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} + /> + ) +} + +export default ImagingRequestTable diff --git a/src/imagings/search/ViewImagings.tsx b/src/imagings/search/ViewImagings.tsx new file mode 100644 index 0000000000..48f4da5fc8 --- /dev/null +++ b/src/imagings/search/ViewImagings.tsx @@ -0,0 +1,64 @@ +import { Button } 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 useTitle from '../../page-header/title/useTitle' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import ImagingSearchRequest from '../model/ImagingSearchRequest' +import ImagingRequestTable from './ImagingRequestTable' + +const ViewImagings = () => { + const { t } = useTranslator() + const { permissions } = useSelector((state: RootState) => state.user) + const history = useHistory() + const setButtons = useButtonToolbarSetter() + useTitle(t('imagings.label')) + + const [searchRequest, setSearchRequest] = useState({ + status: 'all', + text: '', + }) + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestImaging)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setSearchRequest((previousRequest) => ({ ...previousRequest, status: 'all' })) + }, []) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [getButtons, setButtons]) + + return ( +
+ +
+ ) +} + +export default ViewImagings diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 7a7f8e19f8..8bd1108cd2 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -1,8 +1,6 @@ import { configureStore, combineReducers, Action } from '@reduxjs/toolkit' import ReduxThunk, { ThunkAction } from 'redux-thunk' -import imaging from '../../imagings/imaging-slice' -import imagings from '../../imagings/imagings-slice' import lab from '../../labs/lab-slice' import labs from '../../labs/labs-slice' import medication from '../../medications/medication-slice' @@ -29,8 +27,6 @@ const reducer = combineReducers({ labs, medication, medications, - imagings, - imaging, }) const store = configureStore({ From caa77b8f80aed545e0801dbeaa1dad6f3ff78b92 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Tue, 1 Sep 2020 01:12:12 -0400 Subject: [PATCH 34/86] refactor(patient): refactor notes to use react query (#2348) --- .../patients/hooks/useAddPatientNote.test.ts | 49 ++++++ .../patients/hooks/usePatientNote.test.ts | 42 +++++ .../patients/hooks/usePatientNotes.test.ts | 21 +++ .../patients/notes/NewNoteModal.test.tsx | 152 +++++++----------- .../patients/notes/NotesList.test.tsx | 74 +++++++++ .../patients/notes/NotesTab.test.tsx | 66 ++++---- .../patients/notes/ViewNote.test.tsx | 46 ++++++ src/patients/hooks/useAddPatientNote.ts | 44 +++++ src/patients/hooks/usePatientNote.ts | 19 +++ src/patients/hooks/usePatientNotes.ts | 13 ++ src/patients/notes/NewNoteModal.tsx | 51 +++--- src/patients/notes/NoteTab.tsx | 40 +++-- src/patients/notes/NotesList.tsx | 50 ++++++ src/patients/notes/ViewNote.tsx | 32 ++++ src/patients/util/validate-note.ts | 20 +++ src/patients/view/ViewPatient.tsx | 2 +- 16 files changed, 553 insertions(+), 168 deletions(-) create mode 100644 src/__tests__/patients/hooks/useAddPatientNote.test.ts create mode 100644 src/__tests__/patients/hooks/usePatientNote.test.ts create mode 100644 src/__tests__/patients/hooks/usePatientNotes.test.ts create mode 100644 src/__tests__/patients/notes/NotesList.test.tsx create mode 100644 src/__tests__/patients/notes/ViewNote.test.tsx create mode 100644 src/patients/hooks/useAddPatientNote.ts create mode 100644 src/patients/hooks/usePatientNote.ts create mode 100644 src/patients/hooks/usePatientNotes.ts create mode 100644 src/patients/notes/NotesList.tsx create mode 100644 src/patients/notes/ViewNote.tsx create mode 100644 src/patients/util/validate-note.ts diff --git a/src/__tests__/patients/hooks/useAddPatientNote.test.ts b/src/__tests__/patients/hooks/useAddPatientNote.test.ts new file mode 100644 index 0000000000..5444799dcd --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientNote.test.ts @@ -0,0 +1,49 @@ +/* eslint-disable no-console */ + +import useAddPatientNote from '../../../patients/hooks/useAddPatientNote' +import * as validateNote from '../../../patients/util/validate-note' +import PatientRepository from '../../../shared/db/PatientRepository' +import Note from '../../../shared/model/Note' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add note', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should throw an error if note validation fails', async () => { + const expectedError = { nameError: 'some error' } + jest.spyOn(validateNote, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientNote(), { patientId: '123', note: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should add the note to the patient', async () => { + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const givenPatient = { id: 'patientId', notes: [] as Note[] } as Patient + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedNote.id) + const expectedPatient = { ...givenPatient, notes: [expectedNote] } + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddPatientNote(), { + patientId: givenPatient.id, + note: expectedNote, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedNote]) + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientNote.test.ts b/src/__tests__/patients/hooks/usePatientNote.test.ts new file mode 100644 index 0000000000..acfbe68f6d --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientNote.test.ts @@ -0,0 +1,42 @@ +/* eslint-disable no-console */ + +import usePatientNote from '../../../patients/hooks/usePatientNote' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use note', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should return a note given a patient id and note id', async () => { + const expectedPatientId = '123' + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const expectedPatient = { id: expectedPatientId, notes: [expectedNote] } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(expectedPatient) + + const actualNote = await executeQuery(() => usePatientNote(expectedPatientId, expectedNote.id)) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualNote).toEqual(expectedNote) + }) + + it('should throw an error if patient does not have note with id', async () => { + const expectedPatientId = '123' + const expectedNote = { id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' } + const expectedPatient = { + id: expectedPatientId, + notes: [{ id: '426', text: 'eome name', date: '1947-09-09T14:48:00.000Z' }], + } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(expectedPatient) + + try { + await executeQuery(() => usePatientNote(expectedPatientId, expectedNote.id)) + } catch (e) { + expect(e).toEqual(new Error('Timed out in waitFor after 1000ms.')) + } + }) +}) diff --git a/src/__tests__/patients/hooks/usePatientNotes.test.ts b/src/__tests__/patients/hooks/usePatientNotes.test.ts new file mode 100644 index 0000000000..e916de88e7 --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientNotes.test.ts @@ -0,0 +1,21 @@ +import usePatientNotes from '../../../patients/hooks/usePatientNotes' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient notes', () => { + it('should get patient notes', async () => { + const expectedPatientId = '123' + + const expectedNotes = [{ id: '456', text: 'eome name', date: '1947-09-09T14:48:00.000Z' }] + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce({ + id: expectedPatientId, + notes: expectedNotes, + } as Patient) + + const actualNotes = await executeQuery(() => usePatientNotes(expectedPatientId)) + + expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) + expect(actualNotes).toEqual(expectedNotes) + }) +}) diff --git a/src/__tests__/patients/notes/NewNoteModal.test.tsx b/src/__tests__/patients/notes/NewNoteModal.test.tsx index e71cf5f5ca..59cf0c35a9 100644 --- a/src/__tests__/patients/notes/NewNoteModal.test.tsx +++ b/src/__tests__/patients/notes/NewNoteModal.test.tsx @@ -1,41 +1,42 @@ +/* eslint-disable no-console */ + import { Alert, Modal } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount } from 'enzyme' import React from 'react' -import { Provider } from 'react-redux' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' import NewNoteModal from '../../../patients/notes/NewNoteModal' -import * as patientSlice from '../../../patients/patient-slice' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('New Note Modal', () => { + const mockPatient = { + id: '123', + givenName: 'someName', + } as Patient + + const setup = (onCloseSpy = jest.fn()) => { + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(mockPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(mockPatient) + const wrapper = mount( + , + ) + return { wrapper } + } + beforeEach(() => { - jest.spyOn(PatientRepository, 'find') - jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) it('should render a modal with the correct labels', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + const modal = wrapper.find(Modal) expect(modal).toHaveLength(1) expect(modal.prop('title')).toEqual('patient.notes.new') @@ -47,20 +48,7 @@ describe('New Note Modal', () => { }) it('should render a notes rich text editor', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(noteTextField.prop('label')).toEqual('patient.note') @@ -68,29 +56,22 @@ describe('New Note Modal', () => { expect(noteTextField).toHaveLength(1) }) - it('should render note error', () => { - const expectedPatient = { - id: '1234', - givenName: 'some name', - } + it('should render note error', async () => { const expectedError = { - message: 'some message', - note: 'some note error', + message: 'patient.notes.error.unableToAdd', + note: 'patient.notes.error.noteRequired', } - const store = mockStore({ - patient: { - patient: expectedPatient, - noteError: expectedError, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + await act(async () => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) + }) + wrapper.update() const alert = wrapper.find(Alert) const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) + expect(alert.prop('title')).toEqual('states.error') expect(alert.prop('message')).toEqual(expectedError.message) expect(noteTextField.prop('isInvalid')).toBeTruthy() @@ -100,20 +81,7 @@ describe('New Note Modal', () => { describe('on cancel', () => { it('should call the onCloseButtonCLick function when the cancel button is clicked', () => { const onCloseButtonClickSpy = jest.fn() - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - const wrapper = mount( - - - , - ) + const { wrapper } = setup(onCloseButtonClickSpy) act(() => { const modal = wrapper.find(Modal) @@ -126,42 +94,34 @@ describe('New Note Modal', () => { }) describe('on save', () => { - it('should dispatch add note', () => { + it('should dispatch add note', async () => { const expectedNote = 'some note' - jest.spyOn(patientSlice, 'addNote') - const expectedPatient = { - id: '1234', - givenName: 'some name', - } - const store = mockStore({ - patient: { - patient: expectedPatient, - }, - } as any) - - jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient as Patient) - jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient as Patient) - - const wrapper = mount( - - - , - ) + const { wrapper } = setup() + const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) - act(() => { - const noteTextField = wrapper.find(TextFieldWithLabelFormGroup) + await act(async () => { const onChange = noteTextField.prop('onChange') as any - onChange({ currentTarget: { value: expectedNote } }) + await onChange({ currentTarget: { value: expectedNote } }) }) wrapper.update() - act(() => { + + await act(async () => { const modal = wrapper.find(Modal) - const { onClick } = modal.prop('successButton') as any - onClick() + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) + wrapper.update() }) - expect(patientSlice.addNote).toHaveBeenCalledWith(expectedPatient.id, { text: expectedNote }) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + notes: [expect.objectContaining({ text: expectedNote })], + }), + ) + + // Does the form reset value back to blank? + expect(noteTextField.prop('value')).toEqual('') }) }) }) diff --git a/src/__tests__/patients/notes/NotesList.test.tsx b/src/__tests__/patients/notes/NotesList.test.tsx new file mode 100644 index 0000000000..860572c139 --- /dev/null +++ b/src/__tests__/patients/notes/NotesList.test.tsx @@ -0,0 +1,74 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import NotesList from '../../../patients/notes/NotesList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Note from '../../../shared/model/Note' +import Patient from '../../../shared/model/Patient' + +describe('Notes list', () => { + const setup = async (notes: Note[]) => { + const mockPatient = { id: '123', notes } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(mockPatient) + const history = createMemoryHistory() + history.push(`/patients/${mockPatient.id}/notes`) + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + + wrapper.update() + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a list of notes', async () => { + const expectedNotes = [ + { + id: '456', + text: 'some name', + date: '1947-09-09T14:48:00.000Z', + }, + ] + const { wrapper } = await setup(expectedNotes) + const listItems = wrapper.find(ListItem) + + expect(wrapper.exists(List)).toBeTruthy() + expect(listItems).toHaveLength(expectedNotes.length) + expect(listItems.at(0).find('.ref__note-item-date').text().length) + expect(listItems.at(0).find('.ref__note-item-text').text()).toEqual(expectedNotes[0].text) + }) + + it('should display a warning when no notes are present', async () => { + const expectedNotes: Note[] = [] + const { wrapper } = await setup(expectedNotes) + + const alert = wrapper.find(Alert) + + expect(wrapper.exists(Alert)).toBeTruthy() + expect(wrapper.exists(List)).toBeFalsy() + + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes') + expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove') + }) + + it('should navigate to the note view when the note is clicked', async () => { + const expectedNotes = [{ id: '456', text: 'some name', date: '1947-09-09T14:48:00.000Z' }] + const { wrapper, history } = await setup(expectedNotes) + const item = wrapper.find(ListItem) + act(() => { + const onClick = item.prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/patients/123/notes/${expectedNotes[0].id}`) + }) +}) diff --git a/src/__tests__/patients/notes/NotesTab.test.tsx b/src/__tests__/patients/notes/NotesTab.test.tsx index 1f4cda1b77..e91eae2d17 100644 --- a/src/__tests__/patients/notes/NotesTab.test.tsx +++ b/src/__tests__/patients/notes/NotesTab.test.tsx @@ -1,6 +1,9 @@ +/* eslint-disable no-console */ + import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' +import assign from 'lodash/assign' import React from 'react' import { act } from 'react-dom/test-utils' import { Provider } from 'react-redux' @@ -26,16 +29,30 @@ const history = createMemoryHistory() let user: any let store: any -const setup = (patient = expectedPatient, permissions = [Permissions.WritePatients]) => { +const setup = (props: any = {}) => { + const { permissions, patient, route } = assign( + {}, + { + patient: expectedPatient, + permissions: [Permissions.WritePatients], + route: '/patients/123/notes', + }, + props, + ) + user = { permissions } store = mockStore({ patient, user } as any) - const wrapper = mount( - - - - - , - ) + history.push(route) + let wrapper: any + act(() => { + wrapper = mount( + + + + + , + ) + }) return wrapper } @@ -45,6 +62,7 @@ describe('Notes Tab', () => { beforeEach(() => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) it('should render a add notes button', () => { @@ -56,7 +74,7 @@ describe('Notes Tab', () => { }) it('should not render a add notes button if the user does not have permissions', () => { - const wrapper = setup(expectedPatient, []) + const wrapper = setup({ permissions: [] }) const addNotesButton = wrapper.find(components.Button) expect(addNotesButton).toHaveLength(0) @@ -64,7 +82,6 @@ describe('Notes Tab', () => { it('should open the Add Notes Modal', () => { const wrapper = setup() - act(() => { const onClick = wrapper.find(components.Button).prop('onClick') as any onClick() @@ -74,27 +91,14 @@ describe('Notes Tab', () => { expect(wrapper.find(components.Modal).prop('show')).toBeTruthy() }) }) - - describe('notes list', () => { - it('should list the patients diagnoses', () => { - const notes = expectedPatient.notes as Note[] - const wrapper = setup() - - const list = wrapper.find(components.List) - const listItems = wrapper.find(components.ListItem) - - expect(list).toHaveLength(1) - expect(listItems).toHaveLength(notes.length) - }) - - it('should render a warning message if the patient does not have any diagnoses', () => { - const wrapper = setup({ ...expectedPatient, notes: [] }) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.notes.warning.noNotes') - expect(alert.prop('message')).toEqual('patient.notes.addNoteAbove') + describe('/patients/:id/notes', () => { + it('should render the view notes screen when /patients/:id/notes is accessed', () => { + const route = '/patients/123/notes' + const permissions = [Permissions.WritePatients] + const wrapper = setup({ route, permissions }) + act(() => { + expect(wrapper.exists(NoteTab)).toBeTruthy() + }) }) }) }) diff --git a/src/__tests__/patients/notes/ViewNote.test.tsx b/src/__tests__/patients/notes/ViewNote.test.tsx new file mode 100644 index 0000000000..74172470c7 --- /dev/null +++ b/src/__tests__/patients/notes/ViewNote.test.tsx @@ -0,0 +1,46 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Route, Router } from 'react-router-dom' + +import ViewNote from '../../../patients/notes/ViewNote' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +describe('View Note', () => { + const patient = { + id: 'patientId', + notes: [{ id: '123', text: 'some name', date: '1947-09-09T14:48:00.000Z' }], + } as Patient + + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/notes/${patient.notes![0].id}`) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render a note input with the correct data', async () => { + const { wrapper } = await setup() + + const noteText = wrapper.find(TextInputWithLabelFormGroup) + expect(noteText).toHaveLength(1) + expect(noteText.prop('value')).toEqual(patient.notes![0].text) + }) +}) diff --git a/src/patients/hooks/useAddPatientNote.ts b/src/patients/hooks/useAddPatientNote.ts new file mode 100644 index 0000000000..01f301cc8b --- /dev/null +++ b/src/patients/hooks/useAddPatientNote.ts @@ -0,0 +1,44 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' +import { uuid } from '../../shared/util/uuid' +import validateNote from '../util/validate-note' + +interface AddNoteRequest { + patientId: string + note: Omit +} + +async function addNote(request: AddNoteRequest): Promise { + const error = validateNote(request.note) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const notes = patient.notes ? [...patient.notes] : [] + const newNote: Note = { + id: uuid(), + ...request.note, + } + notes.push(newNote) + + await PatientRepository.saveOrUpdate({ + ...patient, + notes, + }) + + return notes + } + + throw error +} + +export default function useAddPatientNote() { + return useMutation(addNote, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['notes', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/usePatientNote.ts b/src/patients/hooks/usePatientNote.ts new file mode 100644 index 0000000000..941fb8ae62 --- /dev/null +++ b/src/patients/hooks/usePatientNote.ts @@ -0,0 +1,19 @@ +import { QueryKey, useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' + +async function getNote(_: QueryKey, patientId: string, noteId: string): Promise { + const patient = await PatientRepository.find(patientId) + const maybeNote = patient.notes?.find((n) => n.id === noteId) + + if (!maybeNote) { + throw new Error('Note not found') + } + + return maybeNote +} + +export default function usePatientNote(patientId: string, noteId: string) { + return useQuery(['notes', patientId, noteId], getNote) +} diff --git a/src/patients/hooks/usePatientNotes.ts b/src/patients/hooks/usePatientNotes.ts new file mode 100644 index 0000000000..c0a5ae0fdc --- /dev/null +++ b/src/patients/hooks/usePatientNotes.ts @@ -0,0 +1,13 @@ +import { QueryKey, useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Note from '../../shared/model/Note' + +async function fetchPatientNotes(_: QueryKey, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.notes || [] +} + +export default function usePatientNotes(patientId: string) { + return useQuery(['notes', patientId], fetchPatientNotes) +} diff --git a/src/patients/notes/NewNoteModal.tsx b/src/patients/notes/NewNoteModal.tsx index 818522d2fb..93885a6475 100644 --- a/src/patients/notes/NewNoteModal.tsx +++ b/src/patients/notes/NewNoteModal.tsx @@ -1,48 +1,53 @@ import { Modal, Alert } from '@hospitalrun/components' import React, { useState } from 'react' -import { useDispatch, useSelector } from 'react-redux' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' -import Note from '../../shared/model/Note' -import { RootState } from '../../shared/store' -import { addNote } from '../patient-slice' +import useAddPatientNote from '../hooks/useAddPatientNote' +import { NoteError } from '../util/validate-note' interface Props { show: boolean toggle: () => void onCloseButtonClick: () => void + patientId: string } +const initialNoteState = { text: '', date: new Date().toISOString() } const NewNoteModal = (props: Props) => { - const { show, toggle, onCloseButtonClick } = props - const dispatch = useDispatch() - const { patient, noteError } = useSelector((state: RootState) => state.patient) + const { show, toggle, onCloseButtonClick, patientId } = props const { t } = useTranslator() - const [note, setNote] = useState({ - text: '', - }) + const [mutate] = useAddPatientNote() - const onFieldChange = (key: string, value: string | any) => { - setNote({ - ...note, - [key]: value, - }) - } + const [noteError, setNoteError] = useState(undefined) + const [note, setNote] = useState(initialNoteState) const onNoteTextChange = (event: React.ChangeEvent) => { const text = event.currentTarget.value - onFieldChange('text', text) + setNote({ + ...note, + text, + }) } - const onSaveButtonClick = () => { - dispatch(addNote(patient.id, note as Note)) + const onSaveButtonClick = async () => { + try { + await mutate({ patientId, note }) + setNote(initialNoteState) + onCloseButtonClick() + } catch (e) { + setNoteError(e) + } } const body = ( - {noteError?.message && ( - + {noteError && ( + )}
@@ -53,8 +58,8 @@ const NewNoteModal = (props: Props) => { name="noteTextField" label={t('patient.note')} value={note.text} - isInvalid={!!noteError?.note} - feedback={t(noteError?.note || '')} + isInvalid={!!noteError?.noteError} + feedback={t(noteError?.noteError || '')} onChange={onNoteTextChange} />
diff --git a/src/patients/notes/NoteTab.tsx b/src/patients/notes/NoteTab.tsx index b32ce3e732..31235a2eb5 100644 --- a/src/patients/notes/NoteTab.tsx +++ b/src/patients/notes/NoteTab.tsx @@ -1,13 +1,16 @@ -import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { Button } from '@hospitalrun/components' import React, { useState } from 'react' import { useSelector } from 'react-redux' +import { Route, Switch } from 'react-router-dom' +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTranslator from '../../shared/hooks/useTranslator' -import Note from '../../shared/model/Note' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import NewNoteModal from './NewNoteModal' +import NotesList from './NotesList' +import ViewNote from './ViewNote' interface Props { patient: Patient @@ -19,6 +22,14 @@ const NoteTab = (props: Props) => { const { permissions } = useSelector((state: RootState) => state.user) const [showNewNoteModal, setShowNoteModal] = useState(false) + const breadcrumbs = [ + { + i18nKey: 'patient.notes.label', + location: `/patients/${patient.id}/notes`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + const onNewNoteClick = () => { setShowNoteModal(true) } @@ -45,25 +56,20 @@ const NoteTab = (props: Props) => {

- {(!patient.notes || patient.notes.length === 0) && ( - - )} - - {patient.notes?.map((note: Note) => ( - - {new Date(note.date).toLocaleString()} -

{note.text}

-
- ))} -
+ + + + + + + + + ) diff --git a/src/patients/notes/NotesList.tsx b/src/patients/notes/NotesList.tsx new file mode 100644 index 0000000000..cd62c0df3f --- /dev/null +++ b/src/patients/notes/NotesList.tsx @@ -0,0 +1,50 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import React from 'react' +import { useHistory } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Note from '../../shared/model/Note' +import usePatientNotes from '../hooks/usePatientNotes' + +interface Props { + patientId: string +} + +const NotesList = (props: Props) => { + const { patientId } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientNotes(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( + + {data.map((note: Note) => ( + history.push(`/patients/${patientId}/notes/${note.id}`)} + > +

{new Date(note.date).toLocaleString()}

+

{note.text}

+
+ ))} +
+ ) +} + +export default NotesList diff --git a/src/patients/notes/ViewNote.tsx b/src/patients/notes/ViewNote.tsx new file mode 100644 index 0000000000..5aef2b0117 --- /dev/null +++ b/src/patients/notes/ViewNote.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { useParams } from 'react-router-dom' + +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import usePatientNote from '../hooks/usePatientNote' + +const ViewNote = () => { + const { t } = useTranslator() + const { noteId, id: patientId } = useParams() + const { data, status } = usePatientNote(patientId, noteId) + + if (data === undefined || status === 'loading') { + return + } + + return ( +
+

Date: {new Date(data.date).toLocaleString()}

+ +
+ ) +} + +export default ViewNote diff --git a/src/patients/util/validate-note.ts b/src/patients/util/validate-note.ts new file mode 100644 index 0000000000..f6b42c8834 --- /dev/null +++ b/src/patients/util/validate-note.ts @@ -0,0 +1,20 @@ +import Note from '../../shared/model/Note' + +export class NoteError extends Error { + noteError?: string + + constructor(message: string, note: string) { + super(message) + this.noteError = note + Object.setPrototypeOf(this, NoteError.prototype) + } +} + +export default function validateNote(note: Partial) { + const error: any = {} + if (!note.text) { + error.noteError = 'patient.notes.error.noteRequired' + } + + return error +} diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index fc9adf6761..e039fb9ccd 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -156,7 +156,7 @@ const ViewPatient = () => { - + From 6ca4b59d84b4ea3f8b864ac3a17ebea79ce00536 Mon Sep 17 00:00:00 2001 From: Tomas Nygren Date: Tue, 1 Sep 2020 15:12:45 +1000 Subject: [PATCH 35/86] refactor(patient): refactor patient appointments to use react query --- .../appointments/AppointmentsList.test.tsx | 53 ++++++---- .../hooks/usePatientAppointments.test.tsx | 39 ++++++++ .../appointments/AppointmentsList.tsx | 98 +++++++++---------- src/patients/hooks/usePatientAppointments.tsx | 15 +++ .../appointments/appointments-slice.ts | 9 -- 5 files changed, 135 insertions(+), 79 deletions(-) create mode 100644 src/__tests__/patients/hooks/usePatientAppointments.test.tsx create mode 100644 src/patients/hooks/usePatientAppointments.tsx diff --git a/src/__tests__/patients/appointments/AppointmentsList.test.tsx b/src/__tests__/patients/appointments/AppointmentsList.test.tsx index e9eacf56ea..38889f4cab 100644 --- a/src/__tests__/patients/appointments/AppointmentsList.test.tsx +++ b/src/__tests__/patients/appointments/AppointmentsList.test.tsx @@ -1,6 +1,6 @@ import * as components from '@hospitalrun/components' import { Table } from '@hospitalrun/components' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -45,26 +45,35 @@ const history = createMemoryHistory() let store: any -const setup = (patient = expectedPatient, appointments = expectedAppointments) => { +const setup = async (patient = expectedPatient, appointments = expectedAppointments) => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'getAppointments').mockResolvedValue(appointments) store = mockStore({ patient, appointments: { appointments } } as any) - const wrapper = mount( - - - - - , - ) - return wrapper + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } } describe('AppointmentsList', () => { describe('Table', () => { - it('should render a list of appointments', () => { - const wrapper = setup() + it('should render a list of appointments', async () => { + const { wrapper } = await setup() const table = wrapper.find(Table) + const columns = table.prop('columns') const actions = table.prop('actions') as any @@ -91,7 +100,7 @@ describe('AppointmentsList', () => { }) it('should navigate to appointment profile on appointment click', async () => { - const wrapper = setup() + const { wrapper } = await setup() const tr = wrapper.find('tr').at(1) act(() => { @@ -104,8 +113,8 @@ describe('AppointmentsList', () => { }) describe('Empty list', () => { - it('should render a warning message if there are no appointments', () => { - const wrapper = setup(expectedPatient, []) + it('should render a warning message if there are no appointments', async () => { + const { wrapper } = await setup(expectedPatient, []) const alert = wrapper.find(components.Alert) expect(alert).toHaveLength(1) @@ -115,8 +124,16 @@ describe('AppointmentsList', () => { }) describe('New appointment button', () => { - it('should render a new appointment button', () => { - const wrapper = setup() + it('should render a new appointment button if there is an appointment', async () => { + const { wrapper } = await setup() + + const addNewAppointmentButton = wrapper.find(components.Button).at(0) + expect(addNewAppointmentButton).toHaveLength(1) + expect(addNewAppointmentButton.text().trim()).toEqual('scheduling.appointments.new') + }) + + it('should render a new appointment button if there are no appointments', async () => { + const { wrapper } = await setup(expectedPatient, []) const addNewAppointmentButton = wrapper.find(components.Button).at(0) expect(addNewAppointmentButton).toHaveLength(1) @@ -124,7 +141,7 @@ describe('AppointmentsList', () => { }) it('should navigate to new appointment page', async () => { - const wrapper = setup() + const { wrapper } = await setup() await act(async () => { await wrapper.find(components.Button).at(0).simulate('click') diff --git a/src/__tests__/patients/hooks/usePatientAppointments.test.tsx b/src/__tests__/patients/hooks/usePatientAppointments.test.tsx new file mode 100644 index 0000000000..f2dee0a559 --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientAppointments.test.tsx @@ -0,0 +1,39 @@ +import usePatientAppointments from '../../../patients/hooks/usePatientAppointments' +import PatientRepository from '../../../shared/db/PatientRepository' +import Appointment from '../../shared/model/Appointment' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient appointments', () => { + it(`should return the should return the patient's appointments`, async () => { + const expectedPatientId = '123' + + const expectedAppointments = [ + { + id: '456', + rev: '1', + patient: expectedPatientId, + startDateTime: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + endDateTime: new Date(2020, 1, 1, 9, 30, 0, 0).toISOString(), + location: 'location', + reason: 'Follow Up', + }, + { + id: '123', + rev: '1', + patient: expectedPatientId, + startDateTime: new Date(2020, 1, 1, 8, 0, 0, 0).toISOString(), + endDateTime: new Date(2020, 1, 1, 8, 30, 0, 0).toISOString(), + location: 'location', + reason: 'Checkup', + }, + ] as Appointment[] + + jest.spyOn(PatientRepository, 'getAppointments').mockResolvedValueOnce(expectedAppointments) + + const actualAppointments = await executeQuery(() => usePatientAppointments(expectedPatientId)) + + expect(PatientRepository.getAppointments).toHaveBeenCalledTimes(1) + expect(PatientRepository.getAppointments).toHaveBeenCalledWith(expectedPatientId) + expect(actualAppointments).toEqual(expectedAppointments) + }) +}) diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx index 647925901e..5536c3449c 100644 --- a/src/patients/appointments/AppointmentsList.tsx +++ b/src/patients/appointments/AppointmentsList.tsx @@ -1,25 +1,24 @@ -import { Button, Table, Spinner, Alert } from '@hospitalrun/components' +import { Button, Table, Alert } from '@hospitalrun/components' import format from 'date-fns/format' -import React, { useEffect } from 'react' -import { useSelector, useDispatch } from 'react-redux' +import React from 'react' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice' +import Loading from '../../shared/components/Loading' import useTranslator from '../../shared/hooks/useTranslator' -import { RootState } from '../../shared/store' +import usePatientsAppointments from '../hooks/usePatientAppointments' interface Props { patientId: string } const AppointmentsList = (props: Props) => { - const dispatch = useDispatch() const history = useHistory() const { t } = useTranslator() const { patientId } = props - const { appointments } = useSelector((state: RootState) => state.appointments) + + const { data, status } = usePatientsAppointments(patientId) const breadcrumbs = [ { @@ -27,11 +26,12 @@ const AppointmentsList = (props: Props) => { location: `/patients/${patientId}/appointments`, }, ] + useAddBreadcrumbs(breadcrumbs) - useEffect(() => { - dispatch(fetchPatientAppointments(patientId)) - }, [dispatch, patientId]) + if (data === undefined || status === 'loading') { + return + } return ( <> @@ -51,49 +51,43 @@ const AppointmentsList = (props: Props) => {
- {appointments ? ( - appointments.length > 0 ? ( -
row.id} - onRowClick={(row) => history.push(`/appointments/${row.id}`)} - columns={[ - { - label: t('scheduling.appointment.startDate'), - key: 'startDateTime', - formatter: (row) => - row.startDateTime - ? format(new Date(row.startDateTime), 'yyyy-MM-dd, hh:mm a') - : '', - }, - { - label: t('scheduling.appointment.endDate'), - key: 'endDateTime', - formatter: (row) => - row.endDateTime - ? format(new Date(row.endDateTime), 'yyyy-MM-dd, hh:mm a') - : '', - }, - { label: t('scheduling.appointment.location'), key: 'location' }, - { label: t('scheduling.appointment.type'), key: 'type' }, - ]} - actionsHeaderText={t('actions.label')} - actions={[ - { - label: t('actions.view'), - action: (row) => history.push(`/appointments/${row.id}`), - }, - ]} - /> - ) : ( - - ) + {data.length > 0 ? ( +
row.id} + onRowClick={(row) => history.push(`/appointments/${row.id}`)} + columns={[ + { + label: t('scheduling.appointment.startDate'), + key: 'startDateTime', + formatter: (row) => + row.startDateTime + ? format(new Date(row.startDateTime), 'yyyy-MM-dd, hh:mm a') + : '', + }, + { + label: t('scheduling.appointment.endDate'), + key: 'endDateTime', + formatter: (row) => + row.endDateTime ? format(new Date(row.endDateTime), 'yyyy-MM-dd, hh:mm a') : '', + }, + { label: t('scheduling.appointment.location'), key: 'location' }, + { label: t('scheduling.appointment.type'), key: 'type' }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: t('actions.view'), + action: (row) => history.push(`/appointments/${row.id}`), + }, + ]} + /> ) : ( - + )} diff --git a/src/patients/hooks/usePatientAppointments.tsx b/src/patients/hooks/usePatientAppointments.tsx new file mode 100644 index 0000000000..95d8a3dcfe --- /dev/null +++ b/src/patients/hooks/usePatientAppointments.tsx @@ -0,0 +1,15 @@ +import { QueryKey, useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Appointments from '../../shared/model/Appointment' + +async function fetchPatientAppointments( + _: QueryKey, + patientId: string, +): Promise { + return PatientRepository.getAppointments(patientId) +} + +export default function usePatientsAppointments(patientId: string) { + return useQuery(['appointments', patientId], fetchPatientAppointments) +} diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index edf21ece5c..df8564db52 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import AppointmentRepository from '../../shared/db/AppointmentRepository' -import PatientRepository from '../../shared/db/PatientRepository' import Appointment from '../../shared/model/Appointment' import { AppThunk } from '../../shared/store' @@ -39,12 +38,4 @@ export const fetchAppointments = (): AppThunk => async (dispatch) => { dispatch(fetchAppointmentsSuccess(appointments)) } -export const fetchPatientAppointments = (patientId: string): AppThunk => async (dispatch) => { - dispatch(fetchAppointmentsStart()) - - const appointments = await PatientRepository.getAppointments(patientId) - - dispatch(fetchAppointmentsSuccess(appointments)) -} - export default appointmentsSlice.reducer From 0187c3406306540b5da3f574a6abe984dcd84754 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Tue, 1 Sep 2020 01:13:23 -0400 Subject: [PATCH 36/86] fix(navigation): align quick menu with sidebar and mobile version --- src/shared/components/navbar/Navbar.tsx | 4 +-- src/shared/components/navbar/pageMap.tsx | 42 ++++++++++++------------ 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/src/shared/components/navbar/Navbar.tsx b/src/shared/components/navbar/Navbar.tsx index 476ea2d4e8..6096259f2a 100644 --- a/src/shared/components/navbar/Navbar.tsx +++ b/src/shared/components/navbar/Navbar.tsx @@ -48,10 +48,10 @@ const Navbar = () => { const addPages = [ pageMap.newPatient, pageMap.newAppointment, - pageMap.newLab, pageMap.newMedication, - pageMap.newIncident, + pageMap.newLab, pageMap.newImaging, + pageMap.newIncident, ] return ( diff --git a/src/shared/components/navbar/pageMap.tsx b/src/shared/components/navbar/pageMap.tsx index 03dceda6c1..27a27e988a 100644 --- a/src/shared/components/navbar/pageMap.tsx +++ b/src/shared/components/navbar/pageMap.tsx @@ -35,6 +35,18 @@ const pageMap: { path: '/appointments', icon: 'appointment', }, + newMedication: { + permission: Permissions.RequestMedication, + label: 'medications.requests.new', + path: '/medications/new', + icon: 'add', + }, + viewMedications: { + permission: Permissions.ViewMedications, + label: 'medications.requests.label', + path: '/medications', + icon: 'medication', + }, newLab: { permission: Permissions.RequestLab, label: 'labs.requests.new', @@ -47,17 +59,17 @@ const pageMap: { path: '/labs', icon: 'lab', }, - newMedication: { - permission: Permissions.RequestMedication, - label: 'medications.requests.new', - path: '/medications/new', + newImaging: { + permission: Permissions.RequestImaging, + label: 'imagings.requests.new', + path: '/imaging/new', icon: 'add', }, - viewMedications: { - permission: Permissions.ViewMedications, - label: 'medications.requests.label', - path: '/medications', - icon: 'medication', + viewImagings: { + permission: Permissions.ReadPatients, + label: 'imagings.requests.label', + path: '/imaging', + icon: 'image', }, newIncident: { permission: Permissions.ReportIncident, @@ -83,18 +95,6 @@ const pageMap: { path: '/visits', icon: 'visit', }, - newImaging: { - permission: Permissions.RequestImaging, - label: 'imagings.requests.new', - path: '/imaging/new', - icon: 'add', - }, - viewImagings: { - permission: Permissions.ReadPatients, - label: 'imagings.requests.label', - path: '/imaging', - icon: 'image', - }, settings: { permission: null, label: 'settings.label', From be1a3342266914204027c391bfd0a10ac3ac8898 Mon Sep 17 00:00:00 2001 From: Blessed Tabvirwa Date: Wed, 2 Sep 2020 00:19:14 +0200 Subject: [PATCH 37/86] refactor(patient-labs): use react query instead of redux (#2358) --- .../patients/hooks/usePatientLabs.test.tsx | 18 +++ src/__tests__/patients/labs/Labs.test.tsx | 61 +++++++++ src/__tests__/patients/labs/LabsList.test.tsx | 118 ++++++++++++++++++ src/__tests__/patients/labs/LabsTab.test.tsx | 92 -------------- .../patients/view/ViewPatient.test.tsx | 11 +- src/patients/hooks/usePatientLabs.tsx | 13 ++ src/patients/labs/Labs.tsx | 30 +++++ src/patients/labs/LabsList.tsx | 54 ++++++++ src/patients/labs/LabsTab.tsx | 60 --------- src/patients/view/ViewPatient.tsx | 4 +- 10 files changed, 302 insertions(+), 159 deletions(-) create mode 100644 src/__tests__/patients/hooks/usePatientLabs.test.tsx create mode 100644 src/__tests__/patients/labs/Labs.test.tsx create mode 100644 src/__tests__/patients/labs/LabsList.test.tsx delete mode 100644 src/__tests__/patients/labs/LabsTab.test.tsx create mode 100644 src/patients/hooks/usePatientLabs.tsx create mode 100644 src/patients/labs/Labs.tsx create mode 100644 src/patients/labs/LabsList.tsx delete mode 100644 src/patients/labs/LabsTab.tsx diff --git a/src/__tests__/patients/hooks/usePatientLabs.test.tsx b/src/__tests__/patients/hooks/usePatientLabs.test.tsx new file mode 100644 index 0000000000..b60a3d410a --- /dev/null +++ b/src/__tests__/patients/hooks/usePatientLabs.test.tsx @@ -0,0 +1,18 @@ +import usePatientLabs from '../../../patients/hooks/usePatientLabs' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import executeQuery from '../../test-utils/use-query.util' + +describe('use patient labs', () => { + it('should get patient labs', async () => { + const expectedPatientId = '123' + const expectedLabs = ([{ id: expectedPatientId, type: 'lab type' }] as unknown) as Lab[] + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValueOnce(expectedLabs) + + const actualLabs = await executeQuery(() => usePatientLabs(expectedPatientId)) + + expect(PatientRepository.getLabs).toHaveBeenCalledTimes(1) + expect(PatientRepository.getLabs).toHaveBeenCalledWith(expectedPatientId) + expect(actualLabs).toEqual(expectedLabs) + }) +}) diff --git a/src/__tests__/patients/labs/Labs.test.tsx b/src/__tests__/patients/labs/Labs.test.tsx new file mode 100644 index 0000000000..8959d4deb4 --- /dev/null +++ b/src/__tests__/patients/labs/Labs.test.tsx @@ -0,0 +1,61 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { Provider } from 'react-redux' +import { Router } from 'react-router-dom' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Labs from '../../../patients/labs/Labs' +import LabsList from '../../../patients/labs/LabsList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) +const history = createMemoryHistory() +const expectedPatient = ({ + id: '123', + rev: '123', + labs: [ + { id: '1', type: 'lab type 1' }, + { id: '2', type: 'lab type 2' }, + ], +} as unknown) as Patient + +let store: any + +const setup = async ( + patient = expectedPatient, + permissions = [Permissions.ViewLabs], + route = '/patients/123/labs', +) => { + store = mockStore({ patient: { patient }, user: { permissions } } as any) + history.push(route) + + const wrapper = await mount( + + + + + , + ) + return { wrapper: wrapper as ReactWrapper } +} + +describe('Labs', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'saveOrUpdate') + }) + + describe('patient labs list', () => { + it('should render patient labs', async () => { + const { wrapper } = await setup() + + expect(wrapper.exists(LabsList)).toBeTruthy() + }) + }) +}) diff --git a/src/__tests__/patients/labs/LabsList.test.tsx b/src/__tests__/patients/labs/LabsList.test.tsx new file mode 100644 index 0000000000..959ddf8edb --- /dev/null +++ b/src/__tests__/patients/labs/LabsList.test.tsx @@ -0,0 +1,118 @@ +import * as components from '@hospitalrun/components' +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 LabsList from '../../../patients/labs/LabsList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Lab from '../../../shared/model/Lab' +import Patient from '../../../shared/model/Patient' +import { RootState } from '../../../shared/store' + +const expectedPatient = { + id: '1234', +} as Patient + +const expectedLabs = [ + { + id: '456', + rev: '1', + patient: '1234', + requestedOn: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + requestedBy: 'someone', + type: 'lab type', + }, + { + id: '123', + rev: '1', + patient: '1234', + requestedOn: new Date(2020, 1, 1, 9, 0, 0, 0).toISOString(), + requestedBy: 'someone', + type: 'lab type', + }, +] as Lab[] + +const mockStore = createMockStore([thunk]) +const history = createMemoryHistory() + +let store: any + +const setup = async (patient = expectedPatient, labs = expectedLabs) => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue(labs) + store = mockStore({ patient, labs: { labs } } as any) + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } +} + +describe('LabsList', () => { + describe('Table', () => { + it('should render a list of labs', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + + const columns = table.prop('columns') + const actions = table.prop('actions') as any + + expect(table).toHaveLength(1) + + expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ + label: 'labs.lab.status', + key: 'status', + }), + ) + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(expectedLabs) + }) + + it('should navigate to lab view on lab click', async () => { + const { wrapper } = await setup() + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').at(0).prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual('/labs/456') + }) + }) + + describe('no patient labs', () => { + it('should render a warning message if there are no labs', async () => { + const { wrapper } = await setup(expectedPatient, []) + const alert = wrapper.find(components.Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') + expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') + }) + }) +}) diff --git a/src/__tests__/patients/labs/LabsTab.test.tsx b/src/__tests__/patients/labs/LabsTab.test.tsx deleted file mode 100644 index bd7d7fa884..0000000000 --- a/src/__tests__/patients/labs/LabsTab.test.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as components from '@hospitalrun/components' -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 LabsTab from '../../../patients/labs/LabsTab' -import PatientRepository from '../../../shared/db/PatientRepository' -import Lab from '../../../shared/model/Lab' -import Patient from '../../../shared/model/Patient' -import Permissions from '../../../shared/model/Permissions' -import { RootState } from '../../../shared/store' - -const expectedPatient = { - id: '123', -} as Patient - -const expectedLabs = [ - { - id: 'labId', - patient: '123', - type: 'type', - status: 'requested', - requestedOn: new Date().toISOString(), - } as Lab, -] - -const mockStore = createMockStore([thunk]) -const history = createMemoryHistory() - -let user: any -let store: any - -const setup = async (labs = expectedLabs) => { - jest.resetAllMocks() - user = { permissions: [Permissions.ReadPatients] } - store = mockStore({ patient: expectedPatient, user } as any) - jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue(labs) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - wrapper.update() - return { wrapper: wrapper as ReactWrapper } -} - -describe('Labs Tab', () => { - it('should list the patients labs', async () => { - const { wrapper } = await setup() - - const table = wrapper.find(Table) - const columns = table.prop('columns') - const actions = table.prop('actions') as any - expect(columns[0]).toEqual(expect.objectContaining({ label: 'labs.lab.type', key: 'type' })) - expect(columns[1]).toEqual( - expect.objectContaining({ label: 'labs.lab.requestedOn', key: 'requestedOn' }), - ) - expect(columns[2]).toEqual( - expect.objectContaining({ - label: 'labs.lab.status', - key: 'status', - }), - ) - - expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) - expect(table.prop('actionsHeaderText')).toEqual('actions.label') - expect(table.prop('data')).toEqual(expectedLabs) - }) - - it('should render a warning message if the patient does not have any labs', async () => { - const { wrapper } = await setup([]) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.labs.warning.noLabs') - expect(alert.prop('message')).toEqual('patient.labs.noLabsMessage') - }) -}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 7ce8bf4bf1..5fd316d904 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -16,7 +16,7 @@ import AppointmentsList from '../../../patients/appointments/AppointmentsList' import CarePlanTab from '../../../patients/care-plans/CarePlanTab' import Diagnoses from '../../../patients/diagnoses/Diagnoses' import GeneralInformation from '../../../patients/GeneralInformation' -import LabsTab from '../../../patients/labs/LabsTab' +import Labs from '../../../patients/labs/Labs' import NotesTab from '../../../patients/notes/NoteTab' import * as patientSlice from '../../../patients/patient-slice' import RelatedPersonTab from '../../../patients/related-persons/RelatedPersonTab' @@ -29,7 +29,7 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('ViewPatient', () => { - const patient = { + const patient = ({ id: '123', prefix: 'prefix', givenName: 'givenName', @@ -44,7 +44,7 @@ describe('ViewPatient', () => { address: 'address', code: 'P00001', dateOfBirth: new Date().toISOString(), - } as Patient + } as unknown) as Patient let history: any let store: MockStore @@ -59,6 +59,7 @@ describe('ViewPatient', () => { patient: { patient }, user: { permissions }, appointments: { appointments: [] }, + labs: { labs: [] }, } as any) history.push('/patients/123') @@ -291,12 +292,12 @@ describe('ViewPatient', () => { const tabsHeader = wrapper.find(TabsHeader) const tabs = tabsHeader.find(Tab) - const labsTab = wrapper.find(LabsTab) + const labsTab = wrapper.find(Labs) expect(history.location.pathname).toEqual(`/patients/${patient.id}/labs`) expect(tabs.at(6).prop('active')).toBeTruthy() expect(labsTab).toHaveLength(1) - expect(labsTab.prop('patientId')).toEqual(patient.id) + expect(labsTab.prop('patient')).toEqual(patient) }) it('should mark the care plans tab as active when it is clicked and render the care plan tab component when route is /patients/:id/care-plans', async () => { diff --git a/src/patients/hooks/usePatientLabs.tsx b/src/patients/hooks/usePatientLabs.tsx new file mode 100644 index 0000000000..be76ba627d --- /dev/null +++ b/src/patients/hooks/usePatientLabs.tsx @@ -0,0 +1,13 @@ +import { QueryKey, useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Lab from '../../shared/model/Lab' + +async function fetchPatientLabs(_: QueryKey, patientId: string): Promise { + const fetchedLabs = await PatientRepository.getLabs(patientId) + return fetchedLabs || [] +} + +export default function usePatientLabs(patientId: string) { + return useQuery(['labs', patientId], fetchPatientLabs) +} diff --git a/src/patients/labs/Labs.tsx b/src/patients/labs/Labs.tsx new file mode 100644 index 0000000000..88b2545ccf --- /dev/null +++ b/src/patients/labs/Labs.tsx @@ -0,0 +1,30 @@ +import React from 'react' +import { Route } from 'react-router-dom' + +import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' +import Patient from '../../shared/model/Patient' +import LabsList from './LabsList' + +interface LabsProps { + patient: Patient +} + +const Labs = (props: LabsProps) => { + const { patient } = props + + const breadcrumbs = [ + { + i18nKey: 'patient.labs.label', + location: `/patients/${patient.id}/labs`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + return ( + + + + ) +} + +export default Labs diff --git a/src/patients/labs/LabsList.tsx b/src/patients/labs/LabsList.tsx new file mode 100644 index 0000000000..0f49b225fd --- /dev/null +++ b/src/patients/labs/LabsList.tsx @@ -0,0 +1,54 @@ +import { Alert, Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' +import { useHistory } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Patient from '../../shared/model/Patient' +import usePatientLabs from '../hooks/usePatientLabs' + +interface Props { + patient: Patient +} + +const LabsList = (props: Props) => { + const { patient } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientLabs(patient.id) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( +
row.id} + data={data} + columns={[ + { label: t('labs.lab.type'), key: 'type' }, + { + label: t('labs.lab.requestedOn'), + key: 'requestedOn', + formatter: (row) => format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a'), + }, + { label: t('labs.lab.status'), key: 'status' }, + ]} + actions={[{ label: t('actions.view'), action: (row) => history.push(`/labs/${row.id}`) }]} + /> + ) +} + +export default LabsList diff --git a/src/patients/labs/LabsTab.tsx b/src/patients/labs/LabsTab.tsx deleted file mode 100644 index 66296cee95..0000000000 --- a/src/patients/labs/LabsTab.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import { Alert, Table } from '@hospitalrun/components' -import format from 'date-fns/format' -import React, { useEffect, useState } from 'react' -import { useHistory } from 'react-router-dom' - -import PatientRepository from '../../shared/db/PatientRepository' -import useTranslator from '../../shared/hooks/useTranslator' -import Lab from '../../shared/model/Lab' - -interface Props { - patientId: string -} - -const LabsTab = (props: Props) => { - const history = useHistory() - const { patientId } = props - const { t } = useTranslator() - - const [labs, setLabs] = useState([]) - - useEffect(() => { - const fetch = async () => { - const fetchedLabs = await PatientRepository.getLabs(patientId) - setLabs(fetchedLabs) - } - - fetch() - }, [patientId]) - - return ( -
- {(!labs || labs.length === 0) && ( - - )} - {labs && labs.length > 0 && ( -
row.id} - data={labs} - columns={[ - { label: t('labs.lab.type'), key: 'type' }, - { - label: t('labs.lab.requestedOn'), - key: 'requestedOn', - formatter: (row) => format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a'), - }, - { label: t('labs.lab.status'), key: 'status' }, - ]} - actions={[{ label: t('actions.view'), action: (row) => history.push(`/labs/${row.id}`) }]} - /> - )} - - ) -} - -export default LabsTab diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index e039fb9ccd..949912f28d 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -22,7 +22,7 @@ import AppointmentsList from '../appointments/AppointmentsList' import CarePlanTab from '../care-plans/CarePlanTab' import Diagnoses from '../diagnoses/Diagnoses' import GeneralInformation from '../GeneralInformation' -import Labs from '../labs/LabsTab' +import Labs from '../labs/Labs' import Note from '../notes/NoteTab' import { fetchPatient } from '../patient-slice' import RelatedPerson from '../related-persons/RelatedPersonTab' @@ -160,7 +160,7 @@ const ViewPatient = () => { - + From 10d5f97879fd3ecc479d998dbd9728541056c420 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2020 06:23:14 +0000 Subject: [PATCH 38/86] build(deps-dev): bump @testing-library/react from 10.4.9 to 11.0.0 Bumps [@testing-library/react](https://github.com/testing-library/react-testing-library) from 10.4.9 to 11.0.0. - [Release notes](https://github.com/testing-library/react-testing-library/releases) - [Changelog](https://github.com/testing-library/react-testing-library/blob/master/CHANGELOG.md) - [Commits](https://github.com/testing-library/react-testing-library/compare/v10.4.9...v11.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 66be374c28..df37beb963 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "@commitlint/config-conventional": "~9.1.1", "@commitlint/core": "~9.1.1", "@commitlint/prompt": "~9.1.1", - "@testing-library/react": "~10.4.0", + "@testing-library/react": "~11.0.0", "@testing-library/react-hooks": "~3.4.1", "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", From c0d75d65817a4c463997ecb3ac3a0c969e1f6fdd Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2020 06:41:02 +0000 Subject: [PATCH 39/86] build(deps-dev): bump eslint-import-resolver-typescript Bumps [eslint-import-resolver-typescript](https://github.com/alexgorbatchev/eslint-import-resolver-typescript) from 2.2.1 to 2.3.0. - [Release notes](https://github.com/alexgorbatchev/eslint-import-resolver-typescript/releases) - [Changelog](https://github.com/alexgorbatchev/eslint-import-resolver-typescript/blob/master/CHANGELOG.md) - [Commits](https://github.com/alexgorbatchev/eslint-import-resolver-typescript/compare/v2.2.1...v2.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df37beb963..0ac33da78c 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "eslint": "~6.8.0", "eslint-config-airbnb": "~18.2.0", "eslint-config-prettier": "~6.11.0", - "eslint-import-resolver-typescript": "~2.2.0", + "eslint-import-resolver-typescript": "~2.3.0", "eslint-plugin-import": "~2.22.0", "eslint-plugin-jest": "~23.20.0", "eslint-plugin-jsx-a11y": "~6.3.0", From d9754b1b7f66cd1a5264330d6c5f65bfa3aaa948 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 3 Sep 2020 07:03:08 +0000 Subject: [PATCH 40/86] build(deps-dev): bump jest from 24.9.0 to 26.4.2 Bumps [jest](https://github.com/facebook/jest) from 24.9.0 to 26.4.2. - [Release notes](https://github.com/facebook/jest/releases) - [Changelog](https://github.com/facebook/jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/jest/compare/v24.9.0...v26.4.2) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ac33da78c..5411a19174 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "eslint-plugin-react-hooks": "~4.1.0", "history": "4.10.1", "husky": "~4.2.1", - "jest": "~24.9.0", + "jest": "~26.4.2", "lint-staged": "~10.2.0", "memdown": "~5.1.0", "prettier": "~2.1.0", From f0cf094a8d6aff76dcc340128961d00521d604df Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Thu, 3 Sep 2020 20:53:30 +0200 Subject: [PATCH 41/86] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5411a19174..44c31fdaa3 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "eslint-plugin-react-hooks": "~4.1.0", "history": "4.10.1", "husky": "~4.2.1", - "jest": "~26.4.2", + "jest": "24.9.0", "lint-staged": "~10.2.0", "memdown": "~5.1.0", "prettier": "~2.1.0", From 902e81ff74f0bae748d215eec88aa86ed1eb5ee2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2020 06:25:14 +0000 Subject: [PATCH 42/86] build(deps-dev): bump lint-staged from 10.2.13 to 10.3.0 Bumps [lint-staged](https://github.com/okonet/lint-staged) from 10.2.13 to 10.3.0. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Commits](https://github.com/okonet/lint-staged/compare/v10.2.13...v10.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0ac33da78c..4712aae79d 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "history": "4.10.1", "husky": "~4.2.1", "jest": "~24.9.0", - "lint-staged": "~10.2.0", + "lint-staged": "~10.3.0", "memdown": "~5.1.0", "prettier": "~2.1.0", "redux-mock-store": "~1.5.4", From 8d520d0a382422d3e9b04443ae9b839723c25824 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 5 Sep 2020 06:26:27 +0000 Subject: [PATCH 43/86] build(deps-dev): bump eslint-plugin-jest from 23.20.0 to 24.0.0 Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 23.20.0 to 24.0.0. - [Release notes](https://github.com/jest-community/eslint-plugin-jest/releases) - [Changelog](https://github.com/jest-community/eslint-plugin-jest/blob/master/CHANGELOG.md) - [Commits](https://github.com/jest-community/eslint-plugin-jest/compare/v23.20.0...v24.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4a2e0745bd..f420183f36 100644 --- a/package.json +++ b/package.json @@ -92,7 +92,7 @@ "eslint-config-prettier": "~6.11.0", "eslint-import-resolver-typescript": "~2.3.0", "eslint-plugin-import": "~2.22.0", - "eslint-plugin-jest": "~23.20.0", + "eslint-plugin-jest": "~24.0.0", "eslint-plugin-jsx-a11y": "~6.3.0", "eslint-plugin-prettier": "~3.1.2", "eslint-plugin-react": "~7.20.0", From dc3c93eeecac464ba0e3fdc9111d1ed32ac566b2 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 13:51:21 +0800 Subject: [PATCH 44/86] feat(added tests): incidentsDownloadCSV re #2292 --- .../list/ViewIncidentsTable.test.tsx | 56 ++++++++++++++++++- .../shared/utils/DataHelpers.test.ts | 33 +++++++++++ src/incidents/list/ViewIncidentsTable.tsx | 44 +++++++-------- 3 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 src/__tests__/shared/utils/DataHelpers.test.ts diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index d01ef96a52..b3df031f4c 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,4 @@ -import { Table } from '@hospitalrun/components' +import { Table, Dropdown } from '@hospitalrun/components' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -6,7 +6,7 @@ import { act } from 'react-dom/test-utils' import { Router } from 'react-router' import IncidentFilter from '../../../incidents/IncidentFilter' -import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' +import ViewIncidentsTable, { populateExportData } from '../../../incidents/list/ViewIncidentsTable' import IncidentSearchRequest from '../../../incidents/model/IncidentSearchRequest' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' @@ -73,6 +73,58 @@ describe('View Incidents Table', () => { expect(incidentsTable.prop('actionsHeaderText')).toEqual('actions.label') }) + it('should display a download button', async () => { + const expectedIncidents: Incident[] = [ + { + id: 'incidentId1', + code: 'someCode', + date: new Date(2020, 7, 4, 0, 0, 0, 0).toISOString(), + reportedOn: new Date(2020, 8, 4, 0, 0, 0, 0).toISOString(), + reportedBy: 'com.test:user', + status: 'reported', + } as Incident, + ] + const { wrapper } = await setup({ status: IncidentFilter.all }, expectedIncidents) + + const dropDownButton = wrapper.find(Dropdown) + expect(dropDownButton.exists()).toBeTruthy() + }) + + it('should populate export data correctly', async () => { + const data = [ + { + category: 'asdf', + categoryItem: 'asdf', + code: 'I-eClU6OdkR', + createdAt: '2020-09-06T04:02:38.011Z', + date: '2020-09-06T04:02:32.855Z', + department: 'asdf', + description: 'asdf', + id: 'af9f968f-61d9-47c3-9321-5da3f381c38b', + reportedBy: 'some user', + reportedOn: '2020-09-06T04:02:38.011Z', + rev: '1-91d1ba60588b779c9554c7e20e15419c', + status: 'reported', + updatedAt: '2020-09-06T04:02:38.011Z', + }, + ] + + const expectedExportData = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + + const exportData = [{}] + populateExportData(exportData, data) + + expect(exportData).toEqual(expectedExportData) + }) + it('should format the data correctly', async () => { const expectedIncidents: Incident[] = [ { diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts new file mode 100644 index 0000000000..e1bf52e693 --- /dev/null +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -0,0 +1,33 @@ +import { getCSV, DownloadLink } from '../../../shared/util/DataHelpers' + +describe('Use Data Helpers util', () => { + it('should construct csv', () => { + const input = [ + { + code: 'I-eClU6OdkR', + date: '2020-09-06 12:02 PM', + reportedBy: 'some user', + reportedOn: '2020-09-06 12:02 PM', + status: 'reported', + }, + ] + const output = getCSV(input) + const expectedOutput = + '"code","date","reportedBy","reportedOn","status"\r\n"I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + expect(output).toMatch(expectedOutput) + }) + + it('should download data as expected', () => { + const response = DownloadLink('data to be downloaded', 'filename.txt') + + const element = document.createElement('a') + element.setAttribute( + 'href', + `data:text/plain;charset=utf-8,${encodeURIComponent('data to be downloaded')}`, + ) + element.setAttribute('download', 'filename.txt') + + element.style.display = 'none' + expect(response).toEqual(element) + }) +}) diff --git a/src/incidents/list/ViewIncidentsTable.tsx b/src/incidents/list/ViewIncidentsTable.tsx index 4c77371a48..e38f723de2 100644 --- a/src/incidents/list/ViewIncidentsTable.tsx +++ b/src/incidents/list/ViewIncidentsTable.tsx @@ -13,6 +13,27 @@ interface Props { searchRequest: IncidentSearchRequest } +export function populateExportData(dataToPopulate: any, theData: any) { + let first = true + if (theData != null) { + theData.forEach((elm: any) => { + const entry = { + code: elm.code, + date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), + reportedBy: elm.reportedBy, + reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), + status: elm.status, + } + if (first) { + dataToPopulate[0] = entry + first = false + } else { + dataToPopulate.push(entry) + } + }) + } +} + function ViewIncidentsTable(props: Props) { const { searchRequest } = props const { t } = useTranslator() @@ -26,29 +47,8 @@ function ViewIncidentsTable(props: Props) { // filter data const exportData = [{}] - function populateExportData() { - let first = true - if (data != null) { - data.forEach((elm) => { - const entry = { - code: elm.code, - date: format(new Date(elm.date), 'yyyy-MM-dd hh:mm a'), - reportedBy: elm.reportedBy, - reportedOn: format(new Date(elm.reportedOn), 'yyyy-MM-dd hh:mm a'), - status: elm.status, - } - if (first) { - exportData[0] = entry - first = false - } else { - exportData.push(entry) - } - }) - } - } - function downloadCSV() { - populateExportData() + populateExportData(exportData, data) const csv = getCSV(exportData) From 02294e6b7c0c21173dede054fb23fdf4d47a3c96 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 14:33:35 +0800 Subject: [PATCH 45/86] fix(incidentscvstests): fixed timezone test bug re #2292 --- src/__tests__/incidents/list/ViewIncidentsTable.test.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx index b3df031f4c..c0168d6794 100644 --- a/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidentsTable.test.tsx @@ -1,4 +1,5 @@ import { Table, Dropdown } from '@hospitalrun/components' +import format from 'date-fns/format' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' @@ -112,9 +113,9 @@ describe('View Incidents Table', () => { const expectedExportData = [ { code: 'I-eClU6OdkR', - date: '2020-09-06 12:02 PM', + date: format(new Date(data[0].date), 'yyyy-MM-dd hh:mm a'), reportedBy: 'some user', - reportedOn: '2020-09-06 12:02 PM', + reportedOn: format(new Date(data[0].reportedOn), 'yyyy-MM-dd hh:mm a'), status: 'reported', }, ] From d577970aa13c5afb440ec3ae885efceee9c58739 Mon Sep 17 00:00:00 2001 From: reidmeyer Date: Sun, 6 Sep 2020 15:07:17 +0800 Subject: [PATCH 46/86] feat(incidentscsvtest): newline bug in tests re #2292 --- src/__tests__/shared/utils/DataHelpers.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/__tests__/shared/utils/DataHelpers.test.ts b/src/__tests__/shared/utils/DataHelpers.test.ts index e1bf52e693..16342f7414 100644 --- a/src/__tests__/shared/utils/DataHelpers.test.ts +++ b/src/__tests__/shared/utils/DataHelpers.test.ts @@ -11,9 +11,9 @@ describe('Use Data Helpers util', () => { status: 'reported', }, ] - const output = getCSV(input) + const output = getCSV(input).replace(/(\r\n|\n|\r)/gm, '') const expectedOutput = - '"code","date","reportedBy","reportedOn","status"\r\n"I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' + '"code","date","reportedBy","reportedOn","status""I-eClU6OdkR","2020-09-06 12:02 PM","some user","2020-09-06 12:02 PM","reported"' expect(output).toMatch(expectedOutput) }) From 9a14ddd93ec5ea78ee5447f6dcd0f0d12a0644a3 Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Mon, 7 Sep 2020 09:55:24 +0200 Subject: [PATCH 47/86] Fixes missing deps on the incidents effect --- src/incidents/visualize/VisualizeIncidents.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 0071cacbcf..7dd6cdc870 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -37,7 +37,7 @@ const VisualizeIncidents = () => { setShowGraph(true) } } - }, [data, monthlyIncidents]) + }, [data, monthlyIncidents, isLoading, incident]) return !showGraph ? ( From 24f9358cb79a3ac580d584a4f95e0fc5b57c29bf Mon Sep 17 00:00:00 2001 From: Maksim Sinik Date: Mon, 7 Sep 2020 11:06:01 +0200 Subject: [PATCH 48/86] Fixes broken tests --- src/__tests__/shared/components/Sidebar.test.tsx | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/__tests__/shared/components/Sidebar.test.tsx b/src/__tests__/shared/components/Sidebar.test.tsx index 3f0035a657..7602971847 100644 --- a/src/__tests__/shared/components/Sidebar.test.tsx +++ b/src/__tests__/shared/components/Sidebar.test.tsx @@ -465,18 +465,18 @@ describe('Sidebar', () => { const wrapper = setup('/incidents') const listItems = wrapper.find(ListItem) - const lastOne = listItems.length - 1 + const reportsLabel = listItems.length - 2 - expect(listItems.at(lastOne).text().trim()).toBe('incidents.reports.label') + expect(listItems.at(reportsLabel).text().trim()).toBe('incidents.reports.label') expect( listItems - .at(lastOne - 1) + .at(reportsLabel - 1) .text() .trim(), ).toBe('incidents.reports.new') expect( listItems - .at(lastOne - 2) + .at(reportsLabel - 2) .text() .trim(), ).toBe('incidents.label') @@ -522,8 +522,7 @@ describe('Sidebar', () => { const wrapper = setup('/incidents') const listItems = wrapper.find(ListItem) - - expect(listItems.at(8).text().trim()).toEqual('incidents.visualize.label') + expect(listItems.at(10).text().trim()).toEqual('incidents.visualize.label') }) it('should not render the incidents visualize link when user does not have the view incident widgets privileges', () => { @@ -588,7 +587,7 @@ describe('Sidebar', () => { const listItems = wrapper.find(ListItem) act(() => { - const onClick = listItems.at(8).prop('onClick') as any + const onClick = listItems.at(10).prop('onClick') as any onClick() }) From eca339a72d089cafbc348ec431c2d36a327299c5 Mon Sep 17 00:00:00 2001 From: blestab Date: Mon, 7 Sep 2020 14:46:22 +0200 Subject: [PATCH 49/86] fix(shared): couchdb auth popup nginx.conf update --- nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nginx.conf b/nginx.conf index 2400141c1e..74aa917de7 100644 --- a/nginx.conf +++ b/nginx.conf @@ -5,6 +5,8 @@ server { root /usr/share/nginx/html; index index.html index.htm; try_files $uri /index.html; + # replace WWW-Authenticate header in response if authorization failed + more_set_headers -s 401 'WWW-Authenticate: Other realm="App"'; } error_page 500 502 503 504 /50x.html; From 6cf6739e9c8fd44db3f293cc1b3e0145c68ef30a Mon Sep 17 00:00:00 2001 From: blestab Date: Mon, 7 Sep 2020 14:47:13 +0200 Subject: [PATCH 50/86] fix(shared): couchdb auth popup pr lgtm merge issue --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a484f6494d..90da8ffa0b 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "@types/escape-string-regexp": "~2.0.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.5.0", - "date-fns": "~2.15.0", + "date-fns": "~2.16.0", "escape-string-regexp": "~4.0.0", "i18next": "~19.7.0", "i18next-browser-languagedetector": "~6.0.0", From 8eb8f6f0910d8259f15c1878249e630ff3a8be18 Mon Sep 17 00:00:00 2001 From: Rafael Sousa Date: Mon, 7 Sep 2020 14:42:08 -0300 Subject: [PATCH 51/86] feat(caregoal): add care goals to patient (#2360) * feat(caregoal): add care goals to patient * feat(caregoal): improve tests * feat(caregoal): applying recommended changes from review Co-authored-by: Maksim Sinik Co-authored-by: Jack Meyer --- .../care-goals/AddCareGoalModal.test.tsx | 101 ++++++ .../patients/care-goals/CareGoalForm.test.tsx | 292 ++++++++++++++++++ .../patients/care-goals/CareGoalTab.test.tsx | 111 +++++++ .../care-goals/CareGoalTable.test.tsx | 96 ++++++ .../patients/care-goals/ViewCareGoal.test.tsx | 48 +++ .../care-goals/ViewCareGoals.test.tsx | 42 +++ .../patients/hooks/useAddCareGoal.test.tsx | 74 +++++ .../patients/util/validate-caregoal.test.ts | 34 ++ .../patients/view/ViewPatient.test.tsx | 26 +- src/patients/care-goals/AddCareGoalModal.tsx | 82 +++++ src/patients/care-goals/CareGoalForm.tsx | 183 +++++++++++ src/patients/care-goals/CareGoalTab.tsx | 61 ++++ src/patients/care-goals/CareGoalTable.tsx | 66 ++++ src/patients/care-goals/ViewCareGoal.tsx | 19 ++ src/patients/care-goals/ViewCareGoals.tsx | 12 + src/patients/hooks/useAddCareGoal.tsx | 47 +++ src/patients/hooks/useCareGoal.tsx | 23 ++ src/patients/hooks/usePatientCareGoals.tsx | 13 + src/patients/util/validate-caregoal.ts | 74 +++++ src/patients/view/ViewPatient.tsx | 9 + .../enUs/translations/patient/index.ts | 33 ++ src/shared/model/CareGoal.ts | 33 ++ src/shared/model/Patient.ts | 2 + src/shared/model/Permissions.ts | 2 + src/user/user-slice.ts | 2 + 25 files changed, 1483 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalForm.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalTab.test.tsx create mode 100644 src/__tests__/patients/care-goals/CareGoalTable.test.tsx create mode 100644 src/__tests__/patients/care-goals/ViewCareGoal.test.tsx create mode 100644 src/__tests__/patients/care-goals/ViewCareGoals.test.tsx create mode 100644 src/__tests__/patients/hooks/useAddCareGoal.test.tsx create mode 100644 src/__tests__/patients/util/validate-caregoal.test.ts create mode 100644 src/patients/care-goals/AddCareGoalModal.tsx create mode 100644 src/patients/care-goals/CareGoalForm.tsx create mode 100644 src/patients/care-goals/CareGoalTab.tsx create mode 100644 src/patients/care-goals/CareGoalTable.tsx create mode 100644 src/patients/care-goals/ViewCareGoal.tsx create mode 100644 src/patients/care-goals/ViewCareGoals.tsx create mode 100644 src/patients/hooks/useAddCareGoal.tsx create mode 100644 src/patients/hooks/useCareGoal.tsx create mode 100644 src/patients/hooks/usePatientCareGoals.tsx create mode 100644 src/patients/util/validate-caregoal.ts create mode 100644 src/shared/model/CareGoal.ts diff --git a/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx new file mode 100644 index 0000000000..b8b23551b0 --- /dev/null +++ b/src/__tests__/patients/care-goals/AddCareGoalModal.test.tsx @@ -0,0 +1,101 @@ +import { Modal } from '@hospitalrun/components' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Add Care Goal Modal', () => { + const patient = { + givenName: 'given Name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const onCloseSpy = jest.fn() + const setup = () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + jest.spyOn(PatientRepository, 'saveOrUpdate') + const history = createMemoryHistory() + const wrapper = mount( + + + , + ) + + wrapper.update() + return { wrapper } + } + + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render a modal', () => { + const { wrapper } = setup() + + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const closeButton = modal.prop('closeButton') + + expect(modal).toHaveLength(1) + expect(modal.prop('title')).toEqual('patient.careGoal.new') + expect(sucessButton?.children).toEqual('patient.careGoal.new') + expect(sucessButton?.icon).toEqual('add') + expect(closeButton?.children).toEqual('actions.cancel') + }) + + it('should render a care goal form', () => { + const { wrapper } = setup() + + const careGoalForm = wrapper.find(CareGoalForm) + expect(careGoalForm).toHaveLength(1) + }) + + it('should save care goal when save button is clicked and close', async () => { + const expectedCreatedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedCreatedDate) + + const expectedCareGoal = { + id: '123', + description: 'some description', + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + createdOn: expectedCreatedDate, + } + + const { wrapper } = setup() + await act(async () => { + const careGoalForm = wrapper.find(CareGoalForm) + const onChange = careGoalForm.prop('onChange') as any + await onChange(expectedCareGoal) + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(Modal) + const sucessButton = modal.prop('successButton') + const onClick = sucessButton?.onClick as any + await onClick() + }) + + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith({ + ...patient, + careGoals: [expectedCareGoal], + }) + + expect(onCloseSpy).toHaveBeenCalledTimes(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalForm.test.tsx b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx new file mode 100644 index 0000000000..fc94456ab9 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalForm.test.tsx @@ -0,0 +1,292 @@ +import { Alert } from '@hospitalrun/components' +import { addMonths, addDays } from 'date-fns' +import { mount } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' + +describe('Care Goal Form', () => { + const onCareGoalChangeSpy = jest.fn() + const careGoal = { + description: 'some description', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, + } as CareGoal + + const setup = (disabled = false, initializeCareGoal = true, error?: any) => { + const wrapper = mount( + , + ) + + return wrapper + } + + it('should render a description input', () => { + const wrapper = setup() + + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + + expect(descriptionInput).toHaveLength(1) + expect(descriptionInput.prop('label')).toEqual('patient.careGoal.description') + expect(descriptionInput.prop('isRequired')).toBeTruthy() + expect(descriptionInput.prop('value')).toBe(careGoal.description) + }) + + it('should call onChange handler when description changes', () => { + const expectedDescription = 'some new description' + const wrapper = setup(false, false) + + act(() => { + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const onChange = descriptionInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedDescription } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ description: expectedDescription }) + }) + + it('should render a priority selector', () => { + const wrapper = setup() + + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + + expect(priority).toHaveLength(1) + expect(priority.prop('label')).toEqual('patient.careGoal.priority.label') + expect(priority.prop('isRequired')).toBeTruthy() + expect(priority.prop('defaultSelected')[0].value).toBe(careGoal.priority) + expect(priority.prop('options')).toEqual([ + { + label: 'patient.careGoal.priority.low', + value: 'low', + }, + { + label: 'patient.careGoal.priority.medium', + value: 'medium', + }, + { + label: 'patient.careGoal.priority.high', + value: 'high', + }, + ]) + }) + + it('should call onChange handler when priority changes', () => { + const expectedPriority = 'high' + const wrapper = setup(false, false) + + act(() => { + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const onChange = prioritySelector.prop('onChange') as any + onChange([expectedPriority]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ priority: expectedPriority }) + }) + + it('should render a status selector', () => { + const wrapper = setup() + + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + + expect(status).toHaveLength(1) + expect(status.prop('label')).toEqual('patient.careGoal.status') + expect(status.prop('isRequired')).toBeTruthy() + expect(status.prop('defaultSelected')[0].value).toBe(careGoal.status) + expect(status.prop('options')).toEqual( + Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when status changes', () => { + const expectedStatus = CareGoalStatus.OnHold + const wrapper = setup(false, false) + + act(() => { + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const onChange = statusSelector.prop('onChange') as any + onChange([expectedStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ status: expectedStatus }) + }) + + it('should render the achievement status selector', () => { + const wrapper = setup() + + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + expect(achievementStatus).toHaveLength(1) + expect(achievementStatus.prop('label')).toEqual('patient.careGoal.achievementStatus') + expect(achievementStatus.prop('isRequired')).toBeTruthy() + expect(achievementStatus.prop('defaultSelected')[0].value).toBe(careGoal.achievementStatus) + expect(achievementStatus.prop('options')).toEqual( + Object.values(CareGoalAchievementStatus).map((v) => ({ label: v, value: v })), + ) + }) + + it('should call onChange handler when achievement status change', () => { + const expectedAchievementStatus = CareGoalAchievementStatus.Improving + const wrapper = setup(false, false) + + act(() => { + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const onChange = achievementStatusSelector.prop('onChange') as any + onChange([expectedAchievementStatus]) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ + achievementStatus: expectedAchievementStatus, + }) + }) + + it('should render a start date picker', () => { + const wrapper = setup() + + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + expect(startDatePicker).toHaveLength(1) + expect(startDatePicker.prop('label')).toEqual('patient.careGoal.startDate') + expect(startDatePicker.prop('isRequired')).toBeTruthy() + expect(startDatePicker.prop('value')).toEqual(new Date(careGoal.startDate)) + }) + + it('should call onChange handler when start date change', () => { + const expectedStartDate = addDays(1, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const onChange = startDatePicker.prop('onChange') as any + onChange(expectedStartDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ startDate: expectedStartDate.toISOString() }) + }) + + it('should render a due date picker', () => { + const wrapper = setup() + + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + expect(dueDatePicker).toHaveLength(1) + expect(dueDatePicker.prop('label')).toEqual('patient.careGoal.dueDate') + expect(dueDatePicker.prop('isRequired')).toBeTruthy() + expect(dueDatePicker.prop('value')).toEqual(new Date(careGoal.dueDate)) + }) + + it('should call onChange handler when due date change', () => { + const expectedDueDate = addDays(31, new Date().getDate()) + const wrapper = setup(false, false) + + act(() => { + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const onChange = dueDatePicker.prop('onChange') as any + onChange(expectedDueDate) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ dueDate: expectedDueDate.toISOString() }) + }) + + it('should render a note input', () => { + const wrapper = setup() + + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + expect(noteInput).toHaveLength(1) + expect(noteInput.prop('label')).toEqual('patient.careGoal.note') + expect(noteInput.prop('isRequired')).toBeFalsy() + expect(noteInput.prop('value')).toEqual(careGoal.note) + }) + + it('should call onChange handler when note change', () => { + const expectedNote = 'some new note' + const wrapper = setup(false, false) + + act(() => { + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + const onChange = noteInput.prop('onChange') as any + onChange({ currentTarget: { value: expectedNote } }) + }) + + expect(onCareGoalChangeSpy).toHaveBeenCalledWith({ note: expectedNote }) + }) + + it('should render all the forms fields disabled if the form is disabled', () => { + const wrapper = setup(true) + + const description = wrapper.findWhere((w) => w.prop('name') === 'description') + const priority = wrapper.findWhere((w) => w.prop('name') === 'priority') + const status = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatus = wrapper.findWhere((w) => w.prop('name') === 'achievementStatus') + const startDate = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDate = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const note = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(description.prop('isEditable')).toBeFalsy() + expect(priority.prop('isEditable')).toBeFalsy() + expect(status.prop('isEditable')).toBeFalsy() + expect(achievementStatus.prop('isEditable')).toBeFalsy() + expect(startDate.prop('isEditable')).toBeFalsy() + expect(dueDate.prop('isEditable')).toBeFalsy() + expect(note.prop('isEditable')).toBeFalsy() + }) + + it('should render the forms field in an error state', () => { + const expectedError = { + message: 'some error message', + description: 'some description error', + status: 'some status error', + achievementStatus: 'some achievement status error', + priority: 'some priority error', + startDate: 'some start date error', + dueDate: 'some due date error', + note: 'some note error', + } + + const wrapper = setup(false, false, expectedError) + + const alert = wrapper.find(Alert) + const descriptionInput = wrapper.findWhere((w) => w.prop('name') === 'description') + const prioritySelector = wrapper.findWhere((w) => w.prop('name') === 'priority') + const statusSelector = wrapper.findWhere((w) => w.prop('name') === 'status') + const achievementStatusSelector = wrapper.findWhere( + (w) => w.prop('name') === 'achievementStatus', + ) + const startDatePicker = wrapper.findWhere((w) => w.prop('name') === 'startDate') + const dueDatePicker = wrapper.findWhere((w) => w.prop('name') === 'dueDate') + const noteInput = wrapper.findWhere((w) => w.prop('name') === 'note') + + expect(alert).toHaveLength(1) + expect(alert.prop('message')).toEqual(expectedError.message) + + expect(descriptionInput.prop('isInvalid')).toBeTruthy() + expect(descriptionInput.prop('feedback')).toEqual(expectedError.description) + + expect(prioritySelector.prop('isInvalid')).toBeTruthy() + // expect(prioritySelector.prop('feedback')).toEqual(expectedError.priority) + + expect(statusSelector.prop('isInvalid')).toBeTruthy() + // expect(statusSelector.prop('feedback')).toEqual(expectedError.status) + + expect(achievementStatusSelector.prop('isInvalid')).toBeTruthy() + // expect(achievementStatusSelector.prop('feedback')).toEqual(expectedError.achievementStatus) + + expect(startDatePicker.prop('isInvalid')).toBeTruthy() + expect(startDatePicker.prop('feedback')).toEqual(expectedError.startDate) + + expect(dueDatePicker.prop('isInvalid')).toBeTruthy() + expect(dueDatePicker.prop('feedback')).toEqual(expectedError.dueDate) + + expect(noteInput.prop('isInvalid')).toBeTruthy() + expect(noteInput.prop('feedback')).toEqual(expectedError.note) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTab.test.tsx b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx new file mode 100644 index 0000000000..57b0d42877 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTab.test.tsx @@ -0,0 +1,111 @@ +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 AddCareGoalModal from '../../../patients/care-goals/AddCareGoalModal' +import CareGoalTab from '../../../patients/care-goals/CareGoalTab' +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Care Goals Tab', () => { + const patient = { id: 'patientId' } as Patient + + const setup = async (route: string, permissions: Permissions[]) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ user: { permissions } } as any) + const history = createMemoryHistory() + history.push(route) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + return wrapper as ReactWrapper + } + + it('should render add care goal button if user has correct permissions', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + const addNewButton = wrapper.find('Button').at(0) + expect(addNewButton).toHaveLength(1) + expect(addNewButton.text().trim()).toEqual('patient.careGoal.new') + }) + + it('should not render add care goal button if user does not have permissions', async () => { + const wrapper = await setup('patients/123/care-goals', []) + + const addNewButton = wrapper.find('Button') + expect(addNewButton).toHaveLength(0) + }) + + it('should open the add care goal modal on click', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeTruthy() + }) + + it('should close the modal when the close button is clicked', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.AddCareGoal]) + + await act(async () => { + const addNewButton = wrapper.find('Button').at(0) + const onClick = addNewButton.prop('onClick') as any + onClick() + }) + + wrapper.update() + + await act(async () => { + const modal = wrapper.find(AddCareGoalModal) + const onClose = modal.prop('onCloseButtonClick') as any + onClose() + }) + + wrapper.update() + + const modal = wrapper.find(AddCareGoalModal) + expect(modal.prop('show')).toBeFalsy() + }) + + it('should render care goal table when on patients/123/care-goals', async () => { + const wrapper = await setup('patients/123/care-goals', [Permissions.ReadCareGoal]) + + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable).toHaveLength(1) + }) + + it('should render care goal view when on patients/123/care-goals/456', async () => { + const wrapper = await setup('patients/123/care-goals/456', [Permissions.ReadCareGoal]) + + const viewCareGoal = wrapper.find(ViewCareGoal) + expect(viewCareGoal).toHaveLength(1) + }) +}) diff --git a/src/__tests__/patients/care-goals/CareGoalTable.test.tsx b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx new file mode 100644 index 0000000000..0917055e96 --- /dev/null +++ b/src/__tests__/patients/care-goals/CareGoalTable.test.tsx @@ -0,0 +1,96 @@ +import { Table, Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('Care Goal Table', () => { + const careGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + createdOn: new Date().toISOString(), + note: 'some note', + } + + const patient = { + givenName: 'given Name', + fullName: 'full Name', + careGoals: [careGoal], + } as Patient + + const setup = async (expectedPatient = patient) => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(expectedPatient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + wrapper.update() + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a table', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + const columns = table.prop('columns') + + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.description', key: 'description' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.startDate', key: 'startDate' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.dueDate', key: 'dueDate' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ label: 'patient.careGoal.status', key: 'status' }), + ) + + const actions = table.prop('actions') as any + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + expect(table.prop('data')).toEqual(patient.careGoals) + }) + + it('should navigate to the care goal view when the view details button is clicked', async () => { + const { wrapper, history } = await setup() + + const tr = wrapper.find('tr').at(1) + + act(() => { + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals/${careGoal.id}`) + }) + + it('should display a warning if there are no care goals', async () => { + const { wrapper } = await setup({ ...patient, careGoals: [] }) + + expect(wrapper.exists(Alert)).toBeTruthy() + const alert = wrapper.find(Alert) + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.careGoals.warning.noCareGoals') + expect(alert.prop('message')).toEqual('patient.careGoals.warning.addCareGoalAbove') + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx new file mode 100644 index 0000000000..380c4f8f20 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoal.test.tsx @@ -0,0 +1,48 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Router, Route } from 'react-router-dom' + +import CareGoalForm from '../../../patients/care-goals/CareGoalForm' +import ViewCareGoal from '../../../patients/care-goals/ViewCareGoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goal', () => { + const patient = { + id: '123', + givenName: 'given Name', + fullName: 'full Name', + careGoals: [{ id: '123', description: 'some description' }], + } as Patient + + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals/${patient.careGoals[0].id}`) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + wrapper.update() + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render the care goal form', async () => { + const { wrapper } = await setup() + const careGoalForm = wrapper.find(CareGoalForm) + + expect(careGoalForm).toHaveLength(1) + expect(careGoalForm.prop('careGoal')).toEqual(patient.careGoals[0]) + }) +}) diff --git a/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx new file mode 100644 index 0000000000..a80f7a7671 --- /dev/null +++ b/src/__tests__/patients/care-goals/ViewCareGoals.test.tsx @@ -0,0 +1,42 @@ +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import { act } from 'react-dom/test-utils' +import { Route, Router } from 'react-router-dom' + +import CareGoalTable from '../../../patients/care-goals/CareGoalTable' +import ViewCareGoals from '../../../patients/care-goals/ViewCareGoals' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' + +describe('View Care Goals', () => { + const patient = { id: '123', careGoals: [] as CareGoal[] } as Patient + const setup = async () => { + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + + const history = createMemoryHistory() + history.push(`/patients/${patient.id}/care-goals`) + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + + return { wrapper: wrapper as ReactWrapper } + } + + it('should render a care goals table with the patient id', async () => { + const { wrapper } = await setup() + + expect(wrapper.exists(CareGoalTable)).toBeTruthy() + const careGoalTable = wrapper.find(CareGoalTable) + expect(careGoalTable.prop('patientId')).toEqual(patient.id) + }) +}) diff --git a/src/__tests__/patients/hooks/useAddCareGoal.test.tsx b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx new file mode 100644 index 0000000000..be5885447e --- /dev/null +++ b/src/__tests__/patients/hooks/useAddCareGoal.test.tsx @@ -0,0 +1,74 @@ +import useAddCareGoal from '../../../patients/hooks/useAddCareGoal' +import { CareGoalError } from '../../../patients/util/validate-caregoal' +import * as validateCareGoal from '../../../patients/util/validate-caregoal' +import PatientRepository from '../../../shared/db/PatientRepository' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../../shared/model/CareGoal' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add care goal', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should add a care goal to the patient', async () => { + const expectedDate = new Date() + Date.now = jest.fn().mockReturnValue(expectedDate) + + const expectedCareGoal: CareGoal = { + id: '123', + description: 'some description', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.Improving, + startDate: new Date().toISOString(), + dueDate: new Date().toISOString(), + note: 'some note', + createdOn: expectedDate.toISOString(), + } + + const givenPatient = { + id: '123', + givenName: 'given name', + fullName: 'full name', + careGoals: [] as CareGoal[], + } as Patient + + const expectedPatient = { + ...givenPatient, + careGoals: [expectedCareGoal], + } as Patient + + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedCareGoal.id) + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + + const result = await executeMutation(() => useAddCareGoal(), { + patientId: givenPatient.id, + careGoal: expectedCareGoal, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(result).toEqual([expectedCareGoal]) + }) + + it('should throw an error if validation fails', async () => { + const expectedError = { + message: 'patient.careGoal.error.unableToAdd', + description: 'some error', + } + jest.spyOn(validateCareGoal, 'default').mockReturnValue(expectedError as CareGoalError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddCareGoal(), { patientId: '123', careGoal: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) +}) diff --git a/src/__tests__/patients/util/validate-caregoal.test.ts b/src/__tests__/patients/util/validate-caregoal.test.ts new file mode 100644 index 0000000000..f0b52ee989 --- /dev/null +++ b/src/__tests__/patients/util/validate-caregoal.test.ts @@ -0,0 +1,34 @@ +import { subDays } from 'date-fns' + +import validateCareGoal from '../../../patients/util/validate-caregoal' +import CareGoal from '../../../shared/model/CareGoal' + +describe('validate care goal', () => { + it('should validate required fields', () => { + const expectedError = { + description: 'patient.careGoal.error.descriptionRequired', + priority: 'patient.careGoal.error.priorityRequired', + status: 'patient.careGoal.error.statusRequired', + achievementStatus: 'patient.careGoal.error.achievementStatusRequired', + startDate: 'patient.careGoal.error.startDate', + dueDate: 'patient.careGoal.error.dueDate', + } + + const actualError = validateCareGoal({} as CareGoal) + + expect(actualError).toEqual(expectedError) + }) + + it('should validate the start date time is before end date time', () => { + const givenCareGoal = { + startDate: new Date().toISOString(), + dueDate: subDays(new Date(), 1).toISOString(), + } as CareGoal + + const actualError = validateCareGoal(givenCareGoal) + + expect(actualError).toEqual( + expect.objectContaining({ dueDate: 'patient.careGoal.error.dueDateMustBeAfterStartDate' }), + ) + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 5fd316d904..d666764cbf 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -131,7 +131,7 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(9) + expect(tabs).toHaveLength(10) expect(tabs.at(0).prop('label')).toEqual('patient.generalInformation') expect(tabs.at(1).prop('label')).toEqual('patient.relatedPersons.label') expect(tabs.at(2).prop('label')).toEqual('scheduling.appointments.label') @@ -140,7 +140,8 @@ describe('ViewPatient', () => { expect(tabs.at(5).prop('label')).toEqual('patient.notes.label') expect(tabs.at(6).prop('label')).toEqual('patient.labs.label') expect(tabs.at(7).prop('label')).toEqual('patient.carePlan.label') - expect(tabs.at(8).prop('label')).toEqual('patient.visits.label') + expect(tabs.at(8).prop('label')).toEqual('patient.careGoal.label') + expect(tabs.at(9).prop('label')).toEqual('patient.visits.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -320,4 +321,25 @@ describe('ViewPatient', () => { expect(tabs.at(7).prop('active')).toBeTruthy() expect(carePlansTab).toHaveLength(1) }) + + it('should mark the care goals tab as active when it is clicked and render the care goal tab component when route is /patients/:id/care-goals', async () => { + const { wrapper } = await setup() + + await act(async () => { + const tabHeader = wrapper.find(TabsHeader) + const tabs = tabHeader.find(Tab) + const onClick = tabs.at(8).prop('onClick') as any + onClick() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const careGoalsTab = tabs.at(8) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/care-goals`) + expect(careGoalsTab.prop('active')).toBeTruthy() + expect(careGoalsTab).toHaveLength(1) + }) }) diff --git a/src/patients/care-goals/AddCareGoalModal.tsx b/src/patients/care-goals/AddCareGoalModal.tsx new file mode 100644 index 0000000000..ce47b8b767 --- /dev/null +++ b/src/patients/care-goals/AddCareGoalModal.tsx @@ -0,0 +1,82 @@ +import { Modal } from '@hospitalrun/components' +import { addMonths } from 'date-fns' +import React, { useState, useEffect } from 'react' + +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' +import Patient from '../../shared/model/Patient' +import useAddCareGoal from '../hooks/useAddCareGoal' +import { CareGoalError } from '../util/validate-caregoal' +import CareGoalForm from './CareGoalForm' + +interface Props { + patient: Patient + show: boolean + onCloseButtonClick: () => void +} + +const initialCareGoalState = { + description: '', + startDate: new Date().toISOString(), + dueDate: addMonths(new Date(), 1).toISOString(), + note: '', + priority: 'medium', + status: CareGoalStatus.Accepted, + achievementStatus: CareGoalAchievementStatus.InProgress, +} as CareGoal + +const AddCareGoalModal = (props: Props) => { + const { t } = useTranslator() + const { patient, show, onCloseButtonClick } = props + const [mutate] = useAddCareGoal() + const [careGoal, setCareGoal] = useState(initialCareGoalState) + const [careGoalError, setCareGoalError] = useState(undefined) + + useEffect(() => { + setCareGoal(initialCareGoalState) + }, [show]) + + const onClose = () => { + onCloseButtonClick() + } + + const onCareGoalChange = (newCareGoal: Partial) => { + setCareGoal(newCareGoal as CareGoal) + } + + const onSaveButtonClick = async () => { + try { + await mutate({ patientId: patient.id, careGoal }) + onClose() + } catch (e) { + setCareGoalError(e) + } + } + + const body = ( + + ) + + return ( + + ) +} + +export default AddCareGoalModal diff --git a/src/patients/care-goals/CareGoalForm.tsx b/src/patients/care-goals/CareGoalForm.tsx new file mode 100644 index 0000000000..b53d7c7a38 --- /dev/null +++ b/src/patients/care-goals/CareGoalForm.tsx @@ -0,0 +1,183 @@ +import { Alert, Row, Column } from '@hospitalrun/components' +import React, { useState } from 'react' + +import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' +import useTranslator from '../../shared/hooks/useTranslator' +import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' + +interface Error { + message?: string + description?: string + status?: string + achievementStatus?: string + priority?: string + startDate?: string + dueDate?: string + note?: string +} + +interface Props { + careGoal: Partial + careGoalError?: Error + onChange?: (newCareGoal: Partial) => void + disabled: boolean +} + +const CareGoalForm = (props: Props) => { + const { careGoal, careGoalError, disabled, onChange } = props + const { t } = useTranslator() + + const [priority, setPriority] = useState(careGoal.priority) + const [status, setStatus] = useState(careGoal.status) + const [achievementStatus, setAchievementStatus] = useState(careGoal.achievementStatus) + + const priorityOptions: Option[] = [ + { label: t('patient.careGoal.priority.low'), value: 'low' }, + { label: t('patient.careGoal.priority.medium'), value: 'medium' }, + { label: t('patient.careGoal.priority.high'), value: 'high' }, + ] + + const statusOptions: Option[] = Object.values(CareGoalStatus).map((v) => ({ label: v, value: v })) + + const achievementsStatusOptions: Option[] = Object.values(CareGoalAchievementStatus).map((v) => ({ + label: v, + value: v, + })) + + const onFieldChange = ( + name: string, + value: string | CareGoalStatus | CareGoalAchievementStatus, + ) => { + if (onChange) { + const newCareGoal = { + ...careGoal, + [name]: value, + } + onChange(newCareGoal) + } + } + + const onPriorityChange = (values: string[]) => { + const value = values[0] as 'low' | 'medium' | 'high' + + onFieldChange('priority', value) + setPriority(value) + } + + return ( + + {careGoalError?.message && } + + + onFieldChange('description', event.currentTarget.value)} + /> + + + + + value === priority)} + isEditable={!disabled} + isInvalid={!!careGoalError?.priority} + onChange={onPriorityChange} + /> + + + + + value === status)} + isEditable={!disabled} + isInvalid={!!careGoalError?.status} + onChange={(values) => { + onFieldChange('status', values[0]) + setStatus(values[0] as CareGoalStatus) + }} + /> + + + value === achievementStatus, + )} + isEditable={!disabled} + isInvalid={!!careGoalError?.achievementStatus} + onChange={(values) => { + onFieldChange('achievementStatus', values[0]) + setAchievementStatus(values[0] as CareGoalAchievementStatus) + }} + /> + + + + + (date ? onFieldChange('startDate', date.toISOString()) : null)} + /> + + + (date ? onFieldChange('dueDate', date.toISOString()) : null)} + /> + + + + + onFieldChange('note', event.currentTarget.value)} + /> + + + + ) +} + +CareGoalForm.defaultProps = { + disabled: false, +} + +export default CareGoalForm diff --git a/src/patients/care-goals/CareGoalTab.tsx b/src/patients/care-goals/CareGoalTab.tsx new file mode 100644 index 0000000000..7d6816c1d2 --- /dev/null +++ b/src/patients/care-goals/CareGoalTab.tsx @@ -0,0 +1,61 @@ +import { Button } from '@hospitalrun/components' +import React, { useState } from 'react' +import { useSelector } from 'react-redux' +import { useParams, Route, Switch } from 'react-router-dom' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import usePatient from '../hooks/usePatient' +import AddCareGoalModal from './AddCareGoalModal' +import ViewCareGoal from './ViewCareGoal' +import ViewCareGoals from './ViewCareGoals' + +const CareGoalTab = () => { + const { id: patientId } = useParams() + const { t } = useTranslator() + const { permissions } = useSelector((state: RootState) => state.user) + const { data, status } = usePatient(patientId) + const [showAddCareGoalModal, setShowAddCareGoalModal] = useState(false) + + if (data === undefined || status === 'loading') { + return + } + + return ( + <> +
+
+ {permissions.includes(Permissions.AddCareGoal) && ( + + )} +
+
+
+ + + + + + + + + setShowAddCareGoalModal(false)} + /> + + ) +} + +export default CareGoalTab diff --git a/src/patients/care-goals/CareGoalTable.tsx b/src/patients/care-goals/CareGoalTable.tsx new file mode 100644 index 0000000000..6cf95cf39a --- /dev/null +++ b/src/patients/care-goals/CareGoalTable.tsx @@ -0,0 +1,66 @@ +import { Alert, Table } from '@hospitalrun/components' +import { format } from 'date-fns' +import React from 'react' +import { useHistory } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import usePatientCareGoals from '../hooks/usePatientCareGoals' + +interface Props { + patientId: string +} + +const CareGoalTable = (props: Props) => { + const { patientId } = props + const history = useHistory() + const { t } = useTranslator() + const { data, status } = usePatientCareGoals(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( +
row.id} + data={data} + columns={[ + { label: t('patient.careGoal.description'), key: 'description' }, + { + label: t('patient.careGoal.startDate'), + key: 'startDate', + formatter: (row) => format(new Date(row.startDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.dueDate'), + key: 'dueDate', + formatter: (row) => format(new Date(row.dueDate), 'yyyy-MM-dd'), + }, + { + label: t('patient.careGoal.status'), + key: 'status', + }, + ]} + actionsHeaderText={t('actions.label')} + actions={[ + { + label: 'actions.view', + action: (row) => history.push(`/patients/${patientId}/care-goals/${row.id}`), + }, + ]} + /> + ) +} + +export default CareGoalTable diff --git a/src/patients/care-goals/ViewCareGoal.tsx b/src/patients/care-goals/ViewCareGoal.tsx new file mode 100644 index 0000000000..83573322b8 --- /dev/null +++ b/src/patients/care-goals/ViewCareGoal.tsx @@ -0,0 +1,19 @@ +import React from 'react' +import { useParams } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useCareGoal from '../hooks/useCareGoal' +import CareGoalForm from './CareGoalForm' + +const ViewCareGoal = () => { + const { careGoalId, id: patientId } = useParams() + const { data: careGoal, status } = useCareGoal(patientId, careGoalId) + + if (careGoal === undefined || status === 'loading') { + return + } + + return +} + +export default ViewCareGoal diff --git a/src/patients/care-goals/ViewCareGoals.tsx b/src/patients/care-goals/ViewCareGoals.tsx new file mode 100644 index 0000000000..d0b41ae25c --- /dev/null +++ b/src/patients/care-goals/ViewCareGoals.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { useParams } from 'react-router-dom' + +import CareGoalTable from './CareGoalTable' + +const ViewCareGoals = () => { + const { id } = useParams() + + return +} + +export default ViewCareGoals diff --git a/src/patients/hooks/useAddCareGoal.tsx b/src/patients/hooks/useAddCareGoal.tsx new file mode 100644 index 0000000000..f18bd2a7e9 --- /dev/null +++ b/src/patients/hooks/useAddCareGoal.tsx @@ -0,0 +1,47 @@ +import { isEmpty } from 'lodash' +import { queryCache, useMutation } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' +import { uuid } from '../../shared/util/uuid' +import validateCareGoal from '../util/validate-caregoal' + +interface AddCareGoalRequest { + patientId: string + careGoal: Omit +} + +async function addCareGoal(request: AddCareGoalRequest): Promise { + const error = validateCareGoal(request.careGoal) + + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const careGoals = patient.careGoals ? [...patient.careGoals] : [] + + const newCareGoal: CareGoal = { + id: uuid(), + createdOn: new Date(Date.now()).toISOString(), + ...request.careGoal, + } + careGoals.push(newCareGoal) + + await PatientRepository.saveOrUpdate({ + ...patient, + careGoals, + }) + + return careGoals + } + + error.message = 'patient.careGoal.error.unableToAdd' + throw error +} + +export default function useAddCareGoal() { + return useMutation(addCareGoal, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['care-goals', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/useCareGoal.tsx b/src/patients/hooks/useCareGoal.tsx new file mode 100644 index 0000000000..1cc240cfdd --- /dev/null +++ b/src/patients/hooks/useCareGoal.tsx @@ -0,0 +1,23 @@ +import { useQuery, QueryKey } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function getCareGoal( + _: QueryKey, + patientId: string, + careGoalId: string, +): Promise { + const patient = await PatientRepository.find(patientId) + const maybeCareGoal = patient.careGoals?.find((c) => c.id === careGoalId) + + if (!maybeCareGoal) { + throw new Error('Care Goal not found') + } + + return maybeCareGoal +} + +export default function useCareGoal(patientId: string, careGoalId: string) { + return useQuery(['care-goals', patientId, careGoalId], getCareGoal) +} diff --git a/src/patients/hooks/usePatientCareGoals.tsx b/src/patients/hooks/usePatientCareGoals.tsx new file mode 100644 index 0000000000..291159b4be --- /dev/null +++ b/src/patients/hooks/usePatientCareGoals.tsx @@ -0,0 +1,13 @@ +import { useQuery, QueryKey } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import CareGoal from '../../shared/model/CareGoal' + +async function fetchPatientCareGoals(_: QueryKey, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.careGoals || [] +} + +export default function usePatientCareGoals(patientId: string) { + return useQuery(['care-goals', patientId], fetchPatientCareGoals) +} diff --git a/src/patients/util/validate-caregoal.ts b/src/patients/util/validate-caregoal.ts new file mode 100644 index 0000000000..75cf848dbc --- /dev/null +++ b/src/patients/util/validate-caregoal.ts @@ -0,0 +1,74 @@ +import { isBefore } from 'date-fns' + +import CareGoal from '../../shared/model/CareGoal' + +export class CareGoalError extends Error { + message: string + + description?: string + + status?: string + + achievementStatus?: string + + priority?: string + + startDate?: string + + dueDate?: string + + constructor( + message: string, + description: string, + status: string, + achievementStatus: string, + priority: string, + startDate: string, + dueDate: string, + ) { + super(message) + this.message = message + this.description = description + this.status = status + this.achievementStatus = achievementStatus + this.priority = priority + this.startDate = startDate + this.dueDate = dueDate + } +} + +export default function validateCareGoal(careGoal: Partial): CareGoalError { + const error = {} as CareGoalError + + if (!careGoal.description) { + error.description = 'patient.careGoal.error.descriptionRequired' + } + + if (!careGoal.status) { + error.status = 'patient.careGoal.error.statusRequired' + } + + if (!careGoal.achievementStatus) { + error.achievementStatus = 'patient.careGoal.error.achievementStatusRequired' + } + + if (!careGoal.priority) { + error.priority = 'patient.careGoal.error.priorityRequired' + } + + if (!careGoal.startDate) { + error.startDate = 'patient.careGoal.error.startDate' + } + + if (!careGoal.dueDate) { + error.dueDate = 'patient.careGoal.error.dueDate' + } + + if (careGoal.startDate && careGoal.dueDate) { + if (isBefore(new Date(careGoal.dueDate), new Date(careGoal.startDate))) { + error.dueDate = 'patient.careGoal.error.dueDateMustBeAfterStartDate' + } + } + + return error +} diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 949912f28d..bc951a4ab6 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -19,6 +19,7 @@ import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import Allergies from '../allergies/Allergies' import AppointmentsList from '../appointments/AppointmentsList' +import CareGoalTab from '../care-goals/CareGoalTab' import CarePlanTab from '../care-plans/CarePlanTab' import Diagnoses from '../diagnoses/Diagnoses' import GeneralInformation from '../GeneralInformation' @@ -134,6 +135,11 @@ const ViewPatient = () => { label={t('patient.carePlan.label')} onClick={() => history.push(`/patients/${patient.id}/care-plans`)} /> + history.push(`/patients/${patient.id}/care-goals`)} + /> { + + + diff --git a/src/shared/locales/enUs/translations/patient/index.ts b/src/shared/locales/enUs/translations/patient/index.ts index 9c40cd1b00..df9098ed62 100644 --- a/src/shared/locales/enUs/translations/patient/index.ts +++ b/src/shared/locales/enUs/translations/patient/index.ts @@ -119,6 +119,39 @@ export default { }, noLabsMessage: 'No labs requests for this person.', }, + careGoal: { + new: 'Add Care Goal', + label: 'Care Goals', + title: 'Title', + description: 'Description', + status: 'Status', + achievementStatus: 'Achievement Status', + priority: { + label: 'Priority', + low: 'low', + medium: 'medium', + high: 'high', + }, + startDate: 'Start Date', + dueDate: 'Due Date', + note: 'Note', + error: { + unableToAdd: 'Unable to add a new care goal.', + descriptionRequired: 'Description is required.', + priorityRequired: 'Priority is required.', + statusRequired: 'Status is required.', + achievementStatusRequired: 'Achievement Status is required.', + startDate: 'Start date is required.', + dueDate: 'Due date is required.', + dueDateMustBeAfterStartDate: 'Due date must be after start date', + }, + }, + careGoals: { + warning: { + noCareGoals: 'No Care Goals', + addCareGoalAbove: 'Add a care goal using the button above.', + }, + }, carePlan: { new: 'Add Care Plan', label: 'Care Plans', diff --git a/src/shared/model/CareGoal.ts b/src/shared/model/CareGoal.ts new file mode 100644 index 0000000000..1513690702 --- /dev/null +++ b/src/shared/model/CareGoal.ts @@ -0,0 +1,33 @@ +export enum CareGoalStatus { + Proposed = 'proposed', + Planned = 'planned', + Accepted = 'accepted', + Active = 'active', + OnHold = 'on hold', + Completed = 'completed', + Cancelled = 'cancelled', + Rejected = 'rejected', +} + +export enum CareGoalAchievementStatus { + InProgress = 'in progress', + Improving = 'improving', + Worsening = 'worsening', + NoChange = 'no change', + Achieved = 'achieved', + NotAchieved = 'not achieved', + NoProgress = 'no progress', + NotAttainable = 'not attainable', +} + +export default interface CareGoal { + id: string + status: CareGoalStatus + achievementStatus: CareGoalAchievementStatus + priority: 'high' | 'medium' | 'low' + description: string + startDate: string + dueDate: string + createdOn: string + note: string +} diff --git a/src/shared/model/Patient.ts b/src/shared/model/Patient.ts index 51ee1d8356..8bbc19825b 100644 --- a/src/shared/model/Patient.ts +++ b/src/shared/model/Patient.ts @@ -1,5 +1,6 @@ import AbstractDBModel from './AbstractDBModel' import Allergy from './Allergy' +import CareGoal from './CareGoal' import CarePlan from './CarePlan' import ContactInformation from './ContactInformation' import Diagnosis from './Diagnosis' @@ -22,6 +23,7 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati notes?: Note[] index: string carePlans: CarePlan[] + careGoals: CareGoal[] bloodType: string visits: Visit[] } diff --git a/src/shared/model/Permissions.ts b/src/shared/model/Permissions.ts index 2b35f5eb5c..d9532b6b36 100644 --- a/src/shared/model/Permissions.ts +++ b/src/shared/model/Permissions.ts @@ -17,6 +17,8 @@ enum Permissions { ResolveIncident = 'resolve:incident', AddCarePlan = 'write:care_plan', ReadCarePlan = 'read:care_plan', + AddCareGoal = 'write:care_goal', + ReadCareGoal = 'read:care_goal', RequestMedication = 'write:medications', CancelMedication = 'cancel:medication', CompleteMedication = 'complete:medication', diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index aeda304d89..7774b1b666 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -39,6 +39,8 @@ const initialState: UserState = { Permissions.ViewIncidentWidgets, Permissions.AddCarePlan, Permissions.ReadCarePlan, + Permissions.AddCareGoal, + Permissions.ReadCareGoal, Permissions.RequestMedication, Permissions.CompleteMedication, Permissions.CancelMedication, From a004cae27fd3f92e7ef7aabf4a06ef2960159583 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 7 Sep 2020 22:39:43 +0000 Subject: [PATCH 52/86] build(deps-dev): bump husky from 4.2.5 to 4.3.0 Bumps [husky](https://github.com/typicode/husky) from 4.2.5 to 4.3.0. - [Release notes](https://github.com/typicode/husky/releases) - [Changelog](https://github.com/typicode/husky/blob/master/CHANGELOG.md) - [Commits](https://github.com/typicode/husky/compare/v4.2.5...v4.3.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29b8ab6f1d..6b373348a7 100644 --- a/package.json +++ b/package.json @@ -100,7 +100,7 @@ "eslint-plugin-react": "~7.20.0", "eslint-plugin-react-hooks": "~4.1.0", "history": "4.10.1", - "husky": "~4.2.1", + "husky": "~4.3.0", "jest": "24.9.0", "lint-staged": "~10.3.0", "memdown": "~5.1.0", From d4340a3a5efbfb2e464c48bb78325df086dec016 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2020 03:11:44 +0000 Subject: [PATCH 53/86] build(deps): bump @hospitalrun/components from 1.16.1 to 2.0.0 Bumps [@hospitalrun/components](https://github.com/HospitalRun/components) from 1.16.1 to 2.0.0. - [Release notes](https://github.com/HospitalRun/components/releases) - [Changelog](https://github.com/HospitalRun/components/blob/master/CHANGELOG.md) - [Commits](https://github.com/HospitalRun/components/compare/v1.16.1...v2.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 6b373348a7..108b24e0e3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~1.16.0", + "@hospitalrun/components": "~2.0.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/json2csv": "~5.0.1", From 015f0877c48f39124f6ad2def9243460fff88f3f Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Tue, 8 Sep 2020 18:32:54 -0500 Subject: [PATCH 54/86] build(deps): bump @hospitalrun/components from 2.0.0 to 2.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 108b24e0e3..c321306f40 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~2.0.0", + "@hospitalrun/components": "~2.0.1", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/json2csv": "~5.0.1", From dbf2c01de187750550776e2d79966ae6f688d4d1 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Tue, 8 Sep 2020 18:44:26 -0500 Subject: [PATCH 55/86] Revert "build(deps): bump @hospitalrun/components from 2.0.0 to 2.0.1" This reverts commit 015f0877c48f39124f6ad2def9243460fff88f3f. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c321306f40..108b24e0e3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~2.0.1", + "@hospitalrun/components": "~2.0.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/json2csv": "~5.0.1", From f0cca2a3d07d28f5b035b3ea8b4bbaf12cd6bc3c Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Tue, 8 Sep 2020 18:51:37 -0500 Subject: [PATCH 56/86] build(deps): fix version for components --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 108b24e0e3..efc1314625 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "~2.0.0", + "@hospitalrun/components": "2.0.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/json2csv": "~5.0.1", From 144d7f31a32ce21e2bfa1c136c4ff8c620e10a28 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 9 Sep 2020 00:02:02 -0500 Subject: [PATCH 57/86] chore: remove unused redux code and renamed files --- ...ddPatientNote.ts => useAddPatientNote.tsx} | 0 .../{usePatientNote.ts => usePatientNote.tsx} | 0 ...usePatientNotes.ts => usePatientNotes.tsx} | 0 src/patients/patient-slice.ts | 72 ------------------- 4 files changed, 72 deletions(-) rename src/patients/hooks/{useAddPatientNote.ts => useAddPatientNote.tsx} (100%) rename src/patients/hooks/{usePatientNote.ts => usePatientNote.tsx} (100%) rename src/patients/hooks/{usePatientNotes.ts => usePatientNotes.tsx} (100%) diff --git a/src/patients/hooks/useAddPatientNote.ts b/src/patients/hooks/useAddPatientNote.tsx similarity index 100% rename from src/patients/hooks/useAddPatientNote.ts rename to src/patients/hooks/useAddPatientNote.tsx diff --git a/src/patients/hooks/usePatientNote.ts b/src/patients/hooks/usePatientNote.tsx similarity index 100% rename from src/patients/hooks/usePatientNote.ts rename to src/patients/hooks/usePatientNote.tsx diff --git a/src/patients/hooks/usePatientNotes.ts b/src/patients/hooks/usePatientNotes.tsx similarity index 100% rename from src/patients/hooks/usePatientNotes.ts rename to src/patients/hooks/usePatientNotes.tsx diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 7dcf0a82ca..36fcfdd0a4 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -5,7 +5,6 @@ import validator from 'validator' import PatientRepository from '../shared/db/PatientRepository' import Diagnosis from '../shared/model/Diagnosis' -import Note from '../shared/model/Note' import Patient from '../shared/model/Patient' import RelatedPerson from '../shared/model/RelatedPerson' import Visit from '../shared/model/Visit' @@ -22,9 +21,7 @@ interface PatientState { updateError?: Error allergyError?: AddAllergyError diagnosisError?: AddDiagnosisError - noteError?: AddNoteError relatedPersonError?: AddRelatedPersonError - carePlanError?: AddCarePlanError visitError?: AddVisitError } @@ -58,23 +55,6 @@ interface AddDiagnosisError { status?: string } -interface AddNoteError { - message?: string - note?: string -} - -interface AddCarePlanError { - message?: string - title?: string - description?: string - status?: string - intent?: string - startDate?: string - endDate?: string - note?: string - condition?: string -} - interface AddVisitError { message?: string status?: string @@ -90,11 +70,8 @@ const initialState: PatientState = { relatedPersons: [], createError: undefined, updateError: undefined, - allergyError: undefined, diagnosisError: undefined, - noteError: undefined, relatedPersonError: undefined, - carePlanError: undefined, visitError: undefined, } @@ -129,10 +106,6 @@ const patientSlice = createSlice({ state.status = 'error' state.updateError = payload }, - addAllergyError(state, { payload }: PayloadAction) { - state.status = 'error' - state.allergyError = payload - }, addDiagnosisError(state, { payload }: PayloadAction) { state.status = 'error' state.diagnosisError = payload @@ -141,18 +114,6 @@ const patientSlice = createSlice({ state.status = 'error' state.relatedPersonError = payload }, - addNoteError(state, { payload }: PayloadAction) { - state.status = 'error' - state.noteError = payload - }, - addCarePlanError(state, { payload }: PayloadAction) { - state.status = 'error' - state.carePlanError = payload - }, - addVisitError(state, { payload }: PayloadAction) { - state.status = 'error' - state.visitError = payload - }, }, }) @@ -165,12 +126,8 @@ export const { updatePatientStart, updatePatientSuccess, updatePatientError, - addAllergyError, addDiagnosisError, addRelatedPersonError, - addNoteError, - addCarePlanError, - addVisitError, } = patientSlice.actions export const fetchPatient = (id: string): AppThunk => async (dispatch) => { @@ -383,35 +340,6 @@ export const addDiagnosis = ( } } -function validateNote(note: Note) { - const error: AddNoteError = {} - if (!note.text) { - error.message = 'patient.notes.error.noteRequired' - } - - return error -} - -export const addNote = ( - patientId: string, - note: Note, - onSuccess?: (patient: Patient) => void, -): AppThunk => async (dispatch) => { - const newNoteError = validateNote(note) - - if (isEmpty(newNoteError)) { - const patient = await PatientRepository.find(patientId) - const notes = patient.notes || [] - notes.push({ id: uuid(), date: new Date().toISOString(), ...note }) - patient.notes = notes - - await dispatch(updatePatient(patient, onSuccess)) - } else { - newNoteError.message = 'patient.notes.error.unableToAdd' - dispatch(addNoteError(newNoteError)) - } -} - function validateVisit(visit: Visit): AddVisitError { const error: AddVisitError = {} From 521da9fbf7bcfcb806ea171c565ec4d1c95f987e Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Wed, 9 Sep 2020 00:09:25 -0500 Subject: [PATCH 58/86] chore: remove replit --- .replit | 2 -- README.md | 8 ++++---- 2 files changed, 4 insertions(+), 6 deletions(-) delete mode 100644 .replit diff --git a/.replit b/.replit deleted file mode 100644 index e5ab7812bc..0000000000 --- a/.replit +++ /dev/null @@ -1,2 +0,0 @@ -language = "nodejs" -run = "npm start" diff --git a/README.md b/README.md index 42928a8062..dd1ef4e355 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ![Status](https://img.shields.io/badge/Status-developing-brightgree) [![Release](https://img.shields.io/github/release/HospitalRun/hospitalrun-frontend.svg)](https://github.com/HospitalRun/hospitalrun-frontend/releases) [![Version](https://img.shields.io/github/package-json/v/hospitalrun/hospitalrun-frontend)](https://github.com/HospitalRun/hospitalrun-frontend/releases) [![GitHub CI](https://github.com/HospitalRun/frontend/workflows/GitHub%20CI/badge.svg)](https://github.com/HospitalRun/frontend/actions) [![Coverage Status](https://coveralls.io/repos/github/HospitalRun/hospitalrun-frontend/badge.svg?branch=master)](https://coveralls.io/github/HospitalRun/hospitalrun-frontend?branch=master) [![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/HospitalRun/hospitalrun-frontend.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/HospitalRun/hospitalrun-frontend/context:javascript) ![Code scanning](https://github.com/HospitalRun/hospitalrun-frontend/workflows/Code%20scanning/badge.svg?branch=master) [![Documentation Status](https://readthedocs.org/projects/hospitalrun-frontend/badge/?version=latest)](https://hospitalrun-frontend.readthedocs.io) [![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FHospitalRun%2Fhospitalrun-frontend.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2FHospitalRun%2Fhospitalrun-frontend?ref=badge_large) [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) -![dependabot](https://api.dependabot.com/badges/status?host=github&repo=HospitalRun/hospitalrun-frontend) [![Slack](https://hospitalrun-slack.herokuapp.com/badge.svg)](https://hospitalrun-slack.herokuapp.com) [![Run on Repl.it](https://repl.it/badge/github/HospitalRun/hospitalrun-frontend)](https://repl.it/github/HospitalRun/hospitalrun-frontend) +![dependabot](https://api.dependabot.com/badges/status?host=github&repo=HospitalRun/hospitalrun-frontend) [![Slack](https://hospitalrun-slack.herokuapp.com/badge.svg)](https://hospitalrun-slack.herokuapp.com) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/HospitalRun/hospitalrun-frontend) @@ -20,9 +20,9 @@ React frontend for [HospitalRun](http://hospitalrun.io/): free software for deve - How can I deploy 1.0.0-beta? - Where do I report a bug or request a feature? -- How can I contribute? (There are several other ways besides coding) -- What is the project structure? -- What is the application infrastructure? +- How can I contribute? (There are several other ways besides coding) +- What is the project structure? +- What is the application infrastructure? - Who is behind HospitalRun? etc. # Would you like to contribute? If yes... From 2fc4ca4bd8f285f8dd60c1459040f051132e4ea8 Mon Sep 17 00:00:00 2001 From: andreltokdis Date: Wed, 9 Sep 2020 15:09:15 +0700 Subject: [PATCH 59/86] fix: add successfullycompleted resolves #2295 --- src/shared/locales/enUs/translations/labs/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/shared/locales/enUs/translations/labs/index.ts b/src/shared/locales/enUs/translations/labs/index.ts index eafaca1ab4..247988a713 100644 --- a/src/shared/locales/enUs/translations/labs/index.ts +++ b/src/shared/locales/enUs/translations/labs/index.ts @@ -4,6 +4,7 @@ export default { filterTitle: 'Filter by status', search: 'Search labs', successfullyUpdated: 'Successfully updated', + successfullyCompleted: 'Succesfully Completed', status: { requested: 'Requested', completed: 'Completed', From 6156f38f8ea3e8bbf8bb9496f210d8815ee4a78e Mon Sep 17 00:00:00 2001 From: Andrel Karunia S Date: Thu, 10 Sep 2020 06:02:18 +0700 Subject: [PATCH 60/86] fix(care plan): fix view button label in list (#2376) --- src/patients/care-plans/CarePlanTable.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/patients/care-plans/CarePlanTable.tsx b/src/patients/care-plans/CarePlanTable.tsx index a606af73d6..387961709e 100644 --- a/src/patients/care-plans/CarePlanTable.tsx +++ b/src/patients/care-plans/CarePlanTable.tsx @@ -52,7 +52,7 @@ const CarePlanTable = (props: Props) => { actionsHeaderText={t('actions.label')} actions={[ { - label: 'actions.view', + label: t('actions.view'), action: (row) => history.push(`/patients/${patientId}/care-plans/${row.id}`), }, ]} From 40ab00feb2b8584a090e607d98793de56a31fa47 Mon Sep 17 00:00:00 2001 From: andreltokdis Date: Thu, 10 Sep 2020 08:59:50 +0700 Subject: [PATCH 61/86] fix: fix typo successfully alphabet text --- src/shared/locales/enUs/translations/labs/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/locales/enUs/translations/labs/index.ts b/src/shared/locales/enUs/translations/labs/index.ts index 247988a713..8105f074e5 100644 --- a/src/shared/locales/enUs/translations/labs/index.ts +++ b/src/shared/locales/enUs/translations/labs/index.ts @@ -4,7 +4,7 @@ export default { filterTitle: 'Filter by status', search: 'Search labs', successfullyUpdated: 'Successfully updated', - successfullyCompleted: 'Succesfully Completed', + successfullyCompleted: 'Successfully completed', status: { requested: 'Requested', completed: 'Completed', From 8c60adb08e5b3ff4f1fc7cd38ff73a74fff76417 Mon Sep 17 00:00:00 2001 From: andreltokdis Date: Thu, 10 Sep 2020 14:10:05 +0700 Subject: [PATCH 62/86] fix: update text for oncomplete lab --- src/labs/ViewLab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index bec53768c6..cc80fd4da6 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -82,7 +82,7 @@ const ViewLab = () => { Toast( 'success', t('states.success'), - `${t('labs.successfullyCompleted')} ${complete.type} ${patient?.fullName} `, + `${t('labs.successfullyCompleted')} ${complete.type} for ${patient?.fullName} `, ) } From fe698cc5b1c3c95842c5a9bd254cde2a35c968e9 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Fri, 11 Sep 2020 21:58:01 -0500 Subject: [PATCH 63/86] refactor(medications): refactor medications search to use react query --- src/__tests__/HospitalRun.test.tsx | 5 +- .../medications/Medications.test.tsx | 2 +- .../medications/ViewMedications.test.tsx | 190 ------------------ .../hooks/useMedicationSearch.test.tsx | 43 ++++ .../medications/medications-slice.test.ts | 144 ------------- .../search/MedicationRequestSearch.test.tsx | 87 ++++++++ .../search/MedicationRequestTable.test.tsx | 91 +++++++++ .../search/ViewMedications.test.tsx | 128 ++++++++++++ src/medications/Medications.tsx | 2 +- src/medications/ViewMedications.tsx | 133 ------------ src/medications/hooks/useMedicationSearch.tsx | 29 +++ src/medications/medications-slice.ts | 75 ------- .../models/MedicationSearchRequest.ts | 6 + src/medications/models/MedicationStatus.ts | 10 + .../search/MedicationRequestSearch.tsx | 71 +++++++ .../search/MedicationRequestTable.tsx | 52 +++++ src/medications/search/ViewMedications.tsx | 71 +++++++ src/shared/db/MedicationRepository.ts | 19 +- src/shared/store/index.ts | 2 - 19 files changed, 595 insertions(+), 565 deletions(-) delete mode 100644 src/__tests__/medications/ViewMedications.test.tsx create mode 100644 src/__tests__/medications/hooks/useMedicationSearch.test.tsx delete mode 100644 src/__tests__/medications/medications-slice.test.ts create mode 100644 src/__tests__/medications/search/MedicationRequestSearch.test.tsx create mode 100644 src/__tests__/medications/search/MedicationRequestTable.test.tsx create mode 100644 src/__tests__/medications/search/ViewMedications.test.tsx delete mode 100644 src/medications/ViewMedications.tsx create mode 100644 src/medications/hooks/useMedicationSearch.tsx delete mode 100644 src/medications/medications-slice.ts create mode 100644 src/medications/models/MedicationSearchRequest.ts create mode 100644 src/medications/models/MedicationStatus.ts create mode 100644 src/medications/search/MedicationRequestSearch.tsx create mode 100644 src/medications/search/MedicationRequestTable.tsx create mode 100644 src/medications/search/ViewMedications.tsx diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 82616b535b..28bb9bf9e5 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -12,7 +12,7 @@ import HospitalRun from '../HospitalRun' import ViewImagings from '../imagings/search/ViewImagings' import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' -import ViewMedications from '../medications/ViewMedications' +import ViewMedications from '../medications/search/ViewMedications' import { addBreadcrumbs } from '../page-header/breadcrumbs/breadcrumbs-slice' import Appointments from '../scheduling/appointments/Appointments' import Settings from '../settings/Settings' @@ -129,11 +129,10 @@ describe('HospitalRun', () => { describe('/medications', () => { it('should render the Medications component when /medications is accessed', async () => { - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) const store = mockStore({ title: 'test', user: { user: { id: '123' }, permissions: [Permissions.ViewMedications] }, - medications: { medications: [] }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) diff --git a/src/__tests__/medications/Medications.test.tsx b/src/__tests__/medications/Medications.test.tsx index eb52b6ce2b..6955d90f89 100644 --- a/src/__tests__/medications/Medications.test.tsx +++ b/src/__tests__/medications/Medications.test.tsx @@ -21,7 +21,7 @@ const mockStore = createMockStore([thunk]) describe('Medications', () => { const setup = (route: string, permissions: Array) => { jest.resetAllMocks() - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) jest .spyOn(MedicationRepository, 'find') .mockResolvedValue({ id: '1234', requestedOn: new Date().toISOString() } as Medication) diff --git a/src/__tests__/medications/ViewMedications.test.tsx b/src/__tests__/medications/ViewMedications.test.tsx deleted file mode 100644 index 96c9c7d524..0000000000 --- a/src/__tests__/medications/ViewMedications.test.tsx +++ /dev/null @@ -1,190 +0,0 @@ -import { TextInput, Select, Table } from '@hospitalrun/components' -import { act } from '@testing-library/react' -import { mount } from 'enzyme' -import { createMemoryHistory } from 'history' -import React from 'react' -import { Provider } from 'react-redux' -import { Router } from 'react-router-dom' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' - -import * as medicationsSlice from '../../medications/medications-slice' -import ViewMedications from '../../medications/ViewMedications' -import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' -import MedicationRepository from '../../shared/db/MedicationRepository' -import Medication from '../../shared/model/Medication' -import Permissions from '../../shared/model/Permissions' -import { RootState } from '../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('View Medications', () => { - const setup = async (medication: Medication, permissions: Permissions[]) => { - let wrapper: any - const expectedMedication = ({ - id: '1234', - medication: 'medication', - patient: 'patientId', - status: 'draft', - intent: 'order', - priority: 'routine', - quantity: { value: 1, unit: 'unit' }, - requestedOn: '2020-03-30T04:43:20.102Z', - } as unknown) as Medication - const history = createMemoryHistory() - const store = mockStore({ - title: '', - user: { permissions }, - medications: { medications: [{ ...expectedMedication, ...medication }] }, - } as any) - const titleSpy = jest.spyOn(titleUtil, 'default') - const setButtonToolBarSpy = jest.fn() - const searchMedicationsSpy = jest.spyOn(medicationsSlice, 'searchMedications') - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) - - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() - return [ - wrapper, - titleSpy, - setButtonToolBarSpy, - { ...expectedMedication, ...medication }, - history, - searchMedicationsSpy, - ] - } - - describe('title', () => { - it('should have the title', async () => { - const permissions: never[] = [] - const [, titleSpy] = await setup({} as Medication, permissions) - expect(titleSpy).toHaveBeenCalledWith('medications.label') - }) - }) - - describe('button bar', () => { - it('should display button to add new medication request', async () => { - const permissions = [Permissions.ViewMedications, Permissions.RequestMedication] - const [, , setButtonToolBarSpy] = await setup({} as Medication, permissions) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect((actualButtons[0] as any).props.children).toEqual('medications.requests.new') - }) - - it('should not display button to add new medication request if the user does not have permissions', async () => { - const permissions: never[] = [] - const [, , setButtonToolBarSpy] = await setup({} as Medication, permissions) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect(actualButtons).toEqual([]) - }) - }) - - describe('table', () => { - it('should render a table with data', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , expectedMedication] = await setup({} as Medication, permissions) - const table = wrapper.find(Table) - const columns = table.prop('columns') - const actions = table.prop('actions') as any - expect(columns[0]).toEqual( - expect.objectContaining({ label: 'medications.medication.medication', key: 'medication' }), - ) - expect(columns[1]).toEqual( - expect.objectContaining({ label: 'medications.medication.priority', key: 'priority' }), - ) - expect(columns[2]).toEqual( - expect.objectContaining({ label: 'medications.medication.intent', key: 'intent' }), - ) - expect(columns[3]).toEqual( - expect.objectContaining({ - label: 'medications.medication.requestedOn', - key: 'requestedOn', - }), - ) - expect(columns[4]).toEqual( - expect.objectContaining({ label: 'medications.medication.status', key: 'status' }), - ) - - expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) - expect(table.prop('actionsHeaderText')).toEqual('actions.label') - expect(table.prop('data')).toEqual([expectedMedication]) - }) - - it('should navigate to the medication when the view button is clicked', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , expectedMedication, history] = await setup({} as Medication, permissions) - const tr = wrapper.find('tr').at(1) - - act(() => { - const onClick = tr.find('button').prop('onClick') as any - onClick({ stopPropagation: jest.fn() }) - }) - expect(history.location.pathname).toEqual(`/medications/${expectedMedication.id}`) - }) - }) - - describe('dropdown', () => { - it('should search for medications when dropdown changes', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , , , searchMedicationsSpy] = await setup({} as Medication, permissions) - - searchMedicationsSpy.mockClear() - - act(() => { - const onChange = wrapper.find(Select).prop('onChange') as any - onChange({ - target: { - value: 'draft', - }, - preventDefault: jest.fn(), - }) - }) - - wrapper.update() - expect(searchMedicationsSpy).toHaveBeenCalledTimes(1) - }) - }) - - describe('search functionality', () => { - beforeEach(() => jest.useFakeTimers()) - - afterEach(() => jest.useRealTimers()) - - it('should search for medications after the search text has not changed for 500 milliseconds', async () => { - const permissions = [Permissions.ViewMedications] - const [wrapper, , , , , searchMedicationsSpy] = await setup({} as Medication, permissions) - - searchMedicationsSpy.mockClear() - const expectedSearchText = 'search text' - - act(() => { - const onClick = wrapper.find(TextInput).at(0).prop('onChange') as any - onClick({ - target: { - value: expectedSearchText, - }, - preventDefault: jest.fn(), - }) - }) - - act(() => { - jest.advanceTimersByTime(500) - }) - - wrapper.update() - - expect(searchMedicationsSpy).toHaveBeenCalledTimes(1) - }) - }) -}) diff --git a/src/__tests__/medications/hooks/useMedicationSearch.test.tsx b/src/__tests__/medications/hooks/useMedicationSearch.test.tsx new file mode 100644 index 0000000000..570119e022 --- /dev/null +++ b/src/__tests__/medications/hooks/useMedicationSearch.test.tsx @@ -0,0 +1,43 @@ +import { act, renderHook } from '@testing-library/react-hooks' + +import useMedicationSearch from '../../../medications/hooks/useMedicationSearch' +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import SortRequest from '../../../shared/db/SortRequest' +import Medication from '../../../shared/model/Medication' +import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +describe('useMedicationSearch', () => { + it('it should search medication requests', async () => { + const expectedSearchRequest: MedicationSearchRequest = { + status: 'all', + text: 'some search request', + } + const expectedMedicationRequests = [{ id: 'some id' }] as Medication[] + jest.spyOn(MedicationRepository, 'search').mockResolvedValue(expectedMedicationRequests) + + let actualData: any + await act(async () => { + const renderHookResult = renderHook(() => useMedicationSearch(expectedSearchRequest)) + const { result } = renderHookResult + await waitUntilQueryIsSuccessful(renderHookResult) + actualData = result.current.data + }) + + expect(MedicationRepository.search).toHaveBeenCalledTimes(1) + expect(MedicationRepository.search).toBeCalledWith({ + ...expectedSearchRequest, + defaultSortRequest, + }) + expect(actualData).toEqual(expectedMedicationRequests) + }) +}) diff --git a/src/__tests__/medications/medications-slice.test.ts b/src/__tests__/medications/medications-slice.test.ts deleted file mode 100644 index 9e7359c673..0000000000 --- a/src/__tests__/medications/medications-slice.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { AnyAction } from 'redux' -import { mocked } from 'ts-jest/utils' - -import medications, { - fetchMedicationsStart, - fetchMedicationsSuccess, - searchMedications, -} from '../../medications/medications-slice' -import MedicationRepository from '../../shared/db/MedicationRepository' -import SortRequest from '../../shared/db/SortRequest' -import Medication from '../../shared/model/Medication' - -interface SearchContainer { - text: string - status: 'draft' | 'active' | 'completed' | 'canceled' | 'all' - defaultSortRequest: SortRequest -} - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const expectedSearchObject: SearchContainer = { - text: 'search string', - status: 'all', - defaultSortRequest, -} - -describe('medications slice', () => { - const setup = (medicationSpyOn: string) => { - const dispatch = jest.fn() - const getState = jest.fn() - jest.spyOn(MedicationRepository, medicationSpyOn as any) - return [dispatch, getState] - } - - beforeEach(() => { - jest.resetAllMocks() - }) - - describe('medications reducer', () => { - it('should create the proper initial state with empty medications array', () => { - const medicationsStore = medications(undefined, {} as AnyAction) - expect(medicationsStore.isLoading).toBeFalsy() - expect(medicationsStore.medications).toHaveLength(0) - expect(medicationsStore.statusFilter).toEqual('all') - }) - - it('it should handle the FETCH_MEDICATIONS_SUCCESS action', () => { - const expectedMedications = [{ id: '1234' }] - const medicationsStore = medications(undefined, { - type: fetchMedicationsSuccess.type, - payload: expectedMedications, - }) - - expect(medicationsStore.isLoading).toBeFalsy() - expect(medicationsStore.medications).toEqual(expectedMedications) - }) - }) - - describe('searchMedications', () => { - it('should dispatch the FETCH_MEDICATIONS_START action', async () => { - const [dispatch, getState] = setup('search') - - await searchMedications('search string', 'all')(dispatch, getState, null) - - expect(dispatch).toHaveBeenCalledWith({ type: fetchMedicationsStart.type }) - }) - - it('should call the MedicationRepository search method with the correct search criteria', async () => { - const [dispatch, getState] = setup('search') - jest.spyOn(MedicationRepository, 'search') - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(MedicationRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - - it('should call the MedicationRepository findAll method if there is no string text and status is set to all', async () => { - const [dispatch, getState] = setup('findAll') - - await searchMedications('', expectedSearchObject.status)(dispatch, getState, null) - - expect(MedicationRepository.findAll).toHaveBeenCalledTimes(1) - }) - - it('should dispatch the FETCH_MEDICATIONS_SUCCESS action', async () => { - const [dispatch, getState] = setup('findAll') - - const expectedMedications = [ - { - medication: 'text', - }, - ] as Medication[] - - const mockedMedicationRepository = mocked(MedicationRepository, true) - mockedMedicationRepository.search.mockResolvedValue(expectedMedications) - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(dispatch).toHaveBeenLastCalledWith({ - type: fetchMedicationsSuccess.type, - payload: expectedMedications, - }) - }) - }) - - describe('sort Request', () => { - it('should have called findAll with sort request in searchMedications method', async () => { - const [dispatch, getState] = setup('findAll') - - await searchMedications('', expectedSearchObject.status)(dispatch, getState, null) - - expect(MedicationRepository.findAll).toHaveBeenCalledWith( - expectedSearchObject.defaultSortRequest, - ) - }) - - it('should include sorts in the search criteria', async () => { - const [dispatch, getState] = setup('search') - - await searchMedications(expectedSearchObject.text, expectedSearchObject.status)( - dispatch, - getState, - null, - ) - - expect(MedicationRepository.search).toHaveBeenCalledWith(expectedSearchObject) - }) - }) -}) diff --git a/src/__tests__/medications/search/MedicationRequestSearch.test.tsx b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx new file mode 100644 index 0000000000..2aa71ee9f3 --- /dev/null +++ b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx @@ -0,0 +1,87 @@ +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestSearch from '../../../medications/search/MedicationRequestSearch' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' +import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' + +describe('Medication Request Search', () => { + const setup = (givenSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' }) => { + const onChangeSpy = jest.fn() + + const wrapper = mount( + , + ) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, onChangeSpy } + } + + it('should render a select component with the default value', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const { wrapper } = setup(expectedSearchRequest) + + const select = wrapper.find(SelectWithLabelFormGroup) + expect(select.prop('label')).toEqual('medications.filterTitle') + expect(select.prop('options')).toEqual([ + { label: 'medications.filter.all', value: 'all' }, + { label: 'medications.status.draft', value: 'draft' }, + { label: 'medications.status.active', value: 'active' }, + { label: 'medications.status.onHold', value: 'on hold' }, + { label: 'medications.status.completed', value: 'completed' }, + { label: 'medications.status.enteredInError', value: 'entered in error' }, + { label: 'medications.status.canceled', value: 'canceled' }, + { label: 'medications.status.unknown', value: 'unknown' }, + ]) + expect(select.prop('defaultSelected')).toEqual([ + { + label: 'medications.status.draft', + value: 'draft', + }, + ]) + expect(select.prop('isEditable')).toBeTruthy() + }) + + it('should update the search request when the filter updates', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const expectedNewValue = 'canceled' + const { wrapper, onChangeSpy } = setup(expectedSearchRequest) + + act(() => { + const select = wrapper.find(SelectWithLabelFormGroup) + const onChange = select.prop('onChange') as any + onChange([expectedNewValue]) + }) + + expect(onChangeSpy).toHaveBeenCalledTimes(1) + expect(onChangeSpy).toHaveBeenCalledWith({ ...expectedSearchRequest, status: expectedNewValue }) + }) + + it('should render a text input with the default value', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const { wrapper } = setup(expectedSearchRequest) + + const textInput = wrapper.find(TextInputWithLabelFormGroup) + expect(textInput.prop('label')).toEqual('medications.search') + expect(textInput.prop('placeholder')).toEqual('medications.search') + expect(textInput.prop('value')).toEqual(expectedSearchRequest.text) + expect(textInput.prop('isEditable')).toBeTruthy() + }) + + it('should update the search request when the text input is updated', () => { + const expectedSearchRequest: MedicationSearchRequest = { text: '', status: 'draft' } + const expectedNewValue = 'someNewValue' + const { wrapper, onChangeSpy } = setup(expectedSearchRequest) + + act(() => { + const textInput = wrapper.find(TextInputWithLabelFormGroup) + const onChange = textInput.prop('onChange') as any + onChange({ target: { value: expectedNewValue } }) + }) + + expect(onChangeSpy).toHaveBeenCalledTimes(1) + expect(onChangeSpy).toHaveBeenCalledWith({ ...expectedSearchRequest, text: expectedNewValue }) + }) +}) diff --git a/src/__tests__/medications/search/MedicationRequestTable.test.tsx b/src/__tests__/medications/search/MedicationRequestTable.test.tsx new file mode 100644 index 0000000000..cdadaad467 --- /dev/null +++ b/src/__tests__/medications/search/MedicationRequestTable.test.tsx @@ -0,0 +1,91 @@ +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 { Router } from 'react-router-dom' + +import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestTable from '../../../medications/search/MedicationRequestTable' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import Medication from '../../../shared/model/Medication' + +describe('Medication Request Table', () => { + const setup = async ( + givenSearchRequest: MedicationSearchRequest = { text: '', status: 'all' }, + givenMedications: Medication[] = [], + ) => { + jest.resetAllMocks() + jest.spyOn(MedicationRepository, 'search').mockResolvedValue(givenMedications) + const history = createMemoryHistory() + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + , + ) + }) + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, history } + } + + it('should render a table with the correct columns', async () => { + const { wrapper } = await setup() + + const table = wrapper.find(Table) + const columns = table.prop('columns') + const actions = table.prop('actions') as any + expect(columns[0]).toEqual( + expect.objectContaining({ label: 'medications.medication.medication', key: 'medication' }), + ) + expect(columns[1]).toEqual( + expect.objectContaining({ label: 'medications.medication.priority', key: 'priority' }), + ) + expect(columns[2]).toEqual( + expect.objectContaining({ label: 'medications.medication.intent', key: 'intent' }), + ) + expect(columns[3]).toEqual( + expect.objectContaining({ + label: 'medications.medication.requestedOn', + key: 'requestedOn', + }), + ) + expect(columns[4]).toEqual( + expect.objectContaining({ label: 'medications.medication.status', key: 'status' }), + ) + + expect(actions[0]).toEqual(expect.objectContaining({ label: 'actions.view' })) + expect(table.prop('actionsHeaderText')).toEqual('actions.label') + }) + + it('should fetch medications and display it', async () => { + const expectedSearchRequest: MedicationSearchRequest = { text: 'someText', status: 'draft' } + const expectedMedicationRequests: Medication[] = [{ id: 'someId' } as Medication] + + const { wrapper } = await setup(expectedSearchRequest, expectedMedicationRequests) + + const table = wrapper.find(Table) + expect(MedicationRepository.search).toHaveBeenCalledWith( + expect.objectContaining(expectedSearchRequest), + ) + expect(table.prop('data')).toEqual(expectedMedicationRequests) + }) + + it('should navigate to the medication when the view button is clicked', async () => { + const expectedSearchRequest: MedicationSearchRequest = { text: 'someText', status: 'draft' } + const expectedMedicationRequests: Medication[] = [{ id: 'someId' } as Medication] + + const { wrapper, history } = await setup(expectedSearchRequest, expectedMedicationRequests) + + const tr = wrapper.find('tr').at(1) + act(() => { + const onClick = tr.find('button').prop('onClick') as any + onClick({ stopPropagation: jest.fn() }) + }) + + expect(history.location.pathname).toEqual(`/medications/${expectedMedicationRequests[0].id}`) + }) +}) diff --git a/src/__tests__/medications/search/ViewMedications.test.tsx b/src/__tests__/medications/search/ViewMedications.test.tsx new file mode 100644 index 0000000000..c113ad3043 --- /dev/null +++ b/src/__tests__/medications/search/ViewMedications.test.tsx @@ -0,0 +1,128 @@ +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 MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' +import MedicationRequestSearch from '../../../medications/search/MedicationRequestSearch' +import MedicationRequestTable from '../../../medications/search/MedicationRequestTable' +import ViewMedications from '../../../medications/search/ViewMedications' +import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' +import * as titleUtil from '../../../page-header/title/useTitle' +import MedicationRepository from '../../../shared/db/MedicationRepository' +import Medication from '../../../shared/model/Medication' +import Permissions from '../../../shared/model/Permissions' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('View Medications', () => { + const setup = async (medication: Medication, permissions: Permissions[] = []) => { + let wrapper: any + const expectedMedication = ({ + id: '1234', + medication: 'medication', + patient: 'patientId', + status: 'draft', + intent: 'order', + priority: 'routine', + quantity: { value: 1, unit: 'unit' }, + requestedOn: '2020-03-30T04:43:20.102Z', + } as unknown) as Medication + const history = createMemoryHistory() + const store = mockStore({ + title: '', + user: { permissions }, + medications: { medications: [{ ...expectedMedication, ...medication }] }, + } as any) + const titleSpy = jest.spyOn(titleUtil, 'default') + const setButtonToolBarSpy = jest.fn() + jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + return { + wrapper: wrapper as ReactWrapper, + history, + titleSpy, + setButtonToolBarSpy, + } + } + + describe('title', () => { + it('should have the title', async () => { + const { titleSpy } = await setup({} as Medication) + + expect(titleSpy).toHaveBeenCalledWith('medications.label') + }) + }) + + describe('button bar', () => { + it('should display button to add new medication request', async () => { + const permissions = [Permissions.ViewMedications, Permissions.RequestMedication] + const { setButtonToolBarSpy } = await setup({} as Medication, permissions) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('medications.requests.new') + }) + + it('should not display button to add new medication request if the user does not have permissions', async () => { + const { setButtonToolBarSpy } = await setup({} as Medication) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect(actualButtons).toEqual([]) + }) + }) + + describe('table', () => { + it('should render a table with data with the default search', async () => { + const { wrapper } = await setup({} as Medication, [Permissions.ViewMedications]) + + const table = wrapper.find(MedicationRequestTable) + expect(table).toHaveLength(1) + expect(table.prop('searchRequest')).toEqual({ text: '', status: 'all' }) + }) + }) + + describe('search', () => { + it('should render a medication request search component', async () => { + const { wrapper } = await setup({} as Medication) + + const search = wrapper.find(MedicationRequestSearch) + expect(search).toHaveLength(1) + expect(search.prop('searchRequest')).toEqual({ text: '', status: 'all' }) + }) + + it('should update the table when the search changes', async () => { + const expectedSearchRequest: MedicationSearchRequest = { + text: 'someNewText', + status: 'draft', + } + const { wrapper } = await setup({} as Medication) + + await act(async () => { + const search = wrapper.find(MedicationRequestSearch) + const onChange = search.prop('onChange') + await onChange(expectedSearchRequest) + }) + wrapper.update() + + const table = wrapper.find(MedicationRequestTable) + expect(table.prop('searchRequest')).toEqual(expectedSearchRequest) + }) + }) +}) diff --git a/src/medications/Medications.tsx b/src/medications/Medications.tsx index c2f7ba5fcb..aa8835d040 100644 --- a/src/medications/Medications.tsx +++ b/src/medications/Medications.tsx @@ -7,8 +7,8 @@ import PrivateRoute from '../shared/components/PrivateRoute' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' import NewMedicationRequest from './requests/NewMedicationRequest' +import MedicationRequests from './search/ViewMedications' import ViewMedication from './ViewMedication' -import MedicationRequests from './ViewMedications' const Medications = () => { const { permissions } = useSelector((state: RootState) => state.user) diff --git a/src/medications/ViewMedications.tsx b/src/medications/ViewMedications.tsx deleted file mode 100644 index 9623938066..0000000000 --- a/src/medications/ViewMedications.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { Button, Table } from '@hospitalrun/components' -import format from 'date-fns/format' -import React, { useState, useEffect, useCallback } from 'react' -import { useSelector, useDispatch } from 'react-redux' -import { useHistory } from 'react-router-dom' - -import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../page-header/title/useTitle' -import SelectWithLabelFormGroup, { - Option, -} from '../shared/components/input/SelectWithLableFormGroup' -import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' -import useDebounce from '../shared/hooks/useDebounce' -import useTranslator from '../shared/hooks/useTranslator' -import Medication from '../shared/model/Medication' -import Permissions from '../shared/model/Permissions' -import { RootState } from '../shared/store' -import { searchMedications } from './medications-slice' - -type MedicationFilter = 'draft' | 'completed' | 'canceled' | 'all' - -const ViewMedications = () => { - const { t } = useTranslator() - const history = useHistory() - const setButtons = useButtonToolbarSetter() - useTitle(t('medications.label')) - - const { permissions } = useSelector((state: RootState) => state.user) - const dispatch = useDispatch() - const { medications } = useSelector((state: RootState) => state.medications) - const [searchFilter, setSearchFilter] = useState('all') - const [searchText, setSearchText] = useState('') - const debouncedSearchText = useDebounce(searchText, 500) - - const getButtons = useCallback(() => { - const buttons: React.ReactNode[] = [] - - if (permissions.includes(Permissions.RequestMedication)) { - buttons.push( - , - ) - } - - return buttons - }, [permissions, history, t]) - - useEffect(() => { - dispatch(searchMedications(debouncedSearchText, searchFilter)) - }, [dispatch, debouncedSearchText, searchFilter]) - - useEffect(() => { - setButtons(getButtons()) - return () => { - setButtons([]) - } - }, [dispatch, getButtons, setButtons]) - - const onViewClick = (medication: Medication) => { - history.push(`/medications/${medication.id}`) - } - - const onSearchBoxChange = (event: React.ChangeEvent) => { - setSearchText(event.target.value) - } - - const filterOptions: Option[] = [ - { label: t('medications.filter.all'), value: 'all' }, - { label: t('medications.status.draft'), value: 'draft' }, - { label: t('medications.status.active'), value: 'active' }, - { label: t('medications.status.onHold'), value: 'on hold' }, - { label: t('medications.status.completed'), value: 'completed' }, - { label: t('medications.status.enteredInError'), value: 'entered in error' }, - { label: t('medications.status.canceled'), value: 'canceled' }, - { label: t('medications.status.unknown'), value: 'unknown' }, - ] - - return ( - <> -
-
- value === searchFilter)} - onChange={(values) => setSearchFilter(values[0] as MedicationFilter)} - isEditable - /> -
-
- -
-
-
-
row.id} - columns={[ - { label: t('medications.medication.medication'), key: 'medication' }, - { label: t('medications.medication.priority'), key: 'priority' }, - { label: t('medications.medication.intent'), key: 'intent' }, - { - label: t('medications.medication.requestedOn'), - key: 'requestedOn', - formatter: (row) => - row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', - }, - { label: t('medications.medication.status'), key: 'status' }, - ]} - data={medications} - actionsHeaderText={t('actions.label')} - actions={[{ label: t('actions.view'), action: (row) => onViewClick(row as Medication) }]} - /> - - - ) -} - -export default ViewMedications diff --git a/src/medications/hooks/useMedicationSearch.tsx b/src/medications/hooks/useMedicationSearch.tsx new file mode 100644 index 0000000000..eb378d3460 --- /dev/null +++ b/src/medications/hooks/useMedicationSearch.tsx @@ -0,0 +1,29 @@ +import { QueryKey, useQuery } from 'react-query' + +import MedicationRepository from '../../shared/db/MedicationRepository' +import SortRequest from '../../shared/db/SortRequest' +import Medication from '../../shared/model/Medication' +import MedicationSearchRequest from '../models/MedicationSearchRequest' + +const defaultSortRequest: SortRequest = { + sorts: [ + { + field: 'requestedOn', + direction: 'desc', + }, + ], +} + +function searchMedicationRequests( + _: QueryKey, + searchRequest: MedicationSearchRequest, +): Promise { + return MedicationRepository.search({ + ...searchRequest, + defaultSortRequest, + }) +} + +export default function useMedicationSearch(searchRequest: MedicationSearchRequest) { + return useQuery(['medication-requests', searchRequest], searchMedicationRequests) +} diff --git a/src/medications/medications-slice.ts b/src/medications/medications-slice.ts deleted file mode 100644 index 40a2fba226..0000000000 --- a/src/medications/medications-slice.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import MedicationRepository from '../shared/db/MedicationRepository' -import SortRequest from '../shared/db/SortRequest' -import Medication from '../shared/model/Medication' -import { AppThunk } from '../shared/store' - -interface MedicationsState { - isLoading: boolean - medications: Medication[] - statusFilter: status -} - -type status = - | 'draft' - | 'active' - | 'on hold' - | 'canceled' - | 'completed' - | 'entered in error' - | 'stopped' - | 'unknown' - | 'all' - -const defaultSortRequest: SortRequest = { - sorts: [ - { - field: 'requestedOn', - direction: 'desc', - }, - ], -} - -const initialState: MedicationsState = { - isLoading: false, - medications: [], - statusFilter: 'all', -} - -const startLoading = (state: MedicationsState) => { - state.isLoading = true -} - -const medicationsSlice = createSlice({ - name: 'medications', - initialState, - reducers: { - fetchMedicationsStart: startLoading, - fetchMedicationsSuccess(state, { payload }: PayloadAction) { - state.isLoading = false - state.medications = payload - }, - }, -}) -export const { fetchMedicationsStart, fetchMedicationsSuccess } = medicationsSlice.actions - -export const searchMedications = (text: string, status: status): AppThunk => async (dispatch) => { - dispatch(fetchMedicationsStart()) - - let medications - - if (text.trim() === '' && status === initialState.statusFilter) { - medications = await MedicationRepository.findAll(defaultSortRequest) - } else { - medications = await MedicationRepository.search({ - text, - status, - defaultSortRequest, - }) - } - - dispatch(fetchMedicationsSuccess(medications)) -} - -export default medicationsSlice.reducer diff --git a/src/medications/models/MedicationSearchRequest.ts b/src/medications/models/MedicationSearchRequest.ts new file mode 100644 index 0000000000..9965736b7c --- /dev/null +++ b/src/medications/models/MedicationSearchRequest.ts @@ -0,0 +1,6 @@ +import { MedicationStatus } from './MedicationStatus' + +export default interface MedicationSearchRequest { + text: string + status: MedicationStatus +} diff --git a/src/medications/models/MedicationStatus.ts b/src/medications/models/MedicationStatus.ts new file mode 100644 index 0000000000..1c3afec3a0 --- /dev/null +++ b/src/medications/models/MedicationStatus.ts @@ -0,0 +1,10 @@ +export type MedicationStatus = + | 'draft' + | 'active' + | 'on hold' + | 'canceled' + | 'completed' + | 'entered in error' + | 'stopped' + | 'unknown' + | 'all' diff --git a/src/medications/search/MedicationRequestSearch.tsx b/src/medications/search/MedicationRequestSearch.tsx new file mode 100644 index 0000000000..84c93cbc57 --- /dev/null +++ b/src/medications/search/MedicationRequestSearch.tsx @@ -0,0 +1,71 @@ +import React, { ChangeEvent } from 'react' + +import SelectWithLabelFormGroup, { + Option, +} from '../../shared/components/input/SelectWithLableFormGroup' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import useTranslator from '../../shared/hooks/useTranslator' +import MedicationSearchRequest from '../models/MedicationSearchRequest' +import { MedicationStatus } from '../models/MedicationStatus' + +interface Props { + searchRequest: MedicationSearchRequest + onChange: (newSearchRequest: MedicationSearchRequest) => void +} + +const MedicationRequestSearch = (props: Props) => { + const { searchRequest, onChange } = props + const { t } = useTranslator() + const filterOptions: Option[] = [ + { label: t('medications.filter.all'), value: 'all' }, + { label: t('medications.status.draft'), value: 'draft' }, + { label: t('medications.status.active'), value: 'active' }, + { label: t('medications.status.onHold'), value: 'on hold' }, + { label: t('medications.status.completed'), value: 'completed' }, + { label: t('medications.status.enteredInError'), value: 'entered in error' }, + { label: t('medications.status.canceled'), value: 'canceled' }, + { label: t('medications.status.unknown'), value: 'unknown' }, + ] + + const onSearchQueryChange = (event: ChangeEvent) => { + const query = event.target.value + onChange({ + ...searchRequest, + text: query, + }) + } + + const onFilterChange = (filter: MedicationStatus) => { + onChange({ + ...searchRequest, + status: filter, + }) + } + + return ( +
+
+ value === searchRequest.status)} + onChange={(values) => onFilterChange(values[0] as MedicationStatus)} + isEditable + /> +
+
+ +
+
+ ) +} + +export default MedicationRequestSearch diff --git a/src/medications/search/MedicationRequestTable.tsx b/src/medications/search/MedicationRequestTable.tsx new file mode 100644 index 0000000000..f684b78a61 --- /dev/null +++ b/src/medications/search/MedicationRequestTable.tsx @@ -0,0 +1,52 @@ +import { Table } from '@hospitalrun/components' +import format from 'date-fns/format' +import React from 'react' +import { useHistory } from 'react-router' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Medication from '../../shared/model/Medication' +import useMedicationRequestSearch from '../hooks/useMedicationSearch' +import MedicationSearchRequest from '../models/MedicationSearchRequest' + +interface Props { + searchRequest: MedicationSearchRequest +} + +const MedicationRequestTable = (props: Props) => { + const { searchRequest } = props + const { t } = useTranslator() + const history = useHistory() + const { data, status } = useMedicationRequestSearch(searchRequest) + + if (data === undefined || status === 'loading') { + return + } + + const onViewClick = (medication: Medication) => { + history.push(`/medications/${medication.id}`) + } + + return ( +
row.id} + columns={[ + { label: t('medications.medication.medication'), key: 'medication' }, + { label: t('medications.medication.priority'), key: 'priority' }, + { label: t('medications.medication.intent'), key: 'intent' }, + { + label: t('medications.medication.requestedOn'), + key: 'requestedOn', + formatter: (row) => + row.requestedOn ? format(new Date(row.requestedOn), 'yyyy-MM-dd hh:mm a') : '', + }, + { label: t('medications.medication.status'), key: 'status' }, + ]} + data={data} + actionsHeaderText={t('actions.label')} + actions={[{ label: t('actions.view'), action: (row) => onViewClick(row as Medication) }]} + /> + ) +} + +export default MedicationRequestTable diff --git a/src/medications/search/ViewMedications.tsx b/src/medications/search/ViewMedications.tsx new file mode 100644 index 0000000000..bcf85b96bd --- /dev/null +++ b/src/medications/search/ViewMedications.tsx @@ -0,0 +1,71 @@ +import { Button, Column, Container, Row } from '@hospitalrun/components' +import React, { useEffect, useCallback, useState } from 'react' +import { useSelector } from 'react-redux' +import { useHistory } from 'react-router-dom' + +import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' +import useTitle from '../../page-header/title/useTitle' +import useTranslator from '../../shared/hooks/useTranslator' +import Permissions from '../../shared/model/Permissions' +import { RootState } from '../../shared/store' +import MedicationSearchRequest from '../models/MedicationSearchRequest' +import MedicationRequestSearch from './MedicationRequestSearch' +import MedicationRequestTable from './MedicationRequestTable' + +const ViewMedications = () => { + const { t } = useTranslator() + const history = useHistory() + const setButtons = useButtonToolbarSetter() + useTitle(t('medications.label')) + + const { permissions } = useSelector((state: RootState) => state.user) + + const getButtons = useCallback(() => { + const buttons: React.ReactNode[] = [] + + if (permissions.includes(Permissions.RequestMedication)) { + buttons.push( + , + ) + } + + return buttons + }, [permissions, history, t]) + + useEffect(() => { + setButtons(getButtons()) + return () => { + setButtons([]) + } + }, [getButtons, setButtons]) + + const [searchRequest, setSearchRequest] = useState({ + text: '', + status: 'all', + }) + + const onSearchRequestChange = (newSearchRequest: MedicationSearchRequest) => { + setSearchRequest(newSearchRequest) + } + + return ( + + + + + + + + + ) +} + +export default ViewMedications diff --git a/src/shared/db/MedicationRepository.ts b/src/shared/db/MedicationRepository.ts index 7d9a09573a..c4456c878b 100644 --- a/src/shared/db/MedicationRepository.ts +++ b/src/shared/db/MedicationRepository.ts @@ -1,20 +1,10 @@ +import MedicationSearchRequest from '../../medications/models/MedicationSearchRequest' import { relationalDb } from '../config/pouchdb' import Medication from '../model/Medication' import Repository from './Repository' import SortRequest from './SortRequest' -interface SearchContainer { - text: string - status: - | 'draft' - | 'active' - | 'on hold' - | 'canceled' - | 'completed' - | 'entered in error' - | 'stopped' - | 'unknown' - | 'all' +interface SearchContainer extends MedicationSearchRequest { defaultSortRequest: SortRequest } class MedicationRepository extends Repository { @@ -29,10 +19,7 @@ class MedicationRepository extends Repository { { $or: [ { - 'data.type': searchValue, - }, - { - 'data.code': searchValue, + 'data.medication': searchValue, }, ], }, diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 8bd1108cd2..888566a5a7 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -4,7 +4,6 @@ import ReduxThunk, { ThunkAction } from 'redux-thunk' import lab from '../../labs/lab-slice' import labs from '../../labs/labs-slice' import medication from '../../medications/medication-slice' -import medications from '../../medications/medications-slice' import breadcrumbs from '../../page-header/breadcrumbs/breadcrumbs-slice' import title from '../../page-header/title/title-slice' import patient from '../../patients/patient-slice' @@ -26,7 +25,6 @@ const reducer = combineReducers({ lab, labs, medication, - medications, }) const store = configureStore({ From d0b603e526f66ce9950d3cbd230797905d66ed7c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 12 Sep 2020 06:27:01 +0000 Subject: [PATCH 64/86] build(deps-dev): bump @types/node from 14.6.4 to 14.10.1 Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 14.6.4 to 14.10.1. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efc1314625..9a6cedce9f 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", "@types/lodash": "^4.14.150", - "@types/node": "~14.6.0", + "@types/node": "~14.10.1", "@types/pouchdb": "~6.4.0", "@types/react": "~16.9.17", "@types/react-dom": "~16.9.4", From c47bda64fa41af1e4faffe8f11c8204fc88d2b96 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 09:10:01 +0000 Subject: [PATCH 65/86] build(deps-dev): bump @commitlint/prompt from 9.1.2 to 11.0.0 Bumps [@commitlint/prompt](https://github.com/conventional-changelog/commitlint) from 9.1.2 to 11.0.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/compare/v9.1.2...v11.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9a6cedce9f..a21337d38a 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "@commitlint/cli": "~9.1.2", "@commitlint/config-conventional": "~9.1.1", "@commitlint/core": "~9.1.1", - "@commitlint/prompt": "~9.1.1", + "@commitlint/prompt": "~11.0.0", "@testing-library/react": "~11.0.0", "@testing-library/react-hooks": "~3.4.1", "@types/enzyme": "^3.10.5", From e55c6783078f36977ef2f0dd607536e453f0a51d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 09:55:57 +0000 Subject: [PATCH 66/86] build(deps-dev): bump @commitlint/core from 9.1.2 to 11.0.0 Bumps [@commitlint/core](https://github.com/conventional-changelog/commitlint) from 9.1.2 to 11.0.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/compare/v9.1.2...v11.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a21337d38a..e3b0dff377 100644 --- a/package.json +++ b/package.json @@ -61,7 +61,7 @@ "devDependencies": { "@commitlint/cli": "~9.1.2", "@commitlint/config-conventional": "~9.1.1", - "@commitlint/core": "~9.1.1", + "@commitlint/core": "~11.0.0", "@commitlint/prompt": "~11.0.0", "@testing-library/react": "~11.0.0", "@testing-library/react-hooks": "~3.4.1", From 0aa45054a19a0d739c80bafefaf32ea1f5f3308c Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 13 Sep 2020 10:47:24 +0000 Subject: [PATCH 67/86] build(deps-dev): bump @commitlint/config-conventional Bumps [@commitlint/config-conventional](https://github.com/conventional-changelog/commitlint) from 9.1.2 to 11.0.0. - [Release notes](https://github.com/conventional-changelog/commitlint/releases) - [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/CHANGELOG.md) - [Commits](https://github.com/conventional-changelog/commitlint/compare/v9.1.2...v11.0.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e3b0dff377..ad688ab75d 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ ], "devDependencies": { "@commitlint/cli": "~9.1.2", - "@commitlint/config-conventional": "~9.1.1", + "@commitlint/config-conventional": "~11.0.0", "@commitlint/core": "~11.0.0", "@commitlint/prompt": "~11.0.0", "@testing-library/react": "~11.0.0", From 3844cd3322f68e12318e90c54161324f1509c8df Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 14 Sep 2020 22:15:38 -0500 Subject: [PATCH 68/86] chore(deps): bump @hospitalrun/components to v3.0.0 --- package.json | 2 +- src/HospitalRun.tsx | 4 +++- src/index.tsx | 1 - src/shared/components/Sidebar.tsx | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ad688ab75d..059e5e8687 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "2.0.0", + "@hospitalrun/components": "~3.0.0", "@reduxjs/toolkit": "~1.4.0", "@types/escape-string-regexp": "~2.0.1", "@types/json2csv": "~5.0.1", diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 118bafdeaf..f852aed4ac 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -33,7 +33,9 @@ const HospitalRun = () => {
- +
+ +
{ return (
row.id} - data={patient.visits || []} + data={patientVisits || []} columns={[ { label: t('patient.visits.startDateTime'), @@ -36,7 +44,7 @@ const VisitTable = () => { actions={[ { label: t('actions.view'), - action: (row) => history.push(`/patients/${patient.id}/visits/${row.id}`), + action: (row) => history.push(`/patients/${patientId}/visits/${row.id}`), }, ]} /> From 1c5860f3ea8e29c5814b1dbe3645a7dd02c46060 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 14 Sep 2020 23:37:30 -0500 Subject: [PATCH 72/86] chore(deps): bumb react-query to 2.18.0 --- package.json | 2 +- src/imagings/hooks/useImagingRequest.tsx | 4 ++-- src/imagings/hooks/useImagingSearch.tsx | 7 ++----- src/incidents/hooks/useIncidents.tsx | 7 ++----- src/incidents/report/ReportIncident.tsx | 2 +- src/medications/hooks/useMedicationSearch.tsx | 4 ++-- src/patients/hooks/useAllergy.tsx | 8 ++------ src/patients/hooks/useCareGoal.tsx | 8 ++------ src/patients/hooks/useCarePlan.tsx | 8 ++------ src/patients/hooks/usePatientAllergies.tsx | 4 ++-- src/patients/hooks/usePatientAppointments.tsx | 7 ++----- src/patients/hooks/usePatientCareGoals.tsx | 4 ++-- src/patients/hooks/usePatientCarePlans.tsx | 4 ++-- src/patients/hooks/usePatientLabs.tsx | 4 ++-- src/patients/hooks/usePatientNote.tsx | 4 ++-- src/patients/hooks/usePatientNotes.tsx | 4 ++-- src/patients/hooks/usePatientVisits.tsx | 4 ++-- src/patients/hooks/useVisit.tsx | 4 ++-- 18 files changed, 34 insertions(+), 55 deletions(-) diff --git a/package.json b/package.json index 059e5e8687..69e4daa811 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.5.13", + "react-query": "~2.18.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", diff --git a/src/imagings/hooks/useImagingRequest.tsx b/src/imagings/hooks/useImagingRequest.tsx index 758736888c..968107ed8c 100644 --- a/src/imagings/hooks/useImagingRequest.tsx +++ b/src/imagings/hooks/useImagingRequest.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import ImagingRepository from '../../shared/db/ImagingRepository' import Imaging from '../../shared/model/Imaging' -function getImagingRequestById(_: QueryKey, imagingRequestId: string): Promise { +function getImagingRequestById(_: string, imagingRequestId: string): Promise { return ImagingRepository.find(imagingRequestId) } diff --git a/src/imagings/hooks/useImagingSearch.tsx b/src/imagings/hooks/useImagingSearch.tsx index 77e067d05b..db661af6af 100644 --- a/src/imagings/hooks/useImagingSearch.tsx +++ b/src/imagings/hooks/useImagingSearch.tsx @@ -1,4 +1,4 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import ImagingRepository from '../../shared/db/ImagingRepository' import SortRequest from '../../shared/db/SortRequest' @@ -14,10 +14,7 @@ const defaultSortRequest: SortRequest = { ], } -function searchImagingRequests( - _: QueryKey, - searchRequest: ImagingSearchRequest, -): Promise { +function searchImagingRequests(_: string, searchRequest: ImagingSearchRequest): Promise { return ImagingRepository.search({ ...searchRequest, defaultSortRequest }) } diff --git a/src/incidents/hooks/useIncidents.tsx b/src/incidents/hooks/useIncidents.tsx index 9953c4b7df..8c8b276c53 100644 --- a/src/incidents/hooks/useIncidents.tsx +++ b/src/incidents/hooks/useIncidents.tsx @@ -1,13 +1,10 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import IncidentRepository from '../../shared/db/IncidentRepository' import Incident from '../../shared/model/Incident' import IncidentSearchRequest from '../model/IncidentSearchRequest' -function fetchIncidents( - _: QueryKey, - searchRequest: IncidentSearchRequest, -): Promise { +function fetchIncidents(_: string, searchRequest: IncidentSearchRequest): Promise { return IncidentRepository.search(searchRequest) } diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx index d3ad799e2c..b774f66cf5 100644 --- a/src/incidents/report/ReportIncident.tsx +++ b/src/incidents/report/ReportIncident.tsx @@ -50,7 +50,7 @@ const ReportIncident = () => { const onSave = async () => { try { const data = await mutate(incident as Incident) - history.push(`/incidents/${data.id}`) + history.push(`/incidents/${data?.id}`) } catch (e) { setError(e) } diff --git a/src/medications/hooks/useMedicationSearch.tsx b/src/medications/hooks/useMedicationSearch.tsx index eb378d3460..3c022590bb 100644 --- a/src/medications/hooks/useMedicationSearch.tsx +++ b/src/medications/hooks/useMedicationSearch.tsx @@ -1,4 +1,4 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import MedicationRepository from '../../shared/db/MedicationRepository' import SortRequest from '../../shared/db/SortRequest' @@ -15,7 +15,7 @@ const defaultSortRequest: SortRequest = { } function searchMedicationRequests( - _: QueryKey, + _: string, searchRequest: MedicationSearchRequest, ): Promise { return MedicationRepository.search({ diff --git a/src/patients/hooks/useAllergy.tsx b/src/patients/hooks/useAllergy.tsx index e1a19a044b..3615cf6ef7 100644 --- a/src/patients/hooks/useAllergy.tsx +++ b/src/patients/hooks/useAllergy.tsx @@ -1,13 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Allergy from '../../shared/model/Allergy' -async function getAllergy( - _: QueryKey, - patientId: string, - allergyId: string, -): Promise { +async function getAllergy(_: string, patientId: string, allergyId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeAllergy = patient.allergies?.find((a) => a.id === allergyId) if (!maybeAllergy) { diff --git a/src/patients/hooks/useCareGoal.tsx b/src/patients/hooks/useCareGoal.tsx index 1cc240cfdd..4166206bc7 100644 --- a/src/patients/hooks/useCareGoal.tsx +++ b/src/patients/hooks/useCareGoal.tsx @@ -1,13 +1,9 @@ -import { useQuery, QueryKey } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CareGoal from '../../shared/model/CareGoal' -async function getCareGoal( - _: QueryKey, - patientId: string, - careGoalId: string, -): Promise { +async function getCareGoal(_: string, patientId: string, careGoalId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeCareGoal = patient.careGoals?.find((c) => c.id === careGoalId) diff --git a/src/patients/hooks/useCarePlan.tsx b/src/patients/hooks/useCarePlan.tsx index 7f3fe9477f..f7f97a21b3 100644 --- a/src/patients/hooks/useCarePlan.tsx +++ b/src/patients/hooks/useCarePlan.tsx @@ -1,13 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CarePlan from '../../shared/model/CarePlan' -async function getCarePlan( - _: QueryKey, - patientId: string, - allergyId: string, -): Promise { +async function getCarePlan(_: string, patientId: string, allergyId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeCarePlan = patient.carePlans?.find((a) => a.id === allergyId) if (!maybeCarePlan) { diff --git a/src/patients/hooks/usePatientAllergies.tsx b/src/patients/hooks/usePatientAllergies.tsx index af2c0d428b..66d3b0cf4c 100644 --- a/src/patients/hooks/usePatientAllergies.tsx +++ b/src/patients/hooks/usePatientAllergies.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Allergy from '../../shared/model/Allergy' -async function fetchPatientAllergies(_: QueryKey, patientId: string): Promise { +async function fetchPatientAllergies(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.allergies || [] } diff --git a/src/patients/hooks/usePatientAppointments.tsx b/src/patients/hooks/usePatientAppointments.tsx index 95d8a3dcfe..6a01fb3220 100644 --- a/src/patients/hooks/usePatientAppointments.tsx +++ b/src/patients/hooks/usePatientAppointments.tsx @@ -1,12 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Appointments from '../../shared/model/Appointment' -async function fetchPatientAppointments( - _: QueryKey, - patientId: string, -): Promise { +async function fetchPatientAppointments(_: string, patientId: string): Promise { return PatientRepository.getAppointments(patientId) } diff --git a/src/patients/hooks/usePatientCareGoals.tsx b/src/patients/hooks/usePatientCareGoals.tsx index 291159b4be..b187c2a72f 100644 --- a/src/patients/hooks/usePatientCareGoals.tsx +++ b/src/patients/hooks/usePatientCareGoals.tsx @@ -1,9 +1,9 @@ -import { useQuery, QueryKey } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CareGoal from '../../shared/model/CareGoal' -async function fetchPatientCareGoals(_: QueryKey, patientId: string): Promise { +async function fetchPatientCareGoals(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.careGoals || [] } diff --git a/src/patients/hooks/usePatientCarePlans.tsx b/src/patients/hooks/usePatientCarePlans.tsx index 68ba6b1453..4e05ba4699 100644 --- a/src/patients/hooks/usePatientCarePlans.tsx +++ b/src/patients/hooks/usePatientCarePlans.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import CarePlan from '../../shared/model/CarePlan' -async function fetchPatientCarePlans(_: QueryKey, patientId: string): Promise { +async function fetchPatientCarePlans(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.carePlans || [] } diff --git a/src/patients/hooks/usePatientLabs.tsx b/src/patients/hooks/usePatientLabs.tsx index be76ba627d..0ccd362b7b 100644 --- a/src/patients/hooks/usePatientLabs.tsx +++ b/src/patients/hooks/usePatientLabs.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Lab from '../../shared/model/Lab' -async function fetchPatientLabs(_: QueryKey, patientId: string): Promise { +async function fetchPatientLabs(_: string, patientId: string): Promise { const fetchedLabs = await PatientRepository.getLabs(patientId) return fetchedLabs || [] } diff --git a/src/patients/hooks/usePatientNote.tsx b/src/patients/hooks/usePatientNote.tsx index 941fb8ae62..83b4b5d546 100644 --- a/src/patients/hooks/usePatientNote.tsx +++ b/src/patients/hooks/usePatientNote.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Note from '../../shared/model/Note' -async function getNote(_: QueryKey, patientId: string, noteId: string): Promise { +async function getNote(_: string, patientId: string, noteId: string): Promise { const patient = await PatientRepository.find(patientId) const maybeNote = patient.notes?.find((n) => n.id === noteId) diff --git a/src/patients/hooks/usePatientNotes.tsx b/src/patients/hooks/usePatientNotes.tsx index c0a5ae0fdc..4e9e21d339 100644 --- a/src/patients/hooks/usePatientNotes.tsx +++ b/src/patients/hooks/usePatientNotes.tsx @@ -1,9 +1,9 @@ -import { QueryKey, useQuery } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Note from '../../shared/model/Note' -async function fetchPatientNotes(_: QueryKey, patientId: string): Promise { +async function fetchPatientNotes(_: string, patientId: string): Promise { const patient = await PatientRepository.find(patientId) return patient.notes || [] } diff --git a/src/patients/hooks/usePatientVisits.tsx b/src/patients/hooks/usePatientVisits.tsx index 9cd8d40f68..57a17d29e8 100644 --- a/src/patients/hooks/usePatientVisits.tsx +++ b/src/patients/hooks/usePatientVisits.tsx @@ -1,9 +1,9 @@ -import { useQuery, QueryKey } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Visit from '../../shared/model/Visit' -async function fetchPatientVisits(_: QueryKey, id: string): Promise { +async function fetchPatientVisits(_: string, id: string): Promise { return (await PatientRepository.find(id)).visits } diff --git a/src/patients/hooks/useVisit.tsx b/src/patients/hooks/useVisit.tsx index 0f9dc1091c..2a953796ca 100644 --- a/src/patients/hooks/useVisit.tsx +++ b/src/patients/hooks/useVisit.tsx @@ -1,10 +1,10 @@ -import { useQuery, QueryKey } from 'react-query' +import { useQuery } from 'react-query' import PatientRepository from '../../shared/db/PatientRepository' import Visit from '../../shared/model/Visit' async function fetchVisit( - _: QueryKey, + _: string, patientId: string, visitsId: string, ): Promise { From 68cf76e5d83a0d7b10438b71c0eccd5306d47bbb Mon Sep 17 00:00:00 2001 From: Rafael Sousa Date: Tue, 15 Sep 2020 01:48:17 -0300 Subject: [PATCH 73/86] test(login): improve test login screen (#2312) Co-authored-by: Matteo Vivona --- src/__tests__/login/Login.test.tsx | 314 +++++++++++++++++++++++++++++ 1 file changed, 314 insertions(+) create mode 100644 src/__tests__/login/Login.test.tsx diff --git a/src/__tests__/login/Login.test.tsx b/src/__tests__/login/Login.test.tsx new file mode 100644 index 0000000000..df6e098b53 --- /dev/null +++ b/src/__tests__/login/Login.test.tsx @@ -0,0 +1,314 @@ +import { Alert } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import { createMemoryHistory } from 'history' +import React from 'react' +import Button from 'react-bootstrap/Button' +import { act } from 'react-dom/test-utils' +import { Provider } from 'react-redux' +import { Router } from 'react-router' +import createMockStore, { MockStore } from 'redux-mock-store' +import thunk from 'redux-thunk' + +import Login from '../../login/Login' +import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import { remoteDb } from '../../shared/config/pouchdb' +import User from '../../shared/model/User' +import { RootState } from '../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('Login', () => { + const history = createMemoryHistory() + let store: MockStore + + const setup = (storeValue: any = { loginError: {}, user: {} as User }) => { + history.push('/login') + store = mockStore(storeValue) + + const wrapper = mount( + + + + + , + ) + + wrapper.update() + return wrapper + } + + describe('Layout initial validations', () => { + it('should render a login form', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + const form = wrapper.find('form') + expect(form).toHaveLength(1) + }) + + it('should render a username and password input', async () => { + let wrapper: any + + await act(async () => { + wrapper = setup() + }) + + const user = wrapper.find(TextInputWithLabelFormGroup) + expect(user).toHaveLength(2) + }) + + it('should render a submit button', async () => { + let wrapper: any + await act(async () => { + wrapper = setup() + }) + + const button = wrapper.find(Button) + expect(button).toHaveLength(1) + }) + }) + + describe('Unable to login', () => { + it('should get field required error message if no username is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn') + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('', 'password') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }) + }) + + it('should show required username error message', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ + user: { + loginError: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }, + } as any) + }) + + let password: ReactWrapper = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const alert = wrapper.find(Alert) + const username = wrapper.find('#usernameTextInput') + password = wrapper.find('#passwordTextInput') + + const usernameFeedback = username.find('Feedback') + const passwordFeedback = password.find('Feedback') + + expect(alert.prop('message')).toEqual('user.login.error.message.required') + expect(usernameFeedback.hasClass('undefined')).not.toBeTruthy() + expect(passwordFeedback.hasClass('undefined')).toBeTruthy() + }) + + it('should get field required error message if no password is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn') + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('username', '') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }) + }) + + it('should show required password error message', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup({ + user: { + loginError: { + message: 'user.login.error.message.required', + username: 'user.login.error.username.required', + password: 'user.login.error.password.required', + }, + }, + } as any) + }) + + let username: ReactWrapper = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + wrapper.update() + + const alert = wrapper.find(Alert) + const password = wrapper.find('#passwordTextInput').at(0) + username = wrapper.find('#usernameTextInput').at(0) + + const passwordFeedback = password.find('Feedback') + const usernameFeedback = username.find('Feedback') + + expect(alert.prop('message')).toEqual('user.login.error.message.required') + expect(passwordFeedback.hasClass('undefined')).not.toBeTruthy() + expect(usernameFeedback.hasClass('undefined')).toBeTruthy() + }) + + it('should get incorrect username or password error when incorrect username or password is provided', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn').mockRejectedValue({ status: 401 }) + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + wrapper.update() + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(remoteDb.logIn).toHaveBeenCalledWith('username', 'password') + expect(history.location.pathname).toEqual('/login') + expect(store.getActions()).toContainEqual({ + type: 'user/loginError', + payload: { + message: 'user.login.error.message.incorrect', + }, + }) + }) + }) + + describe('Sucessfully login', () => { + it('should log in if username and password is correct', async () => { + let wrapper: any + + await act(async () => { + wrapper = await setup() + }) + + jest.spyOn(remoteDb, 'logIn').mockResolvedValue({ + name: 'username', + ok: true, + roles: [], + }) + + jest.spyOn(remoteDb, 'getUser').mockResolvedValue({ + _id: 'userId', + metadata: { + givenName: 'test', + familyName: 'user', + }, + } as any) + + const username = wrapper.find('#usernameTextInput').at(0) + await act(async () => { + const onChange = username.prop('onChange') as any + await onChange({ currentTarget: { value: 'username' } }) + }) + + const password = wrapper.find('#passwordTextInput').at(0) + await act(async () => { + const onChange = password.prop('onChange') as any + await onChange({ currentTarget: { value: 'password' } }) + }) + + const saveButton = wrapper.find({ type: 'submit' }).at(0) + + await act(async () => { + const onClick = saveButton.prop('onClick') as any + await onClick({ preventDefault: jest.fn() }) + }) + + wrapper.update() + + expect(store.getActions()[0].payload.user).toEqual({ + id: 'userId', + givenName: 'test', + familyName: 'user', + }) + expect(store.getActions()[0].type).toEqual('user/loginSuccess') + }) + }) +}) From f053100ededaee0a68445494db09f26eed1940cf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 06:52:33 +0000 Subject: [PATCH 74/86] build(deps): bump react-query from 2.18.0 to 2.19.0 Bumps [react-query](https://github.com/tannerlinsley/react-query) from 2.18.0 to 2.19.0. - [Release notes](https://github.com/tannerlinsley/react-query/releases) - [Commits](https://github.com/tannerlinsley/react-query/compare/v2.18.0...v2.19.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 69e4daa811..3b23d34d84 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.18.0", + "react-query": "~2.19.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", From a551fb965b5e2c1dc59e4eafb6743cba7d448f31 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2020 15:22:29 +0000 Subject: [PATCH 75/86] build(deps): bump react-query from 2.19.0 to 2.20.0 Bumps [react-query](https://github.com/tannerlinsley/react-query) from 2.19.0 to 2.20.0. - [Release notes](https://github.com/tannerlinsley/react-query/releases) - [Commits](https://github.com/tannerlinsley/react-query/compare/v2.19.0...v2.20.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3b23d34d84..cc5fe86dd1 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.19.0", + "react-query": "~2.20.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", From 87cc20eedc2d4866e2d51a6b5e3cc6adaabc3dc6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2020 06:21:51 +0000 Subject: [PATCH 76/86] build(deps-dev): bump lint-staged from 10.3.0 to 10.4.0 Bumps [lint-staged](https://github.com/okonet/lint-staged) from 10.3.0 to 10.4.0. - [Release notes](https://github.com/okonet/lint-staged/releases) - [Commits](https://github.com/okonet/lint-staged/compare/v10.3.0...v10.4.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc5fe86dd1..d0657cbdc3 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "history": "4.10.1", "husky": "~4.3.0", "jest": "24.9.0", - "lint-staged": "~10.3.0", + "lint-staged": "~10.4.0", "memdown": "~5.1.0", "prettier": "~2.1.0", "redux-mock-store": "~1.5.4", From b9ee3093d6eb256176bad8edf1bd34fb2246766a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2020 06:48:56 +0000 Subject: [PATCH 77/86] build(deps): bump react-query from 2.20.1 to 2.21.0 Bumps [react-query](https://github.com/tannerlinsley/react-query) from 2.20.1 to 2.21.0. - [Release notes](https://github.com/tannerlinsley/react-query/releases) - [Commits](https://github.com/tannerlinsley/react-query/compare/v2.20.1...v2.21.0) Signed-off-by: dependabot-preview[bot] --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d0657cbdc3..c8e0ffb916 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.20.0", + "react-query": "~2.21.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", From a4850ba9c862c33b821236a6ce40857836c25ef2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 17 Sep 2020 17:28:43 +0000 Subject: [PATCH 78/86] build(deps): bump react-query from 2.21.2 to 2.22.0 (#2406) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c8e0ffb916..2541ecced6 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.21.0", + "react-query": "~2.22.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", From 9f8e67eb694205f5fe50fc6a901bde684549b8b0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 18 Sep 2020 06:48:55 +0000 Subject: [PATCH 79/86] build(deps-dev): bump @types/node from 14.10.3 to 14.11.1 (#2407) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2541ecced6..4d08ed17e8 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "@types/enzyme": "^3.10.5", "@types/jest": "~26.0.0", "@types/lodash": "^4.14.150", - "@types/node": "~14.10.1", + "@types/node": "~14.11.1", "@types/pouchdb": "~6.4.0", "@types/react": "~16.9.17", "@types/react-dom": "~16.9.4", From d19251c29e8eacc464a6ef834a6d46ec4acdfaf0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2020 07:09:02 +0000 Subject: [PATCH 80/86] build(deps): bump react-query from 2.22.2 to 2.23.0 (#2412) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4d08ed17e8..81348b871a 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "react-bootstrap-typeahead": "~5.1.0", "react-dom": "~16.13.0", "react-i18next": "~11.7.0", - "react-query": "~2.22.0", + "react-query": "~2.23.0", "react-query-devtools": "~2.4.2", "react-redux": "~7.2.0", "react-router": "~5.2.0", From b73cbbe5644902364e85de5b58695d1a78246c68 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2020 07:30:34 +0000 Subject: [PATCH 81/86] build(deps): bump react-query-devtools from 2.4.7 to 2.5.0 (#2413) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81348b871a..3caa4e1731 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "react-dom": "~16.13.0", "react-i18next": "~11.7.0", "react-query": "~2.23.0", - "react-query-devtools": "~2.4.2", + "react-query-devtools": "~2.5.0", "react-redux": "~7.2.0", "react-router": "~5.2.0", "react-router-dom": "~5.2.0", From b36247b670d2e9fdb20434f7fea76ca8d99d4b47 Mon Sep 17 00:00:00 2001 From: Michael Kramer Date: Sat, 19 Sep 2020 13:11:20 -0400 Subject: [PATCH 82/86] refactor(patient): refactor patient diagnoses (#2409) Co-authored-by: Matteo Vivona --- .../requests/NewImagingRequest.test.tsx | 2 +- .../search/MedicationRequestSearch.test.tsx | 2 +- .../diagnoses/AddDiagnosisModal.test.tsx | 92 +++++++++---------- .../patients/diagnoses/Diagnoses.test.tsx | 27 +----- .../patients/diagnoses/DiagnosesList.test.tsx | 51 ++++++++++ .../hooks/useAddPatientDiagnosis.test.tsx | 62 +++++++++++++ .../patients/util/validate-diagnosis.test.ts | 18 ++++ .../input/SelectWithLabelFormGroup.test.tsx | 2 +- src/imagings/requests/NewImagingRequest.tsx | 2 +- src/incidents/list/ViewIncidents.tsx | 2 +- src/labs/ViewLabs.tsx | 2 +- src/medications/ViewMedication.tsx | 2 +- .../requests/NewMedicationRequest.tsx | 2 +- .../search/MedicationRequestSearch.tsx | 2 +- src/patients/ContactInfo.tsx | 2 +- src/patients/GeneralInformation.tsx | 2 +- src/patients/care-goals/CareGoalForm.tsx | 2 +- src/patients/care-plans/CarePlanForm.tsx | 2 +- src/patients/diagnoses/AddDiagnosisModal.tsx | 30 +++--- src/patients/diagnoses/Diagnoses.tsx | 23 ++--- src/patients/diagnoses/DiagnosesList.tsx | 41 +++++++++ src/patients/diagnoses/DiagnosisForm.tsx | 21 ++--- src/patients/hooks/useAddPatientDiagnosis.tsx | 37 ++++++++ src/patients/hooks/usePatientDiagnoses.tsx | 13 +++ src/patients/util/validate-diagnosis.ts | 46 ++++++++++ src/patients/visits/VisitForm.tsx | 2 +- .../appointments/AppointmentDetailForm.tsx | 2 +- .../components/input/LanguageSelector.tsx | 2 +- ...Group.tsx => SelectWithLabelFormGroup.tsx} | 0 src/shared/components/input/index.tsx | 17 ++++ 30 files changed, 380 insertions(+), 130 deletions(-) create mode 100644 src/__tests__/patients/diagnoses/DiagnosesList.test.tsx create mode 100644 src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx create mode 100644 src/__tests__/patients/util/validate-diagnosis.test.ts create mode 100644 src/patients/diagnoses/DiagnosesList.tsx create mode 100644 src/patients/hooks/useAddPatientDiagnosis.tsx create mode 100644 src/patients/hooks/usePatientDiagnoses.tsx create mode 100644 src/patients/util/validate-diagnosis.ts rename src/shared/components/input/{SelectWithLableFormGroup.tsx => SelectWithLabelFormGroup.tsx} (100%) create mode 100644 src/shared/components/input/index.tsx diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx index 21aacc5d8b..59cced4656 100644 --- a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -12,7 +12,7 @@ import NewImagingRequest from '../../../imagings/requests/NewImagingRequest' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' import * as titleUtil from '../../../page-header/title/useTitle' -import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import ImagingRepository from '../../../shared/db/ImagingRepository' diff --git a/src/__tests__/medications/search/MedicationRequestSearch.test.tsx b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx index 2aa71ee9f3..981d7358ce 100644 --- a/src/__tests__/medications/search/MedicationRequestSearch.test.tsx +++ b/src/__tests__/medications/search/MedicationRequestSearch.test.tsx @@ -4,7 +4,7 @@ import { act } from 'react-dom/test-utils' import MedicationSearchRequest from '../../../medications/models/MedicationSearchRequest' import MedicationRequestSearch from '../../../medications/search/MedicationRequestSearch' -import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLableFormGroup' +import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' describe('Medication Request Search', () => { diff --git a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx index 5349e5b095..0ff107dfb8 100644 --- a/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx +++ b/src/__tests__/patients/diagnoses/AddDiagnosisModal.test.tsx @@ -1,27 +1,30 @@ +/* eslint-disable no-console */ import { Modal } from '@hospitalrun/components' import { mount } 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 AddDiagnosisModal from '../../../patients/diagnoses/AddDiagnosisModal' import DiagnosisForm from '../../../patients/diagnoses/DiagnosisForm' -import * as patientSlice from '../../../patients/patient-slice' import PatientRepository from '../../../shared/db/PatientRepository' import { CarePlanIntent, CarePlanStatus } from '../../../shared/model/CarePlan' +import Diagnosis, { DiagnosisStatus } from '../../../shared/model/Diagnosis' import Patient from '../../../shared/model/Patient' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) describe('Add Diagnosis Modal', () => { - const patient = { + const mockDiagnosis: Diagnosis = { + id: '123', + name: 'some name', + diagnosisDate: new Date().toISOString(), + onsetDate: new Date().toISOString(), + abatementDate: new Date().toISOString(), + status: DiagnosisStatus.Active, + visit: '1234', + note: 'some note', + } + const mockPatient = { id: 'patientId', - diagnoses: [{ id: '123', name: 'some name', diagnosisDate: new Date().toISOString() }], + diagnoses: [mockDiagnosis], carePlans: [ { id: '123', @@ -36,28 +39,20 @@ describe('Add Diagnosis Modal', () => { ], } as Patient - const diagnosisError = { - title: 'some diagnosisError error', - } - - const onCloseSpy = jest.fn() - const setup = () => { + const setup = (patient = mockPatient, onCloseSpy = jest.fn()) => { jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) - jest.spyOn(PatientRepository, 'saveOrUpdate') - const store = mockStore({ patient: { patient, diagnosisError } } as any) - const history = createMemoryHistory() + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) + const wrapper = mount( - - - - - , + , ) wrapper.update() return { wrapper } } - + beforeEach(() => { + console.error = jest.fn() + }) it('should render a modal', () => { const { wrapper } = setup() @@ -78,48 +73,45 @@ describe('Add Diagnosis Modal', () => { const diagnosisForm = wrapper.find(DiagnosisForm) expect(diagnosisForm).toHaveLength(1) - expect(diagnosisForm.prop('diagnosisError')).toEqual(diagnosisError) }) it('should dispatch add diagnosis when the save button is clicked', async () => { - const { wrapper } = setup() - jest.spyOn(patientSlice, 'addDiagnosis') + const patient = mockPatient + patient.diagnoses = [] + const { wrapper } = setup(patient) + + const newDiagnosis = mockDiagnosis + newDiagnosis.name = 'New Diagnosis Name' act(() => { const diagnosisForm = wrapper.find(DiagnosisForm) const onChange = diagnosisForm.prop('onChange') as any - if (patient.diagnoses != null) { - onChange(patient.diagnoses[0]) - } + onChange(newDiagnosis) }) wrapper.update() await act(async () => { const modal = wrapper.find(Modal) - const successButton = modal.prop('successButton') - const onClick = successButton?.onClick as any - await onClick() + const onSave = (modal.prop('successButton') as any).onClick + await onSave({} as React.MouseEvent) }) - - expect(patientSlice.addDiagnosis).toHaveBeenCalledTimes(1) - if (patient.diagnoses != null) { - expect(patientSlice.addDiagnosis).toHaveBeenCalledWith(patient.id, patient.diagnoses[0]) - } + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + diagnoses: [expect.objectContaining({ name: 'New Diagnosis Name' })], + }), + ) }) - it('should call the on close function when the cancel button is clicked', () => { - const { wrapper } = setup() - + it('should call the on close function when the cancel button is clicked', async () => { + const onCloseButtonClickSpy = jest.fn() + const { wrapper } = setup(mockPatient, onCloseButtonClickSpy) const modal = wrapper.find(Modal) - - expect(modal).toHaveLength(1) - act(() => { - const cancelButton = modal.prop('closeButton') - const onClick = cancelButton?.onClick as any + const { onClick } = modal.prop('closeButton') as any onClick() }) - - expect(onCloseSpy).toHaveBeenCalledTimes(1) + expect(modal).toHaveLength(1) + expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1) }) }) diff --git a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx index 453bb50cb2..f60b1a58ff 100644 --- a/src/__tests__/patients/diagnoses/Diagnoses.test.tsx +++ b/src/__tests__/patients/diagnoses/Diagnoses.test.tsx @@ -1,3 +1,5 @@ +/* eslint-disable no-console */ + import * as components from '@hospitalrun/components' import { mount } from 'enzyme' import { createMemoryHistory } from 'history' @@ -46,8 +48,8 @@ describe('Diagnoses', () => { beforeEach(() => { jest.resetAllMocks() jest.spyOn(PatientRepository, 'saveOrUpdate') + console.error = jest.fn() }) - it('should render a add diagnoses button', () => { const wrapper = setup() @@ -75,27 +77,4 @@ describe('Diagnoses', () => { expect(wrapper.find(components.Modal).prop('show')).toBeTruthy() }) }) - - describe('diagnoses list', () => { - it('should list the patients diagnoses', () => { - const diagnoses = expectedPatient.diagnoses as Diagnosis[] - const wrapper = setup() - - const list = wrapper.find(components.List) - const listItems = wrapper.find(components.ListItem) - - expect(list).toHaveLength(1) - expect(listItems).toHaveLength(diagnoses.length) - }) - - it('should render a warning message if the patient does not have any diagnoses', () => { - const wrapper = setup({ ...expectedPatient, diagnoses: [] }) - - const alert = wrapper.find(components.Alert) - - expect(alert).toHaveLength(1) - expect(alert.prop('title')).toEqual('patient.diagnoses.warning.noDiagnoses') - expect(alert.prop('message')).toEqual('patient.diagnoses.addDiagnosisAbove') - }) - }) }) diff --git a/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx b/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx new file mode 100644 index 0000000000..e47be82b3d --- /dev/null +++ b/src/__tests__/patients/diagnoses/DiagnosesList.test.tsx @@ -0,0 +1,51 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import { mount, ReactWrapper } from 'enzyme' +import React from 'react' +import { act } from 'react-dom/test-utils' + +import DiagnosesList from '../../../patients/diagnoses/DiagnosesList' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' + +const expectedDiagnoses = [ + { id: '123', name: 'diagnosis1', diagnosisDate: new Date().toISOString() } as Diagnosis, +] + +describe('Diagnoses list', () => { + const setup = async (diagnoses: Diagnosis[]) => { + const mockPatient = { id: '123', diagnoses } as Patient + jest.spyOn(PatientRepository, 'find').mockResolvedValueOnce(mockPatient) + let wrapper: any + await act(async () => { + wrapper = await mount() + }) + + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } + + it('should list the patients diagnoses', async () => { + const diagnoses = expectedDiagnoses as Diagnosis[] + const { wrapper } = await setup(diagnoses) + + const listItems = wrapper.find(ListItem) + + expect(wrapper.exists(List)).toBeTruthy() + expect(listItems).toHaveLength(expectedDiagnoses.length) + expect(listItems.at(0).text()).toEqual(expectedDiagnoses[0].name) + }) + + it('should render a warning message if the patient does not have any diagnoses', async () => { + const { wrapper } = await setup([]) + + const alert = wrapper.find(Alert) + + expect(wrapper.exists(Alert)).toBeTruthy() + expect(wrapper.exists(List)).toBeFalsy() + + expect(alert.prop('color')).toEqual('warning') + expect(alert.prop('title')).toEqual('patient.diagnoses.warning.noDiagnoses') + expect(alert.prop('message')).toEqual('patient.diagnoses.addDiagnosisAbove') + }) +}) diff --git a/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx b/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx new file mode 100644 index 0000000000..c234ae3144 --- /dev/null +++ b/src/__tests__/patients/hooks/useAddPatientDiagnosis.test.tsx @@ -0,0 +1,62 @@ +/* eslint-disable no-console */ + +import useAddPatientDiagnosis from '../../../patients/hooks/useAddPatientDiagnosis' +import * as validateDiagnosis from '../../../patients/util/validate-diagnosis' +import PatientRepository from '../../../shared/db/PatientRepository' +import Diagnosis, { DiagnosisStatus } from '../../../shared/model/Diagnosis' +import Patient from '../../../shared/model/Patient' +import * as uuid from '../../../shared/util/uuid' +import executeMutation from '../../test-utils/use-mutation.util' + +describe('use add diagnosis', () => { + beforeEach(() => { + jest.resetAllMocks() + console.error = jest.fn() + }) + + it('should throw an error if diagnosis validation fails', async () => { + const expectedError = { name: 'some error' } + jest.spyOn(validateDiagnosis, 'default').mockReturnValue(expectedError) + jest.spyOn(PatientRepository, 'saveOrUpdate') + + try { + await executeMutation(() => useAddPatientDiagnosis(), { patientId: '123', note: {} }) + } catch (e) { + expect(e).toEqual(expectedError) + } + + expect(PatientRepository.saveOrUpdate).not.toHaveBeenCalled() + }) + + it('should add the diaganosis to the patient', async () => { + const expectedDiagnosis: Diagnosis = { + id: '123', + name: 'New Diagnosis Name', + diagnosisDate: new Date().toISOString(), + onsetDate: new Date().toISOString(), + abatementDate: new Date().toISOString(), + status: DiagnosisStatus.Active, + visit: '1234', + note: 'some note', + } + const givenPatient = { id: 'patientId', diagnoses: [] as Diagnosis[] } as Patient + jest.spyOn(uuid, 'uuid').mockReturnValue(expectedDiagnosis.id) + const expectedPatient = { ...givenPatient, diagnoses: [expectedDiagnosis] } + jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(expectedPatient) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(givenPatient) + + const result = await executeMutation(() => useAddPatientDiagnosis(), { + patientId: givenPatient.id, + diagnosis: expectedDiagnosis, + }) + + expect(PatientRepository.find).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledTimes(1) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith( + expect.objectContaining({ + diagnoses: [expect.objectContaining({ name: 'New Diagnosis Name' })], + }), + ) + expect(result).toEqual([expectedDiagnosis]) + }) +}) diff --git a/src/__tests__/patients/util/validate-diagnosis.test.ts b/src/__tests__/patients/util/validate-diagnosis.test.ts new file mode 100644 index 0000000000..949bd4f23d --- /dev/null +++ b/src/__tests__/patients/util/validate-diagnosis.test.ts @@ -0,0 +1,18 @@ +import validateDiagnosis from '../../../patients/util/validate-diagnosis' + +describe('validate diagnosis', () => { + it('should check for required fields', () => { + const diagnosis = {} as any + const expectedError = { + name: 'patient.diagnoses.error.nameRequired', + diagnosisDate: 'patient.diagnoses.error.dateRequired', + onsetDate: 'patient.diagnoses.error.dateRequired', + abatementDate: 'patient.diagnoses.error.dateRequired', + status: 'patient.diagnoses.error.statusRequired', + } + + const actualError = validateDiagnosis(diagnosis) + + expect(actualError).toEqual(expectedError) + }) +}) diff --git a/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx index d4cf9cc75a..0d0e9dc903 100644 --- a/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx +++ b/src/__tests__/shared/components/input/SelectWithLabelFormGroup.test.tsx @@ -2,7 +2,7 @@ import { Label, Select } from '@hospitalrun/components' import { shallow } from 'enzyme' import React from 'react' -import SelectWithLabelFormGroup from '../../../../shared/components/input/SelectWithLableFormGroup' +import SelectWithLabelFormGroup from '../../../../shared/components/input/SelectWithLabelFormGroup' describe('select with label form group', () => { describe('layout', () => { diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx index f032647601..e257f7a803 100644 --- a/src/imagings/requests/NewImagingRequest.tsx +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -7,7 +7,7 @@ import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTitle from '../../page-header/title/useTitle' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index 8882214a1f..fcff4d56d9 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -6,7 +6,7 @@ import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonB import useTitle from '../../page-header/title/useTitle' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' import IncidentFilter from '../IncidentFilter' import ViewIncidentsTable from './ViewIncidentsTable' diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx index 6caa3f1663..9e69e8ac4d 100644 --- a/src/labs/ViewLabs.tsx +++ b/src/labs/ViewLabs.tsx @@ -8,7 +8,7 @@ import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarP import useTitle from '../page-header/title/useTitle' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useDebounce from '../shared/hooks/useDebounce' import useTranslator from '../shared/hooks/useTranslator' diff --git a/src/medications/ViewMedication.tsx b/src/medications/ViewMedication.tsx index 1f20cc5404..40067b2090 100644 --- a/src/medications/ViewMedication.tsx +++ b/src/medications/ViewMedication.tsx @@ -8,7 +8,7 @@ import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' import useTitle from '../page-header/title/useTitle' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' diff --git a/src/medications/requests/NewMedicationRequest.tsx b/src/medications/requests/NewMedicationRequest.tsx index e2726bb3f8..0893778539 100644 --- a/src/medications/requests/NewMedicationRequest.tsx +++ b/src/medications/requests/NewMedicationRequest.tsx @@ -7,7 +7,7 @@ import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTitle from '../../page-header/title/useTitle' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' diff --git a/src/medications/search/MedicationRequestSearch.tsx b/src/medications/search/MedicationRequestSearch.tsx index 84c93cbc57..21781a0ef8 100644 --- a/src/medications/search/MedicationRequestSearch.tsx +++ b/src/medications/search/MedicationRequestSearch.tsx @@ -2,7 +2,7 @@ import React, { ChangeEvent } from 'react' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' import MedicationSearchRequest from '../models/MedicationSearchRequest' diff --git a/src/patients/ContactInfo.tsx b/src/patients/ContactInfo.tsx index 70ff3eb473..229b5f685b 100644 --- a/src/patients/ContactInfo.tsx +++ b/src/patients/ContactInfo.tsx @@ -3,7 +3,7 @@ import React, { useEffect, ReactElement } from 'react' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index ce3a63a680..1171bfae6f 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -5,7 +5,7 @@ import React, { ReactElement } from 'react' import DatePickerWithLabelFormGroup from '../shared/components/input/DatePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../shared/components/input/SelectWithLableFormGroup' +} from '../shared/components/input/SelectWithLabelFormGroup' import TextInputWithLabelFormGroup from '../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' import { ContactInfoPiece } from '../shared/model/ContactInformation' diff --git a/src/patients/care-goals/CareGoalForm.tsx b/src/patients/care-goals/CareGoalForm.tsx index b53d7c7a38..972c99d2ef 100644 --- a/src/patients/care-goals/CareGoalForm.tsx +++ b/src/patients/care-goals/CareGoalForm.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' import CareGoal, { CareGoalStatus, CareGoalAchievementStatus } from '../../shared/model/CareGoal' diff --git a/src/patients/care-plans/CarePlanForm.tsx b/src/patients/care-plans/CarePlanForm.tsx index 9ff64150c0..1e27aad0f8 100644 --- a/src/patients/care-plans/CarePlanForm.tsx +++ b/src/patients/care-plans/CarePlanForm.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' diff --git a/src/patients/diagnoses/AddDiagnosisModal.tsx b/src/patients/diagnoses/AddDiagnosisModal.tsx index f96c1eb66f..3d99e9246a 100644 --- a/src/patients/diagnoses/AddDiagnosisModal.tsx +++ b/src/patients/diagnoses/AddDiagnosisModal.tsx @@ -1,14 +1,15 @@ import { Modal } from '@hospitalrun/components' import React, { useState, useEffect } from 'react' -import { useDispatch, useSelector } from 'react-redux' import useTranslator from '../../shared/hooks/useTranslator' -import Diagnosis from '../../shared/model/Diagnosis' -import { RootState } from '../../shared/store' -import { addDiagnosis } from '../patient-slice' +import Diagnosis, { DiagnosisStatus } from '../../shared/model/Diagnosis' +import Patient from '../../shared/model/Patient' +import useAddPatientDiagnosis from '../hooks/useAddPatientDiagnosis' +import { DiagnosisError } from '../util/validate-diagnosis' import DiagnosisForm from './DiagnosisForm' -interface Props { +interface NewDiagnosisModalProps { + patient: Patient show: boolean onCloseButtonClick: () => void } @@ -20,16 +21,16 @@ const initialDiagnosisState = { abatementDate: new Date().toISOString(), note: '', visit: '', + status: DiagnosisStatus.Active, } -const AddDiagnosisModal = (props: Props) => { - const { show, onCloseButtonClick } = props - const dispatch = useDispatch() - const { diagnosisError, patient } = useSelector((state: RootState) => state.patient) +const AddDiagnosisModal = (props: NewDiagnosisModalProps) => { + const { show, onCloseButtonClick, patient } = props const { t } = useTranslator() + const [mutate] = useAddPatientDiagnosis() const [diagnosis, setDiagnosis] = useState(initialDiagnosisState) - + const [diagnosisError, setDiagnosisError] = useState(undefined) useEffect(() => { setDiagnosis(initialDiagnosisState) }, [show]) @@ -37,8 +38,13 @@ const AddDiagnosisModal = (props: Props) => { const onDiagnosisChange = (newDiagnosis: Partial) => { setDiagnosis(newDiagnosis as Diagnosis) } - const onSaveButtonClick = () => { - dispatch(addDiagnosis(patient.id, diagnosis as Diagnosis)) + const onSaveButtonClick = async () => { + try { + await mutate({ diagnosis, patientId: patient.id }) + onCloseButtonClick() + } catch (e) { + setDiagnosisError(e) + } } const body = ( diff --git a/src/patients/diagnoses/Diagnoses.tsx b/src/patients/diagnoses/Diagnoses.tsx index d529fb306c..12ee041896 100644 --- a/src/patients/diagnoses/Diagnoses.tsx +++ b/src/patients/diagnoses/Diagnoses.tsx @@ -1,14 +1,14 @@ -import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { Button } from '@hospitalrun/components' import React, { useState } from 'react' import { useSelector } from 'react-redux' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import useTranslator from '../../shared/hooks/useTranslator' -import Diagnosis from '../../shared/model/Diagnosis' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' import AddDiagnosisModal from './AddDiagnosisModal' +import DiagnosesList from './DiagnosesList' interface Props { patient: Patient @@ -50,19 +50,12 @@ const Diagnoses = (props: Props) => {
- {(!patient.diagnoses || patient.diagnoses.length === 0) && ( - - )} - - {patient.diagnoses?.map((a: Diagnosis) => ( - {a.name} - ))} - - + + ) } diff --git a/src/patients/diagnoses/DiagnosesList.tsx b/src/patients/diagnoses/DiagnosesList.tsx new file mode 100644 index 0000000000..42736e669e --- /dev/null +++ b/src/patients/diagnoses/DiagnosesList.tsx @@ -0,0 +1,41 @@ +import { Alert, List, ListItem } from '@hospitalrun/components' +import React from 'react' + +import Loading from '../../shared/components/Loading' +import useTranslator from '../../shared/hooks/useTranslator' +import Diagnosis from '../../shared/model/Diagnosis' +import usePatientDiagnoses from '../hooks/usePatientDiagnoses' + +interface Props { + patientId: string +} + +const DiagnosesList = (props: Props) => { + const { patientId } = props + const { t } = useTranslator() + const { data, status } = usePatientDiagnoses(patientId) + + if (data === undefined || status === 'loading') { + return + } + + if (data.length === 0) { + return ( + + ) + } + + return ( + + {data.map((diagnosis: Diagnosis) => ( + {diagnosis.name} + ))} + + ) +} + +export default DiagnosesList diff --git a/src/patients/diagnoses/DiagnosisForm.tsx b/src/patients/diagnoses/DiagnosisForm.tsx index e79178806c..7d1a7ebcfc 100644 --- a/src/patients/diagnoses/DiagnosisForm.tsx +++ b/src/patients/diagnoses/DiagnosisForm.tsx @@ -3,12 +3,7 @@ import format from 'date-fns/format' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -import DatePickerWithLabelFormGroup from '../../shared/components/input/DatePickerWithLabelFormGroup' -import SelectWithLabelFormGroup, { - Option, -} from '../../shared/components/input/SelectWithLableFormGroup' -import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' -import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' +import Input, { Option } from '../../shared/components/input' import Diagnosis, { DiagnosisStatus } from '../../shared/model/Diagnosis' import Patient from '../../shared/model/Patient' @@ -74,7 +69,7 @@ const DiagnosisForm = (props: Props) => { - { - { - { - { - { - { - +} + +async function addDiagnosis(request: AddDiagnosisRequest): Promise { + const error = validateDiagnosis(request.diagnosis) + if (isEmpty(error)) { + const patient = await PatientRepository.find(request.patientId) + const diagnoses = patient.diagnoses ? [...patient.diagnoses] : [] + const newDiagnosis: Diagnosis = { + id: uuid(), + ...request.diagnosis, + } + diagnoses.push(newDiagnosis) + await PatientRepository.saveOrUpdate({ ...patient, diagnoses }) + return diagnoses + } + throw error +} + +export default function useAddPatientDiagnosis() { + return useMutation(addDiagnosis, { + onSuccess: async (data, variables) => { + await queryCache.setQueryData(['diagnoses', variables.patientId], data) + }, + throwOnError: true, + }) +} diff --git a/src/patients/hooks/usePatientDiagnoses.tsx b/src/patients/hooks/usePatientDiagnoses.tsx new file mode 100644 index 0000000000..1cd513baf0 --- /dev/null +++ b/src/patients/hooks/usePatientDiagnoses.tsx @@ -0,0 +1,13 @@ +import { useQuery } from 'react-query' + +import PatientRepository from '../../shared/db/PatientRepository' +import Diagnosis from '../../shared/model/Diagnosis' + +async function fetchPatientDiagnoses(_: string, patientId: string): Promise { + const patient = await PatientRepository.find(patientId) + return patient.diagnoses || [] +} + +export default function usePatientDiagnoses(patientId: string) { + return useQuery(['diagnoses', patientId], fetchPatientDiagnoses) +} diff --git a/src/patients/util/validate-diagnosis.ts b/src/patients/util/validate-diagnosis.ts new file mode 100644 index 0000000000..7a5b1ebbeb --- /dev/null +++ b/src/patients/util/validate-diagnosis.ts @@ -0,0 +1,46 @@ +import Diagnosis from '../../shared/model/Diagnosis' + +interface AddDiagnosisError { + message?: string + name?: string + diagnosisDate?: string + onsetDate?: string + abatementDate?: string + status?: string + note?: string +} + +export class DiagnosisError extends Error { + nameError?: string + + constructor(message: string, name: string) { + super(message) + this.nameError = name + Object.setPrototypeOf(this, DiagnosisError.prototype) + } +} + +export default function validateDiagnosis(diagnosis: Partial) { + const error: AddDiagnosisError = {} + + if (!diagnosis.name) { + error.name = 'patient.diagnoses.error.nameRequired' + } + + if (!diagnosis.diagnosisDate) { + error.diagnosisDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.onsetDate) { + error.onsetDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.abatementDate) { + error.abatementDate = 'patient.diagnoses.error.dateRequired' + } + + if (!diagnosis.status) { + error.status = 'patient.diagnoses.error.statusRequired' + } + return error +} diff --git a/src/patients/visits/VisitForm.tsx b/src/patients/visits/VisitForm.tsx index 57eda38b17..ebc38b2424 100644 --- a/src/patients/visits/VisitForm.tsx +++ b/src/patients/visits/VisitForm.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import useTranslator from '../../shared/hooks/useTranslator' diff --git a/src/scheduling/appointments/AppointmentDetailForm.tsx b/src/scheduling/appointments/AppointmentDetailForm.tsx index 03c4c87570..8a323f5271 100644 --- a/src/scheduling/appointments/AppointmentDetailForm.tsx +++ b/src/scheduling/appointments/AppointmentDetailForm.tsx @@ -4,7 +4,7 @@ import React from 'react' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import SelectWithLabelFormGroup, { Option, -} from '../../shared/components/input/SelectWithLableFormGroup' +} from '../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' diff --git a/src/shared/components/input/LanguageSelector.tsx b/src/shared/components/input/LanguageSelector.tsx index e44d943151..c3b7d8ba18 100644 --- a/src/shared/components/input/LanguageSelector.tsx +++ b/src/shared/components/input/LanguageSelector.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import i18n, { resources } from '../../config/i18n' import useTranslator from '../../hooks/useTranslator' -import SelectWithLabelFormGroup, { Option } from './SelectWithLableFormGroup' +import SelectWithLabelFormGroup, { Option } from './SelectWithLabelFormGroup' const LanguageSelector = () => { const { t } = useTranslator() diff --git a/src/shared/components/input/SelectWithLableFormGroup.tsx b/src/shared/components/input/SelectWithLabelFormGroup.tsx similarity index 100% rename from src/shared/components/input/SelectWithLableFormGroup.tsx rename to src/shared/components/input/SelectWithLabelFormGroup.tsx diff --git a/src/shared/components/input/index.tsx b/src/shared/components/input/index.tsx new file mode 100644 index 0000000000..679ac9f14b --- /dev/null +++ b/src/shared/components/input/index.tsx @@ -0,0 +1,17 @@ +import DatePickerWithLabelFormGroup from './DatePickerWithLabelFormGroup' +import DateTimePickerWithLabelFromGroup from './DateTimePickerWithLabelFormGroup' +import LanguageSelector from './LanguageSelector' +import SelectWithLabelFormGroup, { Option } from './SelectWithLabelFormGroup' +import TextFieldWithLabelFormGroup from './TextFieldWithLabelFormGroup' +import TextInputWithLabelFormGroup from './TextInputWithLabelFormGroup' + +export type { Option } + +export default { + DatePickerWithLabelFormGroup, + DateTimePickerWithLabelFromGroup, + LanguageSelector, + SelectWithLabelFormGroup, + TextFieldWithLabelFormGroup, + TextInputWithLabelFormGroup, +} From 812fe6a0f17b68e99d2cce9cd86e9d6da79ccec8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 20 Sep 2020 11:03:38 +0000 Subject: [PATCH 83/86] build(deps-dev): bump ts-jest from 26.3.0 to 26.4.0 (#2414) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3caa4e1731..2bd2bddbbc 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "rimraf": "~3.0.2", "source-map-explorer": "^2.2.2", "standard-version": "~9.0.0", - "ts-jest": "~26.3.0" + "ts-jest": "~26.4.0" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", From a7f771f693f124ecadc5f7f5eaf257ccf367563c Mon Sep 17 00:00:00 2001 From: Bereket Semagn Date: Tue, 22 Sep 2020 21:54:16 -0400 Subject: [PATCH 84/86] feat: Added smooth scroll to index.css (#2408) --- src/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/index.css b/src/index.css index bd5338ff8f..f0874932d2 100644 --- a/src/index.css +++ b/src/index.css @@ -1,3 +1,7 @@ +html { + scroll-behavior: smooth; +} + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', From 6cbeae549c3baab02f3bcb842cf829a0ba76a3e6 Mon Sep 17 00:00:00 2001 From: Blessed Tabvirwa Date: Wed, 23 Sep 2020 04:20:22 +0200 Subject: [PATCH 85/86] refactor(page-header): move title functionality to use a hooks --- src/App.tsx | 5 +- src/HospitalRun.tsx | 3 +- src/__tests__/HospitalRun.test.tsx | 254 ++++------------- src/__tests__/imagings/Imagings.test.tsx | 68 +++-- .../requests/NewImagingRequest.test.tsx | 17 +- .../imagings/search/ViewImagings.test.tsx | 105 ------- src/__tests__/incidents/Incidents.test.tsx | 15 +- .../incidents/list/ViewIncidents.test.tsx | 22 +- .../incidents/report/ReportIncident.test.tsx | 19 +- .../incidents/view/ViewIncident.test.tsx | 19 +- .../view/ViewIncidentDetails.test.tsx | 2 - src/__tests__/labs/Labs.test.tsx | 122 +++------ src/__tests__/labs/ViewLab.test.tsx | 23 +- src/__tests__/labs/ViewLabs.test.tsx | 256 ++++++------------ .../labs/requests/NewLabRequest.test.tsx | 125 +++------ .../medications/Medications.test.tsx | 5 +- .../medications/ViewMedication.test.tsx | 72 +++-- .../requests/NewMedicationRequest.test.tsx | 108 +++----- .../search/ViewMedications.test.tsx | 14 +- .../page-header/title/TitleProvider.test.tsx | 38 +++ .../page-header/title/title-slice.test.ts | 34 --- .../page-header/title/useTitle.test.tsx | 24 -- src/__tests__/patients/Patients.test.tsx | 31 ++- .../patients/edit/EditPatient.test.tsx | 25 +- .../patients/new/NewPatient.test.tsx | 17 +- .../patients/search/ViewPatients.test.tsx | 5 +- .../patients/view/ViewPatient.test.tsx | 17 +- .../appointments/Appointments.test.tsx | 182 +++++++------ .../appointments/ViewAppointments.test.tsx | 14 +- .../edit/EditAppointment.test.tsx | 60 ++-- .../appointments/new/NewAppointment.test.tsx | 13 +- .../view/ViewAppointment.test.tsx | 13 +- src/__tests__/settings/Settings.test.tsx | 13 +- src/dashboard/Dashboard.tsx | 5 +- src/imagings/requests/NewImagingRequest.tsx | 5 +- src/imagings/search/ViewImagings.tsx | 5 +- src/incidents/list/ViewIncidents.tsx | 5 +- src/incidents/report/ReportIncident.tsx | 5 +- src/incidents/view/ViewIncident.tsx | 9 +- .../visualize/VisualizeIncidents.tsx | 6 +- src/labs/ViewLab.tsx | 5 +- src/labs/ViewLabs.tsx | 5 +- src/labs/requests/NewLabRequest.tsx | 5 +- src/medications/ViewMedication.tsx | 5 +- .../requests/NewMedicationRequest.tsx | 5 +- src/medications/search/ViewMedications.tsx | 5 +- src/page-header/title/TitleContext.tsx | 31 +++ src/page-header/title/title-slice.ts | 29 -- src/page-header/title/useTitle.tsx | 12 - src/patients/edit/EditPatient.tsx | 5 +- src/patients/new/NewPatient.tsx | 5 +- src/patients/search/ViewPatients.tsx | 5 +- src/patients/view/ViewPatient.tsx | 5 +- .../appointments/ViewAppointments.tsx | 5 +- .../appointments/edit/EditAppointment.tsx | 5 +- .../appointments/new/NewAppointment.tsx | 5 +- .../appointments/view/ViewAppointment.tsx | 5 +- src/settings/Settings.tsx | 5 +- src/shared/store/index.ts | 2 - 59 files changed, 758 insertions(+), 1171 deletions(-) delete mode 100644 src/__tests__/imagings/search/ViewImagings.test.tsx create mode 100644 src/__tests__/page-header/title/TitleProvider.test.tsx delete mode 100644 src/__tests__/page-header/title/title-slice.test.ts delete mode 100644 src/__tests__/page-header/title/useTitle.test.tsx create mode 100644 src/page-header/title/TitleContext.tsx delete mode 100644 src/page-header/title/title-slice.ts delete mode 100644 src/page-header/title/useTitle.tsx diff --git a/src/App.tsx b/src/App.tsx index 480178220e..0760e629c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom' import HospitalRun from './HospitalRun' import Login from './login/Login' +import { TitleProvider } from './page-header/title/TitleContext' import { remoteDb } from './shared/config/pouchdb' import { getCurrentSession } from './user/user-slice' @@ -41,7 +42,9 @@ const App: React.FC = () => { }> - + + + diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index f852aed4ac..31d8fe336e 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -11,6 +11,7 @@ import Medications from './medications/Medications' import Breadcrumbs from './page-header/breadcrumbs/Breadcrumbs' import { ButtonBarProvider } from './page-header/button-toolbar/ButtonBarProvider' import ButtonToolBar from './page-header/button-toolbar/ButtonToolBar' +import { useTitle } from './page-header/title/TitleContext' import Patients from './patients/Patients' import Appointments from './scheduling/appointments/Appointments' import Settings from './settings/Settings' @@ -20,7 +21,7 @@ import Sidebar from './shared/components/Sidebar' import { RootState } from './shared/store' const HospitalRun = () => { - const { title } = useSelector((state: RootState) => state.title) + const { title } = useTitle() const { sidebarCollapsed } = useSelector((state: RootState) => state.components) const { user } = useSelector((root: RootState) => root.user) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 28bb9bf9e5..326f14ccb7 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -1,5 +1,5 @@ import { Toaster } from '@hospitalrun/components' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' import { Provider } from 'react-redux' @@ -14,6 +14,7 @@ import Incidents from '../incidents/Incidents' import ViewLabs from '../labs/ViewLabs' import ViewMedications from '../medications/search/ViewMedications' import { addBreadcrumbs } from '../page-header/breadcrumbs/breadcrumbs-slice' +import * as titleUtil from '../page-header/title/TitleContext' import Appointments from '../scheduling/appointments/Appointments' import Settings from '../settings/Settings' import ImagingRepository from '../shared/db/ImagingRepository' @@ -23,31 +24,43 @@ import MedicationRepository from '../shared/db/MedicationRepository' import Permissions from '../shared/model/Permissions' import { RootState } from '../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('HospitalRun', () => { + const setup = async (route: string, permissions: Permissions[] = []) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + const store = mockStore({ + user: { user: { id: '123' }, permissions }, + appointments: { appointments: [] }, + medications: { medications: [] }, + labs: { labs: [] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + const wrapper = mount( + + + + + + + , + ) + + await act(async () => { + wrapper.update() + }) + + return { wrapper: wrapper as ReactWrapper, store: store as any } + } + describe('routing', () => { describe('/appointments', () => { it('should render the appointments screen when /appointments is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ReadAppointments] }, - appointments: { appointments: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) - - await act(async () => { - wrapper.update() - }) + const permissions: Permissions[] = [Permissions.ReadAppointments] + const { wrapper, store } = await setup('/appointments', permissions) expect(wrapper.find(Appointments)).toHaveLength(1) @@ -59,22 +72,8 @@ describe('HospitalRun', () => { ) }) - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) - + it('should render the Dashboard when the user does not have read appointment privileges', async () => { + const { wrapper } = await setup('/appointments') expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) @@ -82,45 +81,15 @@ describe('HospitalRun', () => { describe('/labs', () => { it('should render the Labs component when /labs is accessed', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewLabs] }, - labs: { labs: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewLabs] + const { wrapper } = await setup('/labs', permissions) expect(wrapper.find(ViewLabs)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view labs', () => { + it('should render the dashboard if the user does not have permissions to view labs', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = await setup('/labs') expect(wrapper.find(ViewLabs)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -130,44 +99,15 @@ describe('HospitalRun', () => { describe('/medications', () => { it('should render the Medications component when /medications is accessed', async () => { jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewMedications] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewMedications] + const { wrapper } = await setup('/medications', permissions) expect(wrapper.find(ViewMedications)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view medications', () => { + it('should render the dashboard if the user does not have permissions to view medications', async () => { jest.spyOn(MedicationRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = await setup('/medications') expect(wrapper.find(ViewMedications)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -177,44 +117,15 @@ describe('HospitalRun', () => { describe('/incidents', () => { it('should render the Incidents component when /incidents is accessed', async () => { jest.spyOn(IncidentRepository, 'search').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewIncidents] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewIncidents] + const { wrapper } = await setup('/incidents', permissions) expect(wrapper.find(Incidents)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view incidents', () => { + it('should render the dashboard if the user does not have permissions to view incidents', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = await setup('/incidents') expect(wrapper.find(Incidents)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -224,44 +135,15 @@ describe('HospitalRun', () => { describe('/imaging', () => { it('should render the Imagings component when /imaging is accessed', async () => { jest.spyOn(ImagingRepository, 'search').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [Permissions.ViewImagings] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - wrapper.update() + const permissions: Permissions[] = [Permissions.ViewImagings] + const { wrapper } = await setup('/imaging', permissions) expect(wrapper.find(ViewImagings)).toHaveLength(1) }) - it('should render the dashboard if the user does not have permissions to view imagings', () => { + it('should render the dashboard if the user does not have permissions to view imagings', async () => { jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = await setup('/imaging') expect(wrapper.find(ViewImagings)).toHaveLength(0) expect(wrapper.find(Dashboard)).toHaveLength(1) @@ -270,42 +152,16 @@ describe('HospitalRun', () => { describe('/settings', () => { it('should render the Settings component when /settings is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { user: { id: '123' }, permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) - + const { wrapper } = await setup('/settings') expect(wrapper.find(Settings)).toHaveLength(1) }) }) }) describe('layout', () => { - it('should render a Toaster', () => { - const wrapper = mount( - - - - - , - ) + it('should render a Toaster', async () => { + const permissions: Permissions[] = [Permissions.WritePatients] + const { wrapper } = await setup('/', permissions) expect(wrapper.find(Toaster)).toHaveLength(1) }) diff --git a/src/__tests__/imagings/Imagings.test.tsx b/src/__tests__/imagings/Imagings.test.tsx index eb3b03db63..6b1865dc0b 100644 --- a/src/__tests__/imagings/Imagings.test.tsx +++ b/src/__tests__/imagings/Imagings.test.tsx @@ -1,4 +1,4 @@ -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' @@ -7,6 +7,7 @@ import thunk from 'redux-thunk' import Imagings from '../../imagings/Imagings' import NewImagingRequest from '../../imagings/requests/NewImagingRequest' +import * as titleUtil from '../../page-header/title/TitleContext' import ImagingRepository from '../../shared/db/ImagingRepository' import PatientRepository from '../../shared/db/PatientRepository' import Imaging from '../../shared/model/Imaging' @@ -14,9 +15,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Imagings', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ImagingRepository, 'findAll').mockResolvedValue([]) jest .spyOn(ImagingRepository, 'find') @@ -24,48 +27,41 @@ describe('Imagings', () => { jest .spyOn(PatientRepository, 'find') .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + const setup = (permissions: Permissions[], isNew = false) => { + const store = mockStore({ + user: { permissions: [permissions] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + imaging: { + imaging: { id: 'imagingId', patient: 'patient' } as Imaging, + patient: { id: 'patientId', fullName: 'some name' }, + error: {}, + }, + } as any) + + const wrapper = mount( + + + {isNew ? : } + + , + ) + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } describe('routing', () => { describe('/imaging/new', () => { - it('should render the new imaging request screen when /imaging/new is accessed', () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.RequestImaging] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - imaging: { - imaging: { id: 'imagingId', patient: 'patient' } as Imaging, - patient: { id: 'patientId', fullName: 'some name' }, - error: {}, - }, - } as any) - - const wrapper = mount( - - - - - , - ) + it('should render the new imaging request screen when /imaging/new is accessed', async () => { + const permissions: Permissions[] = [Permissions.RequestImaging] + const { wrapper } = setup(permissions, true) expect(wrapper.find(NewImagingRequest)).toHaveLength(1) }) - it('should not navigate to /imagings/new if the user does not have RequestLab permissions', () => { - const store = mockStore({ - title: 'test', - user: { permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + it('should not navigate to /imagings/new if the user does not have RequestImaging permissions', async () => { + const permissions: Permissions[] = [] + const { wrapper } = setup(permissions) expect(wrapper.find(NewImagingRequest)).toHaveLength(0) }) diff --git a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx index 59cced4656..b5abe42662 100644 --- a/src/__tests__/imagings/requests/NewImagingRequest.test.tsx +++ b/src/__tests__/imagings/requests/NewImagingRequest.test.tsx @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import NewImagingRequest from '../../../imagings/requests/NewImagingRequest' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import SelectWithLabelFormGroup from '../../../shared/components/input/SelectWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' @@ -30,14 +30,12 @@ describe('New Imaging Request', () => { jest.resetAllMocks() jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) history = createMemoryHistory() history.push(`/imaging/new`) - const store = mockStore({ - title: '', - } as any) + const store = mockStore({} as any) let wrapper: any await act(async () => { @@ -46,21 +44,24 @@ describe('New Imaging Request', () => { - + + + , ) }) + wrapper.find(NewImagingRequest).props().updateTitle = jest.fn() wrapper.update() return wrapper as ReactWrapper } describe('title and breadcrumbs', () => { - it('should have New Imaging Request as the title', async () => { + it('should have called the useUpdateTitle hook', async () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith('imagings.requests.new') + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) }) diff --git a/src/__tests__/imagings/search/ViewImagings.test.tsx b/src/__tests__/imagings/search/ViewImagings.test.tsx deleted file mode 100644 index 51a02b753d..0000000000 --- a/src/__tests__/imagings/search/ViewImagings.test.tsx +++ /dev/null @@ -1,105 +0,0 @@ -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 { Route, Router } from 'react-router-dom' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' - -import ImagingRequestTable from '../../../imagings/search/ImagingRequestTable' -import ViewImagings from '../../../imagings/search/ViewImagings' -import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' -import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' -import ImagingRepository from '../../../shared/db/ImagingRepository' -import Imaging from '../../../shared/model/Imaging' -import Permissions from '../../../shared/model/Permissions' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('View Imagings', () => { - let history: any - let setButtonToolBarSpy: any - const expectedDate = new Date(2020, 5, 3, 19, 48) - const expectedImaging = { - code: 'I-1234', - id: '1234', - type: 'imaging type', - patient: 'patient', - fullName: 'full name', - status: 'requested', - requestedOn: expectedDate.toISOString(), - requestedBy: 'some user', - } as Imaging - - const setup = async (permissions: Permissions[], mockImagings?: any) => { - jest.resetAllMocks() - jest.spyOn(breadcrumbUtil, 'default') - setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') - jest.spyOn(ImagingRepository, 'search').mockResolvedValue(mockImagings) - jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) - - history = createMemoryHistory() - history.push(`/imaging`) - - const store = mockStore({ - title: '', - user: { permissions }, - } as any) - - let wrapper: any - await act(async () => { - wrapper = await mount( - - - - - - - - - , - ) - }) - wrapper.update() - - return wrapper as ReactWrapper - } - - describe('title', () => { - it('should have the title', async () => { - await setup([Permissions.ViewImagings], []) - expect(titleUtil.default).toHaveBeenCalledWith('imagings.label') - }) - }) - - describe('button bar', () => { - it('should display button to add new imaging request', async () => { - await setup([Permissions.ViewImagings, Permissions.RequestImaging], []) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect((actualButtons[0] as any).props.children).toEqual('imagings.requests.new') - }) - - it('should not display button to add new imaging request if the user does not have permissions', async () => { - await setup([Permissions.ViewImagings], []) - - const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect(actualButtons).toEqual([]) - }) - }) - - describe('table', () => { - it('should render a table with data', async () => { - const wrapper = await setup( - [Permissions.ViewIncident, Permissions.RequestImaging], - [expectedImaging], - ) - - expect(wrapper.exists(ImagingRequestTable)) - }) - }) -}) diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx index 43a32abfbe..07f73e607b 100644 --- a/src/__tests__/incidents/Incidents.test.tsx +++ b/src/__tests__/incidents/Incidents.test.tsx @@ -10,6 +10,7 @@ import Incidents from '../../incidents/Incidents' import ReportIncident from '../../incidents/report/ReportIncident' import ViewIncident from '../../incidents/view/ViewIncident' import VisualizeIncidents from '../../incidents/visualize/VisualizeIncidents' +import * as titleUtil from '../../page-header/title/TitleContext' import IncidentRepository from '../../shared/db/IncidentRepository' import Incident from '../../shared/model/Incident' import Permissions from '../../shared/model/Permissions' @@ -25,10 +26,10 @@ describe('Incidents', () => { date: new Date().toISOString(), reportedOn: new Date().toISOString(), } as Incident + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(IncidentRepository, 'search').mockResolvedValue([]) jest.spyOn(IncidentRepository, 'find').mockResolvedValue(expectedIncident) const store = mockStore({ - title: 'test', user: { permissions }, breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, @@ -39,16 +40,26 @@ describe('Incidents', () => { wrapper = await mount( - + + + , ) }) + wrapper.find(Incidents).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } } + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + await setup([Permissions.ViewIncidents], '/incidents') + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) + }) + }) + describe('routing', () => { describe('/incidents/new', () => { it('should render the new incident screen when /incidents/new is accessed', async () => { diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx index e67c544f5f..db356f4074 100644 --- a/src/__tests__/incidents/list/ViewIncidents.test.tsx +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -12,7 +12,7 @@ import ViewIncidents from '../../../incidents/list/ViewIncidents' import ViewIncidentsTable from '../../../incidents/list/ViewIncidentsTable' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -39,7 +39,7 @@ describe('View Incidents', () => { jest.resetAllMocks() jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(IncidentRepository, 'findAll').mockResolvedValue(expectedIncidents) jest.spyOn(IncidentRepository, 'search').mockResolvedValue(expectedIncidents) @@ -47,7 +47,6 @@ describe('View Incidents', () => { history = createMemoryHistory() history.push(`/incidents`) const store = mockStore({ - title: '', user: { permissions, }, @@ -60,16 +59,25 @@ describe('View Incidents', () => { - + + + , ) }) + wrapper.find(ViewIncidents).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } } + + it('should have called the useUpdateTitle hook', async () => { + await setup([Permissions.ViewIncidents]) + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) + }) + it('should filter incidents by status=reported on first load ', async () => { await setup([Permissions.ViewIncidents]) @@ -78,12 +86,6 @@ describe('View Incidents', () => { }) describe('layout', () => { - it('should set the title', async () => { - await setup([Permissions.ViewIncidents]) - - expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label') - }) - it('should render a table with the incidents', async () => { const { wrapper } = await setup([Permissions.ViewIncidents]) const table = wrapper.find(ViewIncidentsTable) diff --git a/src/__tests__/incidents/report/ReportIncident.test.tsx b/src/__tests__/incidents/report/ReportIncident.test.tsx index 4e3c2ee2da..6e16c55f8d 100644 --- a/src/__tests__/incidents/report/ReportIncident.test.tsx +++ b/src/__tests__/incidents/report/ReportIncident.test.tsx @@ -14,7 +14,7 @@ import ReportIncident from '../../../incidents/report/ReportIncident' import * as validationUtil from '../../../incidents/util/validate-incident' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -34,13 +34,12 @@ describe('Report Incident', () => { const setup = async (permissions: Permissions[]) => { jest.spyOn(breadcrumbUtil, 'default') setButtonToolBarSpy = jest.fn() - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) history = createMemoryHistory() history.push(`/incidents/new`) const store = mockStore({ - title: '', user: { permissions, user: { @@ -56,24 +55,28 @@ describe('Report Incident', () => { - + + + , ) }) + wrapper.find(ReportIncident).props().updateTitle = jest.fn() wrapper.update() return wrapper as ReactWrapper } - describe('layout', () => { - it('should set the title', async () => { + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { await setup([Permissions.ReportIncident]) - - expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.new') + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) + }) + describe('layout', () => { it('should set the breadcrumbs properly', async () => { await setup([Permissions.ReportIncident]) diff --git a/src/__tests__/incidents/view/ViewIncident.test.tsx b/src/__tests__/incidents/view/ViewIncident.test.tsx index 3d944ee096..b0706f7eb9 100644 --- a/src/__tests__/incidents/view/ViewIncident.test.tsx +++ b/src/__tests__/incidents/view/ViewIncident.test.tsx @@ -11,19 +11,20 @@ import ViewIncident from '../../../incidents/view/ViewIncident' import ViewIncidentDetails from '../../../incidents/view/ViewIncidentDetails' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('View Incident', () => { const setup = async (permissions: Permissions[]) => { jest.resetAllMocks() + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(breadcrumbUtil, 'default') - jest.spyOn(titleUtil, 'default') jest.spyOn(IncidentRepository, 'find').mockResolvedValue({ id: '1234', date: new Date().toISOString(), @@ -34,7 +35,6 @@ describe('View Incident', () => { history.push(`/incidents/1234`) const store = mockStore({ - title: '', user: { permissions, }, @@ -47,7 +47,9 @@ describe('View Incident', () => { - + + + @@ -58,18 +60,11 @@ describe('View Incident', () => { return { wrapper: wrapper as ReactWrapper, history } } - it('should render the title correctly', async () => { - await setup([Permissions.ViewIncident]) - - expect(titleUtil.default).toHaveBeenCalledTimes(1) - expect(titleUtil.default).toHaveBeenCalledWith('View Incident') - }) - it('should set the breadcrumbs properly', async () => { await setup([Permissions.ViewIncident]) expect(breadcrumbUtil.default).toHaveBeenCalledWith([ - { i18nKey: 'View Incident', location: '/incidents/1234' }, + { i18nKey: 'incidents.reports.view', location: '/incidents/1234' }, ]) }) diff --git a/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx b/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx index b00faa0f78..69619d1626 100644 --- a/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx +++ b/src/__tests__/incidents/view/ViewIncidentDetails.test.tsx @@ -8,7 +8,6 @@ import { Router } from 'react-router' import ViewIncidentDetails from '../../../incidents/view/ViewIncidentDetails' import * as breadcrumbUtil from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' import IncidentRepository from '../../../shared/db/IncidentRepository' import Incident from '../../../shared/model/Incident' import Permissions from '../../../shared/model/Permissions' @@ -35,7 +34,6 @@ describe('View Incident Details', () => { jest.resetAllMocks() Date.now = jest.fn(() => expectedResolveDate.valueOf()) jest.spyOn(breadcrumbUtil, 'default') - jest.spyOn(titleUtil, 'default') jest.spyOn(IncidentRepository, 'find').mockResolvedValue(mockIncident) incidentRepositorySaveSpy = jest .spyOn(IncidentRepository, 'saveOrUpdate') diff --git a/src/__tests__/labs/Labs.test.tsx b/src/__tests__/labs/Labs.test.tsx index bddfeb3387..e8254db294 100644 --- a/src/__tests__/labs/Labs.test.tsx +++ b/src/__tests__/labs/Labs.test.tsx @@ -1,5 +1,4 @@ -import { act } from '@testing-library/react' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { Provider } from 'react-redux' import { MemoryRouter } from 'react-router-dom' @@ -9,6 +8,7 @@ import thunk from 'redux-thunk' import Labs from '../../labs/Labs' import NewLabRequest from '../../labs/requests/NewLabRequest' import ViewLab from '../../labs/ViewLab' +import * as titleUtil from '../../page-header/title/TitleContext' import LabRepository from '../../shared/db/LabRepository' import PatientRepository from '../../shared/db/PatientRepository' import Lab from '../../shared/model/Lab' @@ -16,9 +16,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Labs', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) jest .spyOn(LabRepository, 'find') @@ -26,48 +28,46 @@ describe('Labs', () => { jest .spyOn(PatientRepository, 'find') .mockResolvedValue({ id: '12345', fullName: 'test test' } as Patient) + const setup = (route: string, permissions: Permissions[] = []) => { + const store = mockStore({ + user: { permissions }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + lab: { + lab: ({ + id: '1234', + patientId: 'patientId', + requestedOn: new Date().toISOString(), + } as unknown) as Lab, + patient: { id: 'patientId', fullName: 'some name' }, + error: {}, + }, + } as any) + + const wrapper = mount( + + + + + + + , + ) + wrapper.update() + return { wrapper: wrapper as ReactWrapper } + } describe('routing', () => { describe('/labs/new', () => { it('should render the new lab request screen when /labs/new is accessed', () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.RequestLab] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - lab: { - lab: { id: 'labId', patient: 'patientId' } as Lab, - patient: { id: 'patientId', fullName: 'some name' }, - error: {}, - }, - } as any) - - const wrapper = mount( - - - - - , - ) + const permissions: Permissions[] = [Permissions.RequestLab] + const { wrapper } = setup('/labs/new', permissions) expect(wrapper.find(NewLabRequest)).toHaveLength(1) }) it('should not navigate to /labs/new if the user does not have RequestLab permissions', () => { - const store = mockStore({ - title: 'test', - user: { permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = mount( - - - - - , - ) + const { wrapper } = setup('/labs/new') expect(wrapper.find(NewLabRequest)).toHaveLength(0) }) @@ -75,55 +75,17 @@ describe('Labs', () => { describe('/labs/:id', () => { it('should render the view lab screen when /labs/:id is accessed', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [Permissions.ViewLab] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - lab: { - lab: { - id: 'labId', - patient: 'patientId', - requestedOn: new Date().toISOString(), - } as Lab, - patient: { id: 'patientId', fullName: 'some name' }, - error: {}, - }, - } as any) - - let wrapper: any - - await act(async () => { - wrapper = await mount( - - - - - , - ) + const permissions: Permissions[] = [Permissions.ViewLab] + const { wrapper } = setup('/labs/1234', permissions) - expect(wrapper.find(ViewLab)).toHaveLength(1) - }) + expect(wrapper.find(ViewLab)).toHaveLength(1) }) + }) - it('should not navigate to /labs/:id if the user does not have ViewLab permissions', async () => { - const store = mockStore({ - title: 'test', - user: { permissions: [] }, - breadcrumbs: { breadcrumbs: [] }, - components: { sidebarCollapsed: false }, - } as any) - - const wrapper = await mount( - - - - - , - ) + it('should not navigate to /labs/:id if the user does not have ViewLab permissions', async () => { + const { wrapper } = setup('/labs/1234') - expect(wrapper.find(ViewLab)).toHaveLength(0) - }) + expect(wrapper.find(ViewLab)).toHaveLength(0) }) }) }) diff --git a/src/__tests__/labs/ViewLab.test.tsx b/src/__tests__/labs/ViewLab.test.tsx index 488464022f..baa9019ac5 100644 --- a/src/__tests__/labs/ViewLab.test.tsx +++ b/src/__tests__/labs/ViewLab.test.tsx @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import ViewLab from '../../labs/ViewLab' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import LabRepository from '../../shared/db/LabRepository' import PatientRepository from '../../shared/db/PatientRepository' @@ -36,14 +36,13 @@ describe('View Lab', () => { } as Lab let setButtonToolBarSpy: any - let titleSpy: any let labRepositorySaveSpy: any const expectedDate = new Date() const setup = async (lab: Lab, permissions: Permissions[], error = {}) => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) setButtonToolBarSpy = jest.fn() - titleSpy = jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'find').mockResolvedValue(lab) labRepositorySaveSpy = jest.spyOn(LabRepository, 'saveOrUpdate').mockResolvedValue(mockLab) @@ -52,7 +51,6 @@ describe('View Lab', () => { history = createMemoryHistory() history.push(`labs/${lab.id}`) const store = mockStore({ - title: '', user: { permissions, }, @@ -71,23 +69,26 @@ describe('View Lab', () => { - + + + , ) }) + wrapper.find(ViewLab).props().updateTitle = jest.fn() wrapper.update() return wrapper } - it('should set the title', async () => { - await setup(mockLab, [Permissions.ViewLab]) - - expect(titleSpy).toHaveBeenCalledWith( - `${mockLab.type} for ${mockPatient.fullName}(${mockLab.code})`, - ) + describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + const expectedLab = { ...mockLab } as Lab + await setup(expectedLab, [Permissions.ViewLab]) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() + }) }) describe('page content', () => { diff --git a/src/__tests__/labs/ViewLabs.test.tsx b/src/__tests__/labs/ViewLabs.test.tsx index fefdea666f..9a14691b4b 100644 --- a/src/__tests__/labs/ViewLabs.test.tsx +++ b/src/__tests__/labs/ViewLabs.test.tsx @@ -1,4 +1,4 @@ -import { TextInput, Select, Table } from '@hospitalrun/components' +import { Select, Table } from '@hospitalrun/components' import { act } from '@testing-library/react' import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import * as labsSlice from '../../labs/labs-slice' import ViewLabs from '../../labs/ViewLabs' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import LabRepository from '../../shared/db/LabRepository' import Lab from '../../shared/model/Lab' import Permissions from '../../shared/model/Permissions' @@ -19,75 +19,65 @@ import { RootState } from '../../shared/store' const mockStore = createMockStore([thunk]) -describe('View Labs', () => { - describe('title', () => { - let titleSpy: any - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [] }, - } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) - }) - - it('should have the title', () => { - expect(titleSpy).toHaveBeenCalledWith('labs.label') - }) +let history: any +const expectedLab = { + code: 'L-1234', + id: '1234', + type: 'lab type', + patient: 'patientId', + status: 'requested', + requestedOn: new Date().toISOString(), +} as Lab + +const setup = (permissions: Permissions[] = [Permissions.ViewLabs, Permissions.RequestLab]) => { + const store = mockStore({ + user: { permissions }, + labs: { labs: [expectedLab] }, + } as any) + history = createMemoryHistory() + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) + + const wrapper = mount( + + + + + + + , + ) + + wrapper.find(ViewLabs).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper: wrapper as ReactWrapper } +} + +describe('title', () => { + it('should have called the useUpdateTitle hook', async () => { + setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) +}) +describe('View Labs', () => { describe('button bar', () => { it('should display button to add new lab request', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [] }, - } as any) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + setup() const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect((actualButtons[0] as any).props.children).toEqual('labs.requests.new') }) it('should not display button to add new lab request if the user does not have permissions', async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs] }, - labs: { labs: [] }, - } as any) const setButtonToolBarSpy = jest.fn() jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) - await act(async () => { - await mount( - - - - - , - ) - }) + setup([]) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] expect(actualButtons).toEqual([]) @@ -95,40 +85,8 @@ describe('View Labs', () => { }) describe('table', () => { - let wrapper: ReactWrapper - let history: any - const expectedLab = { - code: 'L-1234', - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab - - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() - - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - wrapper.update() - }) - it('should render a table with data', () => { + const { wrapper } = setup() const table = wrapper.find(Table) const columns = table.prop('columns') const actions = table.prop('actions') as any @@ -147,6 +105,7 @@ describe('View Labs', () => { }) it('should navigate to the lab when the view button is clicked', () => { + const { wrapper } = setup() const tr = wrapper.find('tr').at(1) act(() => { @@ -160,110 +119,53 @@ describe('View Labs', () => { describe('dropdown', () => { it('should search for labs when dropdown changes', () => { const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - let wrapper: ReactWrapper - let history: any - const expectedLab = { - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab + const { wrapper } = setup() - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() + searchLabsSpy.mockClear() - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) - - searchLabsSpy.mockClear() - - act(() => { - const onChange = wrapper.find(Select).prop('onChange') as any - onChange({ - target: { - value: 'requested', - }, - preventDefault: jest.fn(), - }) + act(() => { + const onChange = wrapper.find(Select).prop('onChange') as any + onChange({ + target: { + value: 'requested', + }, + preventDefault: jest.fn(), }) - - wrapper.update() - expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) + + wrapper.update() + expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) }) +}) - describe('search functionality', () => { - beforeEach(() => jest.useFakeTimers()) - - afterEach(() => jest.useRealTimers()) +describe('search functionality', () => { + beforeEach(() => jest.useFakeTimers()) - it('should search for labs after the search text has not changed for 500 milliseconds', () => { - const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - let wrapper: ReactWrapper - let history: any - const expectedLab = { - id: '1234', - type: 'lab type', - patient: 'patientId', - status: 'requested', - requestedOn: '2020-03-30T04:43:20.102Z', - } as Lab + afterEach(() => jest.useRealTimers()) - beforeEach(async () => { - const store = mockStore({ - title: '', - user: { permissions: [Permissions.ViewLabs, Permissions.RequestLab] }, - labs: { labs: [expectedLab] }, - } as any) - history = createMemoryHistory() + it('should search for labs after the search text has not changed for 500 milliseconds', async () => { + const searchLabsSpy = jest.spyOn(labsSlice, 'searchLabs') - jest.spyOn(LabRepository, 'findAll').mockResolvedValue([expectedLab]) - await act(async () => { - wrapper = await mount( - - - - - , - ) - }) + searchLabsSpy.mockClear() - searchLabsSpy.mockClear() - const expectedSearchText = 'search text' + beforeEach(async () => { + const { wrapper } = setup() - act(() => { - const onClick = wrapper.find(TextInput).prop('onChange') as any - onClick({ - target: { - value: expectedSearchText, - }, - preventDefault: jest.fn(), - }) - }) + searchLabsSpy.mockClear() - act(() => { - jest.advanceTimersByTime(500) + act(() => { + const onChange = wrapper.find(Select).prop('onChange') as any + onChange({ + target: { + value: 'requested', + }, + preventDefault: jest.fn(), }) - - wrapper.update() - - expect(searchLabsSpy).toHaveBeenCalledTimes(1) - expect(searchLabsSpy).toHaveBeenLastCalledWith(expectedSearchText) }) + + wrapper.update() + expect(searchLabsSpy).toHaveBeenCalledTimes(1) }) }) }) diff --git a/src/__tests__/labs/requests/NewLabRequest.test.tsx b/src/__tests__/labs/requests/NewLabRequest.test.tsx index 83ce663649..a9175944b3 100644 --- a/src/__tests__/labs/requests/NewLabRequest.test.tsx +++ b/src/__tests__/labs/requests/NewLabRequest.test.tsx @@ -9,7 +9,7 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import NewLabRequest from '../../../labs/requests/NewLabRequest' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import LabRepository from '../../../shared/db/LabRepository' @@ -21,47 +21,34 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('New Lab Request', () => { - describe('title and breadcrumbs', () => { - let titleSpy: any + const setup = async (store = mockStore({ lab: { status: 'loading', error: {} } } as any)) => { const history = createMemoryHistory() + history.push(`/labs/new`) + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) - beforeEach(() => { - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - history.push('/labs/new') - - mount( - - + const wrapper: ReactWrapper = await mount( + + + - - , - ) - }) + + + , + ) - it('should have New Lab Request as the title', () => { - expect(titleSpy).toHaveBeenCalledWith('labs.requests.new') - }) - }) + wrapper.find(NewLabRequest).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper } + } describe('form layout', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - history.push('/labs/new') - - wrapper = mount( - - - - - , - ) + it('should have called the useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) - it('should render a patient typeahead', () => { + it('should render a patient typeahead', async () => { + const { wrapper } = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -76,7 +63,8 @@ describe('New Lab Request', () => { expect(typeahead.prop('searchAccessor')).toEqual('fullName') }) - it('should render a type input box', () => { + it('should render a type input box', async () => { + const { wrapper } = await setup() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup) expect(typeInputBox).toBeDefined() @@ -85,7 +73,8 @@ describe('New Lab Request', () => { expect(typeInputBox.prop('isEditable')).toBeTruthy() }) - it('should render a notes text field', () => { + it('should render a notes text field', async () => { + const { wrapper } = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -94,41 +83,31 @@ describe('New Lab Request', () => { expect(notesTextField.prop('isEditable')).toBeTruthy() }) - it('should render a save button', () => { + it('should render a save button', async () => { + const { wrapper } = await setup() const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('labs.requests.save') }) - it('should render a cancel button', () => { + it('should render a cancel button', async () => { + const { wrapper } = await setup() const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') }) }) - describe('errors', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() + describe('errors', async () => { const error = { message: 'some message', patient: 'some patient message', type: 'some type error', } + const store = mockStore({ lab: { status: 'error', error } } as any) + const { wrapper } = await setup(store) - beforeEach(() => { - history.push('/labs/new') - const store = mockStore({ title: '', lab: { status: 'error', error } } as any) - wrapper = mount( - - - - - , - ) - }) - - it('should display errors', () => { + it('should display errors', async () => { const alert = wrapper.find(Alert) const typeInput = wrapper.find(TextInputWithLabelFormGroup) const patientTypeahead = wrapper.find(Typeahead) @@ -144,21 +123,10 @@ describe('New Lab Request', () => { }) }) - describe('on cancel', () => { - let wrapper: ReactWrapper + describe('on cancel', async () => { const history = createMemoryHistory() - beforeEach(() => { - history.push('/labs/new') - const store = mockStore({ title: '', lab: { status: 'loading', error: {} } } as any) - wrapper = mount( - - - - - , - ) - }) + const { wrapper } = await setup() it('should navigate back to /labs', () => { const cancelButton = wrapper.find(Button).at(1) @@ -172,8 +140,7 @@ describe('New Lab Request', () => { }) }) - describe('on save', () => { - let wrapper: ReactWrapper + describe('on save', async () => { const history = createMemoryHistory() let labRepositorySaveSpy: any const expectedDate = new Date() @@ -186,7 +153,11 @@ describe('New Lab Request', () => { id: '1234', requestedOn: expectedDate.toISOString(), } as Lab - + const store = mockStore({ + lab: { status: 'loading', error: {} }, + user: { user: { id: 'fake id' } }, + } as any) + const { wrapper } = await setup(store) beforeEach(() => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) @@ -195,20 +166,6 @@ describe('New Lab Request', () => { jest .spyOn(PatientRepository, 'search') .mockResolvedValue([{ id: expectedLab.patient, fullName: 'some full name' }] as Patient[]) - - history.push('/labs/new') - const store = mockStore({ - title: '', - lab: { status: 'loading', error: {} }, - user: { user: { id: 'fake id' } }, - } as any) - wrapper = mount( - - - - - , - ) }) it('should save the lab request and navigate to "/labs/:id"', async () => { diff --git a/src/__tests__/medications/Medications.test.tsx b/src/__tests__/medications/Medications.test.tsx index 6955d90f89..8bacbf0f05 100644 --- a/src/__tests__/medications/Medications.test.tsx +++ b/src/__tests__/medications/Medications.test.tsx @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Medications from '../../medications/Medications' import NewMedicationRequest from '../../medications/requests/NewMedicationRequest' import ViewMedication from '../../medications/ViewMedication' +import { TitleProvider } from '../../page-header/title/TitleContext' import MedicationRepository from '../../shared/db/MedicationRepository' import PatientRepository from '../../shared/db/PatientRepository' import Medication from '../../shared/model/Medication' @@ -54,7 +55,9 @@ describe('Medications', () => { const wrapper = mount( - + + + , ) diff --git a/src/__tests__/medications/ViewMedication.test.tsx b/src/__tests__/medications/ViewMedication.test.tsx index 399d51ef8a..65ccdf8bd5 100644 --- a/src/__tests__/medications/ViewMedication.test.tsx +++ b/src/__tests__/medications/ViewMedication.test.tsx @@ -11,7 +11,7 @@ import thunk from 'redux-thunk' import ViewMedication from '../../medications/ViewMedication' import * as ButtonBarProvider from '../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import MedicationRepository from '../../shared/db/MedicationRepository' import PatientRepository from '../../shared/db/PatientRepository' @@ -42,7 +42,7 @@ describe('View Medication', () => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) const setButtonToolBarSpy = jest.fn() - const titleSpy = jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) jest.spyOn(MedicationRepository, 'find').mockResolvedValue(medication) const medicationRepositorySaveSpy = jest @@ -72,37 +72,29 @@ describe('View Medication', () => { - + + + , ) }) + wrapper.find(ViewMedication).props().updateTitle = jest.fn() wrapper.update() - return [ + return { wrapper, mockPatient, - { ...mockMedication, ...medication }, - titleSpy, + expectedMedication: { ...mockMedication, ...medication }, medicationRepositorySaveSpy, history, - ] + } } - it('should set the title', async () => { - const [, mockPatient, mockMedication, titleSpy] = await setup({} as Medication, [ - Permissions.ViewMedication, - ]) - - expect(titleSpy).toHaveBeenCalledWith( - `${mockMedication.medication} for ${mockPatient.fullName}`, - ) - }) - describe('page content', () => { it('should display the patient full name for the for', async () => { - const [wrapper, mockPatient] = await setup({} as Medication, [Permissions.ViewMedication]) + const { wrapper, mockPatient } = await setup({} as Medication, [Permissions.ViewMedication]) const forPatientDiv = wrapper.find('.for-patient') expect(forPatientDiv.find('h4').text().trim()).toEqual('medications.medication.for') @@ -110,7 +102,7 @@ describe('View Medication', () => { }) it('should display the medication ', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const medicationTypeDiv = wrapper.find('.medication-medication') @@ -122,7 +114,7 @@ describe('View Medication', () => { }) it('should display the requested on date', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const requestedOnDiv = wrapper.find('.requested-on') @@ -134,14 +126,14 @@ describe('View Medication', () => { }) it('should not display the canceled date if the medication is not canceled', async () => { - const [wrapper] = await setup({} as Medication, [Permissions.ViewMedication]) + const { wrapper } = await setup({} as Medication, [Permissions.ViewMedication]) const completedOnDiv = wrapper.find('.canceled-on') expect(completedOnDiv).toHaveLength(0) }) it('should display the notes in the notes text field', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) @@ -154,7 +146,7 @@ describe('View Medication', () => { describe('draft medication request', () => { it('should display a warning badge if the status is draft', async () => { - const [wrapper, , expectedMedication] = await setup({} as Medication, [ + const { wrapper, expectedMedication } = await setup({} as Medication, [ Permissions.ViewMedication, ]) const medicationStatusDiv = wrapper.find('.medication-status') @@ -168,7 +160,7 @@ describe('View Medication', () => { }) it('should display a update medication and cancel medication button if the medication is in a draft state', async () => { - const [wrapper] = await setup({} as Medication, [ + const { wrapper } = await setup({} as Medication, [ Permissions.ViewMedication, Permissions.CompleteMedication, Permissions.CancelMedication, @@ -183,7 +175,7 @@ describe('View Medication', () => { describe('canceled medication request', () => { it('should display a danger badge if the status is canceled', async () => { - const [wrapper, , expectedMedication] = await setup({ status: 'canceled' } as Medication, [ + const { wrapper, expectedMedication } = await setup({ status: 'canceled' } as Medication, [ Permissions.ViewMedication, ]) @@ -198,7 +190,7 @@ describe('View Medication', () => { }) it('should display the canceled on date if the medication request has been canceled', async () => { - const [wrapper, , expectedMedication] = await setup( + const { wrapper, expectedMedication } = await setup( { status: 'canceled', canceledOn: '2020-03-30T04:45:20.102Z', @@ -215,7 +207,7 @@ describe('View Medication', () => { }) it('should not display update and cancel button if the medication is canceled', async () => { - const [wrapper] = await setup( + const { wrapper } = await setup( { status: 'canceled', } as Medication, @@ -227,7 +219,7 @@ describe('View Medication', () => { }) it('should not display an update button if the medication is canceled', async () => { - const [wrapper] = await setup({ status: 'canceled' } as Medication, [ + const { wrapper } = await setup({ status: 'canceled' } as Medication, [ Permissions.ViewMedication, ]) @@ -239,9 +231,10 @@ describe('View Medication', () => { describe('on update', () => { it('should update the medication with the new information', async () => { - const [wrapper, , mockMedication, , medicationRepositorySaveSpy, history] = await setup({}, [ - Permissions.ViewMedication, - ]) + const { wrapper, expectedMedication, medicationRepositorySaveSpy, history } = await setup( + {}, + [Permissions.ViewMedication], + ) const expectedNotes = 'expected notes' const notesTextField = wrapper.find(TextFieldWithLabelFormGroup).at(0) @@ -256,10 +249,10 @@ describe('View Medication', () => { onClick() }) - expect(medicationRepositorySaveSpy).toHaveBeenCalledTimes(1) + expect(medicationRepositorySaveSpy).toHaveBeenCalled() expect(medicationRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ - ...mockMedication, + ...expectedMedication, notes: expectedNotes, }), ) @@ -269,11 +262,10 @@ describe('View Medication', () => { describe('on cancel', () => { it('should mark the status as canceled and fill in the cancelled on date with the current time', async () => { - const [wrapper, , mockMedication, , medicationRepositorySaveSpy, history] = await setup({}, [ - Permissions.ViewMedication, - Permissions.CompleteMedication, - Permissions.CancelMedication, - ]) + const { wrapper, expectedMedication, medicationRepositorySaveSpy, history } = await setup( + {}, + [Permissions.ViewMedication, Permissions.CompleteMedication, Permissions.CancelMedication], + ) const cancelButton = wrapper.find(Button).at(1) await act(async () => { @@ -282,10 +274,10 @@ describe('View Medication', () => { }) wrapper.update() - expect(medicationRepositorySaveSpy).toHaveBeenCalledTimes(1) + expect(medicationRepositorySaveSpy).toHaveBeenCalled() expect(medicationRepositorySaveSpy).toHaveBeenCalledWith( expect.objectContaining({ - ...mockMedication, + ...expectedMedication, status: 'canceled', canceledOn: expectedDate.toISOString(), }), diff --git a/src/__tests__/medications/requests/NewMedicationRequest.test.tsx b/src/__tests__/medications/requests/NewMedicationRequest.test.tsx index a4067013ea..48805bd208 100644 --- a/src/__tests__/medications/requests/NewMedicationRequest.test.tsx +++ b/src/__tests__/medications/requests/NewMedicationRequest.test.tsx @@ -9,7 +9,7 @@ import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import NewMedicationRequest from '../../../medications/requests/NewMedicationRequest' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../../shared/components/input/TextInputWithLabelFormGroup' import MedicationRepository from '../../../shared/db/MedicationRepository' @@ -21,47 +21,36 @@ import { RootState } from '../../../shared/store' const mockStore = createMockStore([thunk]) describe('New Medication Request', () => { - describe('title and breadcrumbs', () => { - let titleSpy: any + const setup = async ( + store = mockStore({ medication: { status: 'loading', error: {} } } as any), + ) => { const history = createMemoryHistory() + history.push(`/medications/new`) + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) - beforeEach(() => { - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - titleSpy = jest.spyOn(titleUtil, 'default') - history.push('/medications/new') - - mount( - - + const wrapper: ReactWrapper = await mount( + + + - - , - ) - }) + + + , + ) - it('should have New Medication Request as the title', () => { - expect(titleSpy).toHaveBeenCalledWith('medications.requests.new') - }) - }) + wrapper.find(NewMedicationRequest).props().updateTitle = jest.fn() + wrapper.update() + return { wrapper } + } describe('form layout', () => { - let wrapper: ReactWrapper - const history = createMemoryHistory() - - beforeEach(() => { - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - history.push('/medications/new') - - wrapper = mount( - - - - - , - ) + it('should have called the useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalledTimes(1) }) - it('should render a patient typeahead', () => { + it('should render a patient typeahead', async () => { + const { wrapper } = await setup() const typeaheadDiv = wrapper.find('.patient-typeahead') expect(typeaheadDiv).toBeDefined() @@ -76,7 +65,8 @@ describe('New Medication Request', () => { expect(typeahead.prop('searchAccessor')).toEqual('fullName') }) - it('should render a medication input box', () => { + it('should render a medication input box', async () => { + const { wrapper } = await setup() const typeInputBox = wrapper.find(TextInputWithLabelFormGroup).at(0) expect(typeInputBox).toBeDefined() @@ -85,7 +75,8 @@ describe('New Medication Request', () => { expect(typeInputBox.prop('isEditable')).toBeTruthy() }) - it('should render a notes text field', () => { + it('should render a notes text field', async () => { + const { wrapper } = await setup() const notesTextField = wrapper.find(TextFieldWithLabelFormGroup) expect(notesTextField).toBeDefined() @@ -94,34 +85,24 @@ describe('New Medication Request', () => { expect(notesTextField.prop('isEditable')).toBeTruthy() }) - it('should render a save button', () => { + it('should render a save button', async () => { + const { wrapper } = await setup() const saveButton = wrapper.find(Button).at(0) expect(saveButton).toBeDefined() expect(saveButton.text().trim()).toEqual('actions.save') }) - it('should render a cancel button', () => { + it('should render a cancel button', async () => { + const { wrapper } = await setup() const cancelButton = wrapper.find(Button).at(1) expect(cancelButton).toBeDefined() expect(cancelButton.text().trim()).toEqual('actions.cancel') }) }) - describe('on cancel', () => { - let wrapper: ReactWrapper + describe('on cancel', async () => { const history = createMemoryHistory() - - beforeEach(() => { - history.push('/medications/new') - const store = mockStore({ title: '', medication: { status: 'loading', error: {} } } as any) - wrapper = mount( - - - - - , - ) - }) + const { wrapper } = await setup() it('should navigate back to /medications', () => { const cancelButton = wrapper.find(Button).at(1) @@ -135,8 +116,7 @@ describe('New Medication Request', () => { }) }) - describe('on save', () => { - let wrapper: ReactWrapper + describe('on save', async () => { const history = createMemoryHistory() let medicationRepositorySaveSpy: any const expectedDate = new Date() @@ -148,7 +128,11 @@ describe('New Medication Request', () => { id: '1234', requestedOn: expectedDate.toISOString(), } as Medication - + const store = mockStore({ + medication: { status: 'loading', error: {} }, + user: { user: { id: 'fake id' } }, + } as any) + const { wrapper } = await setup(store) beforeEach(() => { jest.resetAllMocks() Date.now = jest.fn(() => expectedDate.valueOf()) @@ -161,20 +145,6 @@ describe('New Medication Request', () => { .mockResolvedValue([ { id: expectedMedication.patient, fullName: 'some full name' }, ] as Patient[]) - - history.push('/medications/new') - const store = mockStore({ - title: '', - medication: { status: 'loading', error: {} }, - user: { user: { id: 'fake id' } }, - } as any) - wrapper = mount( - - - - - , - ) }) it('should save the medication request and navigate to "/medications/:id"', async () => { diff --git a/src/__tests__/medications/search/ViewMedications.test.tsx b/src/__tests__/medications/search/ViewMedications.test.tsx index c113ad3043..f1e083b27c 100644 --- a/src/__tests__/medications/search/ViewMedications.test.tsx +++ b/src/__tests__/medications/search/ViewMedications.test.tsx @@ -12,12 +12,13 @@ import MedicationRequestSearch from '../../../medications/search/MedicationReque import MedicationRequestTable from '../../../medications/search/MedicationRequestTable' import ViewMedications from '../../../medications/search/ViewMedications' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import MedicationRepository from '../../../shared/db/MedicationRepository' import Medication from '../../../shared/model/Medication' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('View Medications', () => { @@ -35,11 +36,10 @@ describe('View Medications', () => { } as unknown) as Medication const history = createMemoryHistory() const store = mockStore({ - title: '', user: { permissions }, medications: { medications: [{ ...expectedMedication, ...medication }] }, } as any) - const titleSpy = jest.spyOn(titleUtil, 'default') + const titleSpy = jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) const setButtonToolBarSpy = jest.fn() jest.spyOn(MedicationRepository, 'search').mockResolvedValue([]) jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) @@ -49,7 +49,9 @@ describe('View Medications', () => { wrapper = await mount( - + + + , ) @@ -64,10 +66,10 @@ describe('View Medications', () => { } describe('title', () => { - it('should have the title', async () => { + it('should have called the useUpdateTitle hook', async () => { const { titleSpy } = await setup({} as Medication) - expect(titleSpy).toHaveBeenCalledWith('medications.label') + expect(titleSpy).toHaveBeenCalled() }) }) diff --git a/src/__tests__/page-header/title/TitleProvider.test.tsx b/src/__tests__/page-header/title/TitleProvider.test.tsx new file mode 100644 index 0000000000..85275956cc --- /dev/null +++ b/src/__tests__/page-header/title/TitleProvider.test.tsx @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react-hooks' +import React from 'react' +import { Provider } from 'react-redux' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' + +import { TitleProvider } from '../../../page-header/title/TitleContext' +import { RootState } from '../../../shared/store' + +const mockStore = createMockStore([thunk]) + +describe('useTitle', () => { + const store = mockStore({ + user: { user: { id: '123' }, permissions: [] }, + appointments: { appointments: [] }, + medications: { medications: [] }, + labs: { labs: [] }, + imagings: { imagings: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + } as any) + + it('should call the updateTitle with the correct data.', () => { + const wrapper = ({ children }: any) => ( + + {children} + + ) + + const useTitle = jest.fn() + const expectedTitle = 'title' + + renderHook(() => useTitle(expectedTitle), { wrapper } as any) + + expect(useTitle).toHaveBeenCalledTimes(1) + expect(useTitle).toHaveBeenCalledWith(expectedTitle) + }) +}) diff --git a/src/__tests__/page-header/title/title-slice.test.ts b/src/__tests__/page-header/title/title-slice.test.ts deleted file mode 100644 index 8684ed6513..0000000000 --- a/src/__tests__/page-header/title/title-slice.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { AnyAction } from 'redux' - -import title, { updateTitle, changeTitle } from '../../../page-header/title/title-slice' - -describe('title slice', () => { - describe('title reducer', () => { - it('should create the proper initial title reducer', () => { - const patientsStore = title(undefined, {} as AnyAction) - expect(patientsStore.title).toEqual('') - }) - }) - - it('should handle the CHANGE_TITLE action', () => { - const expectedTitle = 'expected title' - const patientsStore = title(undefined, { - type: changeTitle.type, - payload: expectedTitle, - }) - - expect(patientsStore.title).toEqual(expectedTitle) - }) - - describe('updateTitle', () => { - it('should dispatch the CHANGE_TITLE event', async () => { - const dispatch = jest.fn() - const getState = jest.fn() - const expectedTitle = 'expected title' - - await updateTitle(expectedTitle)(dispatch, getState, null) - - expect(dispatch).toHaveBeenCalledWith({ type: changeTitle.type, payload: expectedTitle }) - }) - }) -}) diff --git a/src/__tests__/page-header/title/useTitle.test.tsx b/src/__tests__/page-header/title/useTitle.test.tsx deleted file mode 100644 index 8155df39c3..0000000000 --- a/src/__tests__/page-header/title/useTitle.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { renderHook } from '@testing-library/react-hooks' -import React from 'react' -import { Provider } from 'react-redux' -import createMockStore from 'redux-mock-store' -import thunk from 'redux-thunk' - -import * as titleSlice from '../../../page-header/title/title-slice' -import useTitle from '../../../page-header/title/useTitle' -import { RootState } from '../../../shared/store' - -const mockStore = createMockStore([thunk]) - -describe('useTitle', () => { - it('should call the updateTitle with the correct data', () => { - const wrapper = ({ children }: any) => {children} - - jest.spyOn(titleSlice, 'updateTitle') - const expectedTitle = 'title' - - renderHook(() => useTitle(expectedTitle), { wrapper } as any) - expect(titleSlice.updateTitle).toHaveBeenCalledTimes(1) - expect(titleSlice.updateTitle).toHaveBeenCalledWith(expectedTitle) - }) -}) diff --git a/src/__tests__/patients/Patients.test.tsx b/src/__tests__/patients/Patients.test.tsx index 8214eb15bf..d153419d28 100644 --- a/src/__tests__/patients/Patients.test.tsx +++ b/src/__tests__/patients/Patients.test.tsx @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Dashboard from '../../dashboard/Dashboard' import HospitalRun from '../../HospitalRun' import { addBreadcrumbs } from '../../page-header/breadcrumbs/breadcrumbs-slice' +import * as titleUtil from '../../page-header/title/TitleContext' import EditPatient from '../../patients/edit/EditPatient' import NewPatient from '../../patients/new/NewPatient' import ViewPatient from '../../patients/view/ViewPatient' @@ -17,9 +18,11 @@ import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('/patients/new', () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) it('should render the new patient screen when /patients/new is accessed', async () => { const store = mockStore({ title: 'test', @@ -35,7 +38,9 @@ describe('/patients/new', () => { wrapper = await mount( - + + + , ) @@ -63,7 +68,9 @@ describe('/patients/new', () => { } as any)} > - + + + , ) @@ -99,7 +106,9 @@ describe('/patients/edit/:id', () => { const wrapper = mount( - + + + , ) @@ -127,7 +136,9 @@ describe('/patients/edit/:id', () => { } as any)} > - + + + , ) @@ -146,7 +157,9 @@ describe('/patients/edit/:id', () => { } as any)} > - + + + , ) @@ -179,7 +192,9 @@ describe('/patients/:id', () => { const wrapper = mount( - + + + , ) @@ -206,7 +221,9 @@ describe('/patients/:id', () => { } as any)} > - + + + , ) diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index da394a5c40..a49d95261c 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -8,7 +8,7 @@ import { Router, Route } from 'react-router-dom' import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import EditPatient from '../../../patients/edit/EditPatient' import GeneralInformation from '../../../patients/GeneralInformation' import * as patientSlice from '../../../patients/patient-slice' @@ -42,6 +42,7 @@ describe('Edit Patient', () => { let store: MockStore const setup = () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'saveOrUpdate').mockResolvedValue(patient) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) @@ -53,12 +54,15 @@ describe('Edit Patient', () => { - + + + , ) + wrapper.find(EditPatient).props().updateTitle = jest.fn() wrapper.update() return wrapper } @@ -67,6 +71,13 @@ describe('Edit Patient', () => { jest.restoreAllMocks() }) + it('should have called the useUpdateTitle hook', async () => { + await act(async () => { + await setup() + }) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() + }) + it('should render an edit patient form', async () => { let wrapper: any await act(async () => { @@ -86,16 +97,6 @@ describe('Edit Patient', () => { expect(store.getActions()).toContainEqual(patientSlice.fetchPatientSuccess(patient)) }) - it('should use "Edit Patient: " plus patient full name as the title', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - expect(titleUtil.default).toHaveBeenCalledWith( - 'patients.editPatient: givenName familyName suffix (P00001)', - ) - }) - it('should dispatch updatePatient when save button is clicked', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/patients/new/NewPatient.test.tsx b/src/__tests__/patients/new/NewPatient.test.tsx index 57c4979560..7fa6adc287 100644 --- a/src/__tests__/patients/new/NewPatient.test.tsx +++ b/src/__tests__/patients/new/NewPatient.test.tsx @@ -9,7 +9,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import GeneralInformation from '../../../patients/GeneralInformation' import NewPatient from '../../../patients/new/NewPatient' import * as patientSlice from '../../../patients/patient-slice' @@ -17,6 +17,7 @@ import PatientRepository from '../../../shared/db/PatientRepository' import Patient from '../../../shared/model/Patient' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('New Patient', () => { @@ -29,6 +30,7 @@ describe('New Patient', () => { let store: MockStore const setup = (error?: any) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'save') const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.save.mockResolvedValue(patient) @@ -41,7 +43,9 @@ describe('New Patient', () => { - + + + , @@ -64,15 +68,6 @@ describe('New Patient', () => { expect(wrapper.find(GeneralInformation)).toHaveLength(1) }) - it('should use "New Patient" as the header', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - - expect(titleUtil.default).toHaveBeenCalledWith('patients.newPatient') - }) - it('should pass the error object to general information', async () => { const expectedError = { message: 'some message' } let wrapper: any diff --git a/src/__tests__/patients/search/ViewPatients.test.tsx b/src/__tests__/patients/search/ViewPatients.test.tsx index b2bf936d9a..7d9f333fd1 100644 --- a/src/__tests__/patients/search/ViewPatients.test.tsx +++ b/src/__tests__/patients/search/ViewPatients.test.tsx @@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router-dom' import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' +import { TitleProvider } from '../../../page-header/title/TitleContext' import SearchPatients from '../../../patients/search/SearchPatients' import ViewPatients from '../../../patients/search/ViewPatients' import PatientRepository from '../../../shared/db/PatientRepository' @@ -18,7 +19,9 @@ describe('Patients', () => { return mount( - + + + , ) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index d666764cbf..80713efce7 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -10,7 +10,7 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import Allergies from '../../../patients/allergies/Allergies' import AppointmentsList from '../../../patients/appointments/AppointmentsList' import CarePlanTab from '../../../patients/care-plans/CarePlanTab' @@ -26,6 +26,7 @@ import Patient from '../../../shared/model/Patient' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('ViewPatient', () => { @@ -50,6 +51,7 @@ describe('ViewPatient', () => { let store: MockStore const setup = async (permissions = [Permissions.ReadPatients]) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(PatientRepository, 'find') jest.spyOn(PatientRepository, 'getLabs').mockResolvedValue([]) const mockedPatientRepository = mocked(PatientRepository, true) @@ -69,12 +71,15 @@ describe('ViewPatient', () => { - + + + , ) }) + wrapper.find(ViewPatient).props().updateTitle = jest.fn() wrapper.update() return { wrapper: wrapper as ReactWrapper } @@ -92,14 +97,10 @@ describe('ViewPatient', () => { expect(store.getActions()).toContainEqual(patientSlice.fetchPatientSuccess(patient)) }) - it('should render a header with the patients given, family, and suffix', async () => { - jest.spyOn(titleUtil, 'default') - + it('should have called useUpdateTitle hook', async () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith( - `${patient.givenName} ${patient.familyName} ${patient.suffix} (${patient.code})`, - ) + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "Edit Patient" button to the button tool bar if has WritePatients permissions', async () => { diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 2de4c800d8..869fc348a6 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -1,4 +1,4 @@ -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import React from 'react' import { act } from 'react-dom/test-utils' import { Provider } from 'react-redux' @@ -9,6 +9,7 @@ import thunk from 'redux-thunk' import Dashboard from '../../../dashboard/Dashboard' import HospitalRun from '../../../HospitalRun' import { addBreadcrumbs } from '../../../page-header/breadcrumbs/breadcrumbs-slice' +import * as titleUtil from '../../../page-header/title/TitleContext' import Appointments from '../../../scheduling/appointments/Appointments' import EditAppointment from '../../../scheduling/appointments/edit/EditAppointment' import NewAppointment from '../../../scheduling/appointments/new/NewAppointment' @@ -20,29 +21,54 @@ import Patient from '../../../shared/model/Patient' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) +let route: any describe('/appointments', () => { - it('should render the appointments screen when /appointments is accessed', async () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { + const appointment = { + id: '123', + patient: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ title: 'test', - user: { permissions: [Permissions.ReadAppointments] }, - appointments: { appointments: [] }, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) + if (!renderHr) { + wrapper.find(Appointments).props().updateTitle = jest.fn() + } + wrapper.update() - await act(async () => { - wrapper.update() - }) + return { wrapper: wrapper as ReactWrapper, store } + } + + it('should render the appointments screen when /appointments is accessed', async () => { + route = '/appointments' + const permissions: Permissions[] = [Permissions.ReadAppointments] + const { wrapper, store } = setup(route, permissions) expect(wrapper.find(ViewAppointments)).toHaveLength(1) @@ -54,46 +80,66 @@ describe('/appointments', () => { ) }) - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + it('should render the Dashboard when the user does not have read appointment privileges', async () => { + route = '/appointments' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) + await act(async () => { + wrapper.update() + }) expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) describe('/appointments/new', () => { - it('should render the new appointment screen when /appointments/new is accessed', async () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { + const appointment = { + id: '123', + patient: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) + jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) + jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) + const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WriteAppointments] }, - appointment: {}, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) - + if (!renderHr) { + wrapper.find(NewAppointment).props().updateTitle = jest.fn() + } wrapper.update() + return { wrapper: wrapper as ReactWrapper, store } + } + it('should render the new appointment screen when /appointments/new is accessed', async () => { + route = '/appointments/new' + const permissions: Permissions[] = [Permissions.WriteAppointments] + const { wrapper, store } = setup(route, permissions, false) + + await act(async () => { + await wrapper.update() + }) + expect(wrapper.find(NewAppointment)).toHaveLength(1) expect(store.getActions()).toContainEqual( addBreadcrumbs([ @@ -105,27 +151,17 @@ describe('/appointments/new', () => { }) it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/new' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) describe('/appointments/edit/:id', () => { - it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + // eslint-disable-next-line no-shadow + const setup = (route: string, permissions: Permissions[], renderHr: boolean = false) => { const appointment = { id: '123', patient: '456', @@ -135,24 +171,38 @@ describe('/appointments/edit/:id', () => { id: '456', } as Patient + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) const store = mockStore({ title: 'test', - user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] }, - appointment: { appointment, patient: {} as Patient }, + user: { user: { id: '123' }, permissions }, + appointment: { appointment, patient: { id: '456' } as Patient }, + appointments: [{ appointment, patient: { id: '456' } as Patient }], breadcrumbs: { breadcrumbs: [] }, components: { sidebarCollapsed: false }, } as any) const wrapper = mount( - - + + {renderHr ? : } , ) + if (!renderHr) { + wrapper.find(EditAppointment).props().updateTitle = jest.fn() + } + wrapper.update() + + return { wrapper: wrapper as ReactWrapper, store } + } + + it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + route = '/appointments/edit/123' + const permissions: Permissions[] = [Permissions.WriteAppointments, Permissions.ReadAppointments] + const { wrapper, store } = setup(route, permissions, false) expect(wrapper.find(EditAppointment)).toHaveLength(1) @@ -170,39 +220,17 @@ describe('/appointments/edit/:id', () => { }) it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/edit/123' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) it('should render the Dashboard when the user does not have write appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + route = '/appointments/edit/123' + const permissions: Permissions[] = [] + const { wrapper } = setup(route, permissions, true) expect(wrapper.find(Dashboard)).toHaveLength(1) }) diff --git a/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx index d1f7590fd7..c58addfc0a 100644 --- a/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx +++ b/src/__tests__/scheduling/appointments/ViewAppointments.test.tsx @@ -9,7 +9,7 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../page-header/title/useTitle' +import * as titleUtil from '../../../page-header/title/TitleContext' import ViewAppointments from '../../../scheduling/appointments/ViewAppointments' import AppointmentRepository from '../../../shared/db/AppointmentRepository' import PatientRepository from '../../../shared/db/PatientRepository' @@ -17,6 +17,8 @@ import Appointment from '../../../shared/model/Appointment' import Patient from '../../../shared/model/Patient' import { RootState } from '../../../shared/store' +const { TitleProvider } = titleUtil + describe('ViewAppointments', () => { const expectedAppointments = [ { @@ -31,6 +33,7 @@ describe('ViewAppointments', () => { ] as Appointment[] const setup = async () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'findAll').mockResolvedValue(expectedAppointments) jest.spyOn(PatientRepository, 'find') const mockedPatientRepository = mocked(PatientRepository, true) @@ -42,18 +45,19 @@ describe('ViewAppointments', () => { return mount( - + + + , ) } - it('should use "Appointments" as the header', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called the useUpdateTitle hook', async () => { await act(async () => { await setup() }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "New Appointment" button to the button tool bar', async () => { diff --git a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx index 5745e2dc14..02783de5fa 100644 --- a/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/edit/EditAppointment.test.tsx @@ -1,6 +1,6 @@ import { Button } from '@hospitalrun/components' import { roundToNearestMinutes, addMinutes } from 'date-fns' -import { mount } from 'enzyme' +import { mount, ReactWrapper } from 'enzyme' import { createMemoryHistory } from 'history' import React from 'react' import { act } from 'react-dom/test-utils' @@ -10,7 +10,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import EditAppointment from '../../../../scheduling/appointments/edit/EditAppointment' @@ -20,6 +20,7 @@ import Appointment from '../../../../shared/model/Appointment' import Patient from '../../../../shared/model/Patient' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Edit Appointment', () => { @@ -33,7 +34,7 @@ describe('Edit Appointment', () => { type: 'type', } as Appointment - const patient = { + const patient = ({ id: '456', prefix: 'prefix', givenName: 'givenName', @@ -49,12 +50,13 @@ describe('Edit Appointment', () => { address: 'address', code: 'P00001', dateOfBirth: new Date().toISOString(), - } as Patient + } as unknown) as Patient let history: any let store: MockStore - const setup = () => { + const setup = async () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'saveOrUpdate') jest.spyOn(AppointmentRepository, 'find') jest.spyOn(PatientRepository, 'find') @@ -70,18 +72,21 @@ describe('Edit Appointment', () => { store = mockStore({ appointment: { appointment, patient } } as any) history.push('/appointments/edit/123') - const wrapper = mount( + const wrapper = await mount( - + + + , ) + wrapper.find(EditAppointment).props().updateTitle = jest.fn() wrapper.update() - return wrapper + return { wrapper: wrapper as ReactWrapper } } beforeEach(() => { @@ -89,19 +94,15 @@ describe('Edit Appointment', () => { }) it('should render an edit appointment form', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() expect(wrapper.find(AppointmentDetailForm)).toHaveLength(1) }) it('should dispatch fetchAppointment when component loads', async () => { + const { wrapper } = await setup() await act(async () => { - await setup() + await wrapper.update() }) expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) @@ -112,22 +113,17 @@ describe('Edit Appointment', () => { ) }) - it('should use the correct title', async () => { - jest.spyOn(titleUtil, 'default') - await act(async () => { - await setup() - }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.editAppointment') + it('should have called useUpdateTitle hook', async () => { + await setup() + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should dispatch updateAppointment when save button is clicked', async () => { - let wrapper: any + const { wrapper } = await setup() await act(async () => { - wrapper = await setup() + await wrapper.update() }) - wrapper.update() - const saveButton = wrapper.find(Button).at(0) const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') @@ -144,12 +140,7 @@ describe('Edit Appointment', () => { }) it('should navigate to /appointments/:id when save is successful', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() const saveButton = wrapper.find(Button).at(0) const onClick = saveButton.prop('onClick') as any @@ -162,12 +153,7 @@ describe('Edit Appointment', () => { }) it('should navigate to /appointments/:id when cancel is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() + const { wrapper } = await setup() const cancelButton = wrapper.find(Button).at(1) const onClick = cancelButton.prop('onClick') as any diff --git a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx index 2f81a3b7dd..2d876a0f34 100644 --- a/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/new/NewAppointment.test.tsx @@ -10,7 +10,7 @@ import createMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import NewAppointment from '../../../../scheduling/appointments/new/NewAppointment' @@ -21,6 +21,7 @@ import Lab from '../../../../shared/model/Lab' import Patient from '../../../../shared/model/Patient' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) const mockedComponents = mocked(components, true) @@ -30,6 +31,7 @@ describe('New Appointment', () => { const expectedNewAppointment = { id: '123' } const setup = () => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'save') mocked(AppointmentRepository, true).save.mockResolvedValue( expectedNewAppointment as Appointment, @@ -49,7 +51,9 @@ describe('New Appointment', () => { - + + + , @@ -60,13 +64,12 @@ describe('New Appointment', () => { } describe('header', () => { - it('should use "New Appointment" as the header', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called useUpdateTitle hook', async () => { await act(async () => { await setup() }) - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.new') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) }) diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 3490f28e18..c8687753a1 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -10,7 +10,7 @@ import thunk from 'redux-thunk' import { mocked } from 'ts-jest/utils' import * as ButtonBarProvider from '../../../../page-header/button-toolbar/ButtonBarProvider' -import * as titleUtil from '../../../../page-header/title/useTitle' +import * as titleUtil from '../../../../page-header/title/TitleContext' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm' import ViewAppointment from '../../../../scheduling/appointments/view/ViewAppointment' @@ -21,6 +21,7 @@ import Patient from '../../../../shared/model/Patient' import Permissions from '../../../../shared/model/Permissions' import { RootState } from '../../../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) const appointment = { @@ -41,6 +42,7 @@ describe('View Appointment', () => { let store: MockStore const setup = async (status = 'completed', permissions = [Permissions.ReadAppointments]) => { + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment) jest.spyOn(AppointmentRepository, 'delete').mockResolvedValue(appointment) jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient) @@ -65,7 +67,9 @@ describe('View Appointment', () => { - + + + , @@ -80,11 +84,10 @@ describe('View Appointment', () => { jest.restoreAllMocks() }) - it('should use the correct title', async () => { - jest.spyOn(titleUtil, 'default') + it('should have called the useUpdateTitle hook', async () => { await setup() - expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) it('should add a "Edit Appointment" button to the button tool bar if has WriteAppointment permissions', async () => { diff --git a/src/__tests__/settings/Settings.test.tsx b/src/__tests__/settings/Settings.test.tsx index 18ad6a78f4..1d3b89de64 100644 --- a/src/__tests__/settings/Settings.test.tsx +++ b/src/__tests__/settings/Settings.test.tsx @@ -6,15 +6,16 @@ import { Router } from 'react-router-dom' import createMockStore from 'redux-mock-store' import thunk from 'redux-thunk' -import * as titleUtil from '../../page-header/title/useTitle' +import * as titleUtil from '../../page-header/title/TitleContext' import Settings from '../../settings/Settings' import { RootState } from '../../shared/store' +const { TitleProvider } = titleUtil const mockStore = createMockStore([thunk]) describe('Settings', () => { const setup = () => { - jest.spyOn(titleUtil, 'default') + jest.spyOn(titleUtil, 'useUpdateTitle').mockImplementation(() => jest.fn()) const store = mockStore({ title: 'test' } as any) @@ -24,7 +25,9 @@ describe('Settings', () => { const wrapper = mount( - + + + , ) @@ -33,9 +36,9 @@ describe('Settings', () => { } describe('layout', () => { - it('should set the title', () => { + it('should call the useUpdateTitle hook', () => { setup() - expect(titleUtil.default).toHaveBeenCalledWith('settings.label') + expect(titleUtil.useUpdateTitle).toHaveBeenCalled() }) }) }) diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 216c5fe3ef..cf2a1910e0 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,11 +1,12 @@ import React from 'react' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import useTranslator from '../shared/hooks/useTranslator' const Dashboard: React.FC = () => { const { t } = useTranslator() - useTitle(t('dashboard.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('dashboard.label')) return

Example

} diff --git a/src/imagings/requests/NewImagingRequest.tsx b/src/imagings/requests/NewImagingRequest.tsx index e257f7a803..4ad9848f97 100644 --- a/src/imagings/requests/NewImagingRequest.tsx +++ b/src/imagings/requests/NewImagingRequest.tsx @@ -4,7 +4,7 @@ import React, { useState } from 'react' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, } from '../../shared/components/input/SelectWithLabelFormGroup' @@ -19,8 +19,9 @@ import { ImagingRequestError } from '../util/validate-imaging-request' const NewImagingRequest = () => { const { t } = useTranslator() const history = useHistory() + const updateTitle = useUpdateTitle() + updateTitle(t('imagings.requests.new')) const [mutate] = useRequestImaging() - useTitle(t('imagings.requests.new')) const [error, setError] = useState() const [visitOption, setVisitOption] = useState([] as Option[]) diff --git a/src/imagings/search/ViewImagings.tsx b/src/imagings/search/ViewImagings.tsx index 48f4da5fc8..5c31dfa4b1 100644 --- a/src/imagings/search/ViewImagings.tsx +++ b/src/imagings/search/ViewImagings.tsx @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' @@ -16,7 +16,8 @@ const ViewImagings = () => { const { permissions } = useSelector((state: RootState) => state.user) const history = useHistory() const setButtons = useButtonToolbarSetter() - useTitle(t('imagings.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('imagings.label')) const [searchRequest, setSearchRequest] = useState({ status: 'all', diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx index fcff4d56d9..c2092649ab 100644 --- a/src/incidents/list/ViewIncidents.tsx +++ b/src/incidents/list/ViewIncidents.tsx @@ -3,7 +3,7 @@ import React, { useEffect, useState } from 'react' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, } from '../../shared/components/input/SelectWithLabelFormGroup' @@ -15,7 +15,8 @@ const ViewIncidents = () => { const { t } = useTranslator() const history = useHistory() const setButtonToolBar = useButtonToolbarSetter() - useTitle(t('incidents.reports.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.label')) const [searchFilter, setSearchFilter] = useState(IncidentFilter.reported) useEffect(() => { diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx index b774f66cf5..be4fbfeb9f 100644 --- a/src/incidents/report/ReportIncident.tsx +++ b/src/incidents/report/ReportIncident.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import DateTimePickerWithLabelFormGroup from '../../shared/components/input/DateTimePickerWithLabelFormGroup' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' @@ -16,7 +16,8 @@ const ReportIncident = () => { const [mutate] = useReportIncident() const history = useHistory() const { t } = useTranslator() - useTitle(t('incidents.reports.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.new')) const breadcrumbs = [ { i18nKey: 'incidents.reports.new', diff --git a/src/incidents/view/ViewIncident.tsx b/src/incidents/view/ViewIncident.tsx index 9122033fc0..faa3a99146 100644 --- a/src/incidents/view/ViewIncident.tsx +++ b/src/incidents/view/ViewIncident.tsx @@ -3,17 +3,20 @@ import { useSelector } from 'react-redux' import { useParams } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' +import useTranslator from '../../shared/hooks/useTranslator' import { RootState } from '../../shared/store' import ViewIncidentDetails from './ViewIncidentDetails' const ViewIncident = () => { const { id } = useParams() const { permissions } = useSelector((root: RootState) => root.user) - useTitle('View Incident') + const { t } = useTranslator() + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.reports.view')) useAddBreadcrumbs([ { - i18nKey: 'View Incident', + i18nKey: 'incidents.reports.view', location: `/incidents/${id}`, }, ]) diff --git a/src/incidents/visualize/VisualizeIncidents.tsx b/src/incidents/visualize/VisualizeIncidents.tsx index 7dd6cdc870..1b441753cf 100644 --- a/src/incidents/visualize/VisualizeIncidents.tsx +++ b/src/incidents/visualize/VisualizeIncidents.tsx @@ -1,7 +1,7 @@ import { LineGraph, Spinner } from '@hospitalrun/components' import React, { useEffect, useState } from 'react' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import useIncidents from '../hooks/useIncidents' import IncidentFilter from '../IncidentFilter' @@ -9,7 +9,9 @@ import IncidentSearchRequest from '../model/IncidentSearchRequest' const VisualizeIncidents = () => { const { t } = useTranslator() - useTitle(t('incidents.visualize.view')) + const updateTitle = useUpdateTitle() + updateTitle(t('incidents.visualize.view')) + const searchFilter = IncidentFilter.reported const searchRequest: IncidentSearchRequest = { status: searchFilter } const { data, isLoading } = useIncidents(searchRequest) diff --git a/src/labs/ViewLab.tsx b/src/labs/ViewLab.tsx index 6e5e3ddc9f..d253c4cb21 100644 --- a/src/labs/ViewLab.tsx +++ b/src/labs/ViewLab.tsx @@ -5,7 +5,7 @@ import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../shared/components/input/TextFieldWithLabelFormGroup' import useTranslator from '../shared/hooks/useTranslator' import Lab from '../shared/model/Lab' @@ -30,7 +30,8 @@ const ViewLab = () => { const [newNotes, setNewNotes] = useState() const [isEditable, setIsEditable] = useState(true) - useTitle(getTitle(patient, labToView)) + const updateTitle = useUpdateTitle() + updateTitle(getTitle(patient, labToView)) const breadcrumbs = [ { diff --git a/src/labs/ViewLabs.tsx b/src/labs/ViewLabs.tsx index 9e69e8ac4d..82874548b9 100644 --- a/src/labs/ViewLabs.tsx +++ b/src/labs/ViewLabs.tsx @@ -5,7 +5,7 @@ import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, } from '../shared/components/input/SelectWithLabelFormGroup' @@ -23,7 +23,8 @@ const ViewLabs = () => { const { t } = useTranslator() const history = useHistory() const setButtons = useButtonToolbarSetter() - useTitle(t('labs.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('labs.label')) const { permissions } = useSelector((state: RootState) => state.user) const dispatch = useDispatch() diff --git a/src/labs/requests/NewLabRequest.tsx b/src/labs/requests/NewLabRequest.tsx index 3882fc6f11..e5ff4bce33 100644 --- a/src/labs/requests/NewLabRequest.tsx +++ b/src/labs/requests/NewLabRequest.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import TextFieldWithLabelFormGroup from '../../shared/components/input/TextFieldWithLabelFormGroup' import TextInputWithLabelFormGroup from '../../shared/components/input/TextInputWithLabelFormGroup' import PatientRepository from '../../shared/db/PatientRepository' @@ -18,7 +18,8 @@ const NewLabRequest = () => { const { t } = useTranslator() const dispatch = useDispatch() const history = useHistory() - useTitle(t('labs.requests.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('labs.requests.new')) const { status, error } = useSelector((state: RootState) => state.lab) const [newLabRequest, setNewLabRequest] = useState({ diff --git a/src/medications/ViewMedication.tsx b/src/medications/ViewMedication.tsx index 40067b2090..c54b9a2c1c 100644 --- a/src/medications/ViewMedication.tsx +++ b/src/medications/ViewMedication.tsx @@ -5,7 +5,7 @@ import { useSelector, useDispatch } from 'react-redux' import { useParams, useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, } from '../shared/components/input/SelectWithLabelFormGroup' @@ -32,7 +32,8 @@ const ViewMedication = () => { const [medicationToView, setMedicationToView] = useState() const [isEditable, setIsEditable] = useState(true) - useTitle(getTitle(patient, medicationToView)) + const updateTitle = useUpdateTitle() + updateTitle(getTitle(patient, medicationToView)) const breadcrumbs = [ { diff --git a/src/medications/requests/NewMedicationRequest.tsx b/src/medications/requests/NewMedicationRequest.tsx index 0893778539..3a92887b27 100644 --- a/src/medications/requests/NewMedicationRequest.tsx +++ b/src/medications/requests/NewMedicationRequest.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import SelectWithLabelFormGroup, { Option, } from '../../shared/components/input/SelectWithLabelFormGroup' @@ -21,7 +21,8 @@ const NewMedicationRequest = () => { const { t } = useTranslator() const dispatch = useDispatch() const history = useHistory() - useTitle(t('medications.requests.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('medications.requests.new')) const { status, error } = useSelector((state: RootState) => state.medication) const [newMedicationRequest, setNewMedicationRequest] = useState(({ diff --git a/src/medications/search/ViewMedications.tsx b/src/medications/search/ViewMedications.tsx index bcf85b96bd..3865d20b67 100644 --- a/src/medications/search/ViewMedications.tsx +++ b/src/medications/search/ViewMedications.tsx @@ -4,7 +4,7 @@ import { useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Permissions from '../../shared/model/Permissions' import { RootState } from '../../shared/store' @@ -16,7 +16,8 @@ const ViewMedications = () => { const { t } = useTranslator() const history = useHistory() const setButtons = useButtonToolbarSetter() - useTitle(t('medications.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('medications.label')) const { permissions } = useSelector((state: RootState) => state.user) diff --git a/src/page-header/title/TitleContext.tsx b/src/page-header/title/TitleContext.tsx new file mode 100644 index 0000000000..9ff6b3f1e9 --- /dev/null +++ b/src/page-header/title/TitleContext.tsx @@ -0,0 +1,31 @@ +import * as React from 'react' + +type SetTitle = (title: string) => void +type State = { title: string } +type TitleProviderProps = { children: React.ReactNode } +const TitleStateContext = React.createContext(undefined) +const TitleDispatchContext = React.createContext(undefined) + +function TitleProvider({ children }: TitleProviderProps) { + const [title, setTitle] = React.useState('') + return ( + + {children} + + ) +} +function useTitle() { + const context = React.useContext(TitleStateContext) + if (context === undefined) { + throw new Error('useTitle must be used within a TitleProvider') + } + return context +} +function useUpdateTitle() { + const context = React.useContext(TitleDispatchContext) + if (context === undefined) { + throw new Error('useUpdateTitle must be used within a TitleProvider') + } + return context +} +export { TitleProvider, useTitle, useUpdateTitle } diff --git a/src/page-header/title/title-slice.ts b/src/page-header/title/title-slice.ts deleted file mode 100644 index 3b3bc3339b..0000000000 --- a/src/page-header/title/title-slice.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit' - -import { AppThunk } from '../../shared/store' - -interface TitleState { - title: string -} - -const initialState: TitleState = { - title: '', -} - -const titleSlice = createSlice({ - name: 'title', - initialState, - reducers: { - changeTitle(state, { payload }: PayloadAction) { - state.title = payload - }, - }, -}) - -export const { changeTitle } = titleSlice.actions - -export const updateTitle = (title: string): AppThunk => async (dispatch) => { - dispatch(changeTitle(title)) -} - -export default titleSlice.reducer diff --git a/src/page-header/title/useTitle.tsx b/src/page-header/title/useTitle.tsx deleted file mode 100644 index ff2382457f..0000000000 --- a/src/page-header/title/useTitle.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' - -import { updateTitle } from './title-slice' - -export default function useTitle(title: string): void { - const dispatch = useDispatch() - - useEffect(() => { - dispatch(updateTitle(title)) - }) -} diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index 884a26e91d..933f9fd0fc 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' import { RootState } from '../../shared/store' @@ -31,7 +31,8 @@ const EditPatient = () => { (state: RootState) => state.patient, ) - useTitle( + const updateTitle = useUpdateTitle() + updateTitle( `${t('patients.editPatient')}: ${getPatientFullName(reduxPatient)} (${getPatientCode( reduxPatient, )})`, diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index 30fd1d1393..666909e3dc 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' import { RootState } from '../../shared/store' @@ -36,7 +36,8 @@ const NewPatient = () => { dateOfBirth: '1963-01-09T05:00:00.000Z', } as Patient - useTitle(t('patients.newPatient')) + const updateTitle = useUpdateTitle() + updateTitle(t('patients.newPatient')) useAddBreadcrumbs(breadcrumbs, true) const onCancel = () => { diff --git a/src/patients/search/ViewPatients.tsx b/src/patients/search/ViewPatients.tsx index 918afaaa4c..4e90e64d92 100644 --- a/src/patients/search/ViewPatients.tsx +++ b/src/patients/search/ViewPatients.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import SearchPatients from './SearchPatients' @@ -14,7 +14,8 @@ const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }] const ViewPatients = () => { const { t } = useTranslator() const history = useHistory() - useTitle(t('patients.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('patients.label')) const dispatch = useDispatch() const setButtonToolBar = useButtonToolbarSetter() diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index b4506e979b..11c7f72ef0 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -12,7 +12,7 @@ import { import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import useTranslator from '../../shared/hooks/useTranslator' import Patient from '../../shared/model/Patient' import Permissions from '../../shared/model/Permissions' @@ -48,7 +48,8 @@ const ViewPatient = () => { const { patient, status } = useSelector((state: RootState) => state.patient) const { permissions } = useSelector((state: RootState) => state.user) - useTitle(`${getPatientFullName(patient)} (${getPatientCode(patient)})`) + const updateTitle = useUpdateTitle() + updateTitle(`${getPatientFullName(patient)} (${getPatientCode(patient)})`) const setButtonToolBar = useButtonToolbarSetter() diff --git a/src/scheduling/appointments/ViewAppointments.tsx b/src/scheduling/appointments/ViewAppointments.tsx index 8ae3114efd..0746b9e27a 100644 --- a/src/scheduling/appointments/ViewAppointments.tsx +++ b/src/scheduling/appointments/ViewAppointments.tsx @@ -5,7 +5,7 @@ import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../page-header/title/useTitle' +import { useUpdateTitle } from '../../page-header/title/TitleContext' import PatientRepository from '../../shared/db/PatientRepository' import useTranslator from '../../shared/hooks/useTranslator' import { RootState } from '../../shared/store' @@ -24,7 +24,8 @@ const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/app const ViewAppointments = () => { const { t } = useTranslator() const history = useHistory() - useTitle(t('scheduling.appointments.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.label')) const dispatch = useDispatch() const { appointments } = useSelector((state: RootState) => state.appointments) const [events, setEvents] = useState([]) diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index 73e30bed85..7c4987ee9c 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -4,7 +4,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory, useParams } from 'react-router-dom' import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../../page-header/title/useTitle' +import { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Appointment from '../../../shared/model/Appointment' import { RootState } from '../../../shared/store' @@ -14,7 +14,8 @@ import { getAppointmentLabel } from '../util/scheduling-appointment.util' const EditAppointment = () => { const { t } = useTranslator() - useTitle(t('scheduling.appointments.editAppointment')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.editAppointment')) const history = useHistory() const dispatch = useDispatch() diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index b51fe91315..5af533c154 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -6,7 +6,7 @@ import { useDispatch, useSelector } from 'react-redux' import { useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' -import useTitle from '../../../page-header/title/useTitle' +import { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Appointment from '../../../shared/model/Appointment' import { RootState } from '../../../shared/store' @@ -22,7 +22,8 @@ const NewAppointment = () => { const { t } = useTranslator() const history = useHistory() const dispatch = useDispatch() - useTitle(t('scheduling.appointments.new')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.new')) useAddBreadcrumbs(breadcrumbs, true) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index b40c336035..b8002eb819 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -5,7 +5,7 @@ import { useParams, useHistory } from 'react-router-dom' import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs' import { useButtonToolbarSetter } from '../../../page-header/button-toolbar/ButtonBarProvider' -import useTitle from '../../../page-header/title/useTitle' +import { useUpdateTitle } from '../../../page-header/title/TitleContext' import useTranslator from '../../../shared/hooks/useTranslator' import Permissions from '../../../shared/model/Permissions' import { RootState } from '../../../shared/store' @@ -15,7 +15,8 @@ import { getAppointmentLabel } from '../util/scheduling-appointment.util' const ViewAppointment = () => { const { t } = useTranslator() - useTitle(t('scheduling.appointments.viewAppointment')) + const updateTitle = useUpdateTitle() + updateTitle(t('scheduling.appointments.viewAppointment')) const dispatch = useDispatch() const { id } = useParams() const history = useHistory() diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 0d31f89c69..a1cedee093 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -1,13 +1,14 @@ import { Row, Column } from '@hospitalrun/components' import React from 'react' -import useTitle from '../page-header/title/useTitle' +import { useUpdateTitle } from '../page-header/title/TitleContext' import LanguageSelector from '../shared/components/input/LanguageSelector' import useTranslator from '../shared/hooks/useTranslator' const Settings = () => { const { t } = useTranslator() - useTitle(t('settings.label')) + const updateTitle = useUpdateTitle() + updateTitle(t('settings.label')) return ( <> diff --git a/src/shared/store/index.ts b/src/shared/store/index.ts index 888566a5a7..8d5171b12e 100644 --- a/src/shared/store/index.ts +++ b/src/shared/store/index.ts @@ -5,7 +5,6 @@ import lab from '../../labs/lab-slice' import labs from '../../labs/labs-slice' import medication from '../../medications/medication-slice' import breadcrumbs from '../../page-header/breadcrumbs/breadcrumbs-slice' -import title from '../../page-header/title/title-slice' import patient from '../../patients/patient-slice' import patients from '../../patients/patients-slice' import appointment from '../../scheduling/appointments/appointment-slice' @@ -16,7 +15,6 @@ import components from '../components/component-slice' const reducer = combineReducers({ patient, patients, - title, user, appointment, appointments, From 4a3361bb890a8741f57b69ab5e8e66f234e54dd3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 23 Sep 2020 07:23:17 +0000 Subject: [PATCH 86/86] build(deps-dev): bump eslint-plugin-react from 7.20.6 to 7.21.0 (#2415) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2bd2bddbbc..83ae8ec69c 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "eslint-plugin-jest": "~24.0.0", "eslint-plugin-jsx-a11y": "~6.3.0", "eslint-plugin-prettier": "~3.1.2", - "eslint-plugin-react": "~7.20.0", + "eslint-plugin-react": "~7.21.0", "eslint-plugin-react-hooks": "~4.1.0", "history": "4.10.1", "husky": "~4.3.0",