From e513b172e25f24126969564a0e7d4f421758e626 Mon Sep 17 00:00:00 2001 From: Archie Wheeler Date: Thu, 6 Feb 2020 20:29:19 +0000 Subject: [PATCH 01/87] fix(patients): stop "Loading..." when patient has no related persons re #1789 --- .../related-persons/RelatedPersonTab.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 2069429d0a..481f557ad2 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -32,9 +32,9 @@ const RelatedPersonTab = (props: Props) => { fetchedRelatedPersons.push(fetchedRelatedPerson) }), ) - - setRelatedPersons(fetchedRelatedPersons) } + + setRelatedPersons(fetchedRelatedPersons) } fetchRelatedPersons() @@ -88,14 +88,17 @@ const RelatedPersonTab = (props: Props) => {
{relatedPersons ? ( - - {relatedPersons.map((r) => ( - {r.fullName} - ))} - - ) : ( -

Loading...

- )} + (relatedPersons.length > 0) ? ( + + {relatedPersons.map((r) => ( + {r.fullName} + ))} + + ) : ( +

No related persons have been added yet.

+ )) : ( +

Loading...

+ )}
From c156b5bba25b008be3bd7d11cd393e2f12fb49cb Mon Sep 17 00:00:00 2001 From: Archie Wheeler Date: Fri, 7 Feb 2020 10:25:37 +0000 Subject: [PATCH 02/87] fix(persons): replace "No related persons" message with a warning re #1789 --- src/patients/related-persons/RelatedPersonTab.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 481f557ad2..7349852aeb 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { Button, Panel, List, ListItem } from '@hospitalrun/components' +import { Button, Panel, List, ListItem, Alert } from '@hospitalrun/components' import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' import RelatedPerson from 'model/RelatedPerson' import { useTranslation } from 'react-i18next' @@ -95,7 +95,7 @@ const RelatedPersonTab = (props: Props) => { ))} ) : ( -

No related persons have been added yet.

+ )) : (

Loading...

)} From 65b40aeba2c9f75d4d5944e046cdaa7da126f1d1 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 8 Feb 2020 16:28:41 +0100 Subject: [PATCH 03/87] feat(breadcrumb): add a breadcrumb underneath the page header fix #1770 --- src/HospitalRun.tsx | 2 + src/__tests__/components/Breadcrumb.test.tsx | 33 ++++++++++++++ src/components/Breadcrumb.tsx | 46 ++++++++++++++++++++ src/index.css | 14 +++++- 4 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/components/Breadcrumb.test.tsx create mode 100644 src/components/Breadcrumb.tsx diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 158dda828d..f9bf8567d0 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,6 +5,7 @@ import { Toaster } from '@hospitalrun/components' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' +import Breadcrumb from 'components/Breadcrumb' import Sidebar from './components/Sidebar' import Permissions from './model/Permissions' import Dashboard from './dashboard/Dashboard' @@ -21,6 +22,7 @@ const HospitalRun = () => { return (
+
diff --git a/src/__tests__/components/Breadcrumb.test.tsx b/src/__tests__/components/Breadcrumb.test.tsx new file mode 100644 index 0000000000..3ad64f2d5c --- /dev/null +++ b/src/__tests__/components/Breadcrumb.test.tsx @@ -0,0 +1,33 @@ +import '../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { Router } from 'react-router' +import Breadcrumb from 'components/Breadcrumb' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' + +describe('Breadcrumb', () => { + let history = createMemoryHistory() + const setup = (location: string) => { + history = createMemoryHistory() + history.push(location) + return mount( + + + , + ) + } + + it('should render the breadcrumb items', () => { + const wrapper = setup('/patients') + const breadcrumbItem = wrapper.find(HrBreadcrumbItem) + + expect(wrapper.find(HrBreadcrumb)).toHaveLength(1) + expect( + breadcrumbItem.matchesElement(patients), + ).toBeTruthy() + }) +}) diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx new file mode 100644 index 0000000000..e0de776d84 --- /dev/null +++ b/src/components/Breadcrumb.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { useLocation, useHistory } from 'react-router' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' + +interface Item { + name: string + url: string +} + +function getItems(pathname: string): Item[] { + if (!pathname || pathname === '/') { + return [{ name: 'dashboard', url: '/' }] + } + + return pathname + .substring(1) + .split('/') + .map((name) => ({ name, url: '/' })) +} + +const Breadcrumb = () => { + const { pathname } = useLocation() + const history = useHistory() + const items = getItems(pathname) + const lastIndex = items.length - 1 + + return ( + + {items.map((item, index) => { + const isLast = index === lastIndex + const onClick = !isLast ? () => history.push(item.url) : undefined + + return ( + + {item.name} + + ) + })} + + ) +} + +export default Breadcrumb diff --git a/src/index.css b/src/index.css index 49f3c5114d..4dba66ac57 100644 --- a/src/index.css +++ b/src/index.css @@ -24,7 +24,7 @@ code { bottom: 0; left: 0; z-index: 0; /* Behind the navbar */ - padding: 48px 0 0; /* Height of navbar */ + padding: 75px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); } @@ -88,3 +88,15 @@ code { border-color: transparent; box-shadow: 0 0 0 3px rgba(255, 255, 255, .25); } + +.breadcrumb { + z-index: 1; + position: relative; + padding: .2rem 1rem; + background-color: white; + border-bottom: .1rem solid lightgray; +} + +.breadcrumb-item > span, .breadcrumb-item > a { + text-transform: capitalize; +} From da6bdb19e3609e1d8fca6d08349c71814162f536 Mon Sep 17 00:00:00 2001 From: Archie Wheeler Date: Sat, 8 Feb 2020 17:24:56 +0000 Subject: [PATCH 04/87] fix(patients): add test for displaying No Related Persons warning re #1789 --- .../related-persons/RelatedPersons.test.tsx | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx index 55c06cd7f8..865440d485 100644 --- a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx +++ b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx @@ -4,7 +4,7 @@ import { Router } from 'react-router' import { createMemoryHistory } from 'history' import { mount, ReactWrapper } from 'enzyme' import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' -import { Button, List, ListItem } from '@hospitalrun/components' +import { Button, List, ListItem, Alert } from '@hospitalrun/components' import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' import { act } from '@testing-library/react' import PatientRepository from 'clients/db/PatientRepository' @@ -196,4 +196,39 @@ describe('Related Persons Tab', () => { expect(history.location.pathname).toEqual('/patients/123001') }) }) + + describe('EmptyList', () => { + const patient = { + id: '123', + rev: '123', + } as Patient + + const user = { + permissions: [Permissions.WritePatients, Permissions.ReadPatients], + } + + beforeEach(async () => { + jest.spyOn(PatientRepository, 'find') + mocked(PatientRepository.find).mockResolvedValue({ + fullName: 'test test', + id: '123001', + } as Patient) + + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + }) + + it('should display a warning if patient has no related persons', () => { + const warning = wrapper.find(Alert) + expect(warning).toBeDefined() + }) + }) }) From 099e50d846e1574dff2b28aafd77b7d01c4da955 Mon Sep 17 00:00:00 2001 From: Archie Wheeler Date: Sat, 8 Feb 2020 17:47:54 +0000 Subject: [PATCH 05/87] fix(patients): internationalize No Related Persons warning & loading re #1789 --- src/locales/en-US/translation.json | 9 +++++++-- src/patients/related-persons/RelatedPersonTab.tsx | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 37a4cfd210..23c4f0ef41 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -38,9 +38,13 @@ "relatedPersonRequired": "Related Person is required.", "relationshipTypeRequired": "Relationship Type is required." }, + "warning": { + "noRelatedPersons": "No Related Persons" + }, "label": "Related Persons", "new": "New Related Person", - "relationshipType": "Relationship Type" + "relationshipType": "Relationship Type", + "addRelatedPersonAbove": "Add a related person using the button above." }, "types": { "charity": "Charity", @@ -66,7 +70,8 @@ }, "states": { "success": "Success!", - "error": "Error!" + "error": "Error!", + "loading": "Loading..." }, "scheduling": { "label": "Scheduling", diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 45e37f66bb..55214a584e 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -108,12 +108,12 @@ const RelatedPersonTab = (props: Props) => { ) : ( ) ) : ( -

Loading...

+

{t('states.loading')}

)}
From e6ce4cb979d3f3cd7c3704490ad4251955f4922f Mon Sep 17 00:00:00 2001 From: Archie Wheeler Date: Sun, 9 Feb 2020 12:24:30 +0000 Subject: [PATCH 06/87] fix(patients): replace "Loading..." text with Spinner component re #1789 --- src/locales/en-US/translation.json | 3 +-- src/patients/related-persons/RelatedPersonTab.tsx | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 23c4f0ef41..a6632e20c6 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -70,8 +70,7 @@ }, "states": { "success": "Success!", - "error": "Error!", - "loading": "Loading..." + "error": "Error!" }, "scheduling": { "label": "Scheduling", diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 55214a584e..8eecc152da 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect } from 'react' -import { Button, Panel, List, ListItem, Alert } from '@hospitalrun/components' +import { Button, Panel, List, ListItem, Alert, Spinner } from '@hospitalrun/components' import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' import RelatedPerson from 'model/RelatedPerson' import { useTranslation } from 'react-i18next' @@ -113,7 +113,7 @@ const RelatedPersonTab = (props: Props) => { /> ) ) : ( -

{t('states.loading')}

+ )}
From a68ed7e282ec630049671f1f4a292cae6ed91379 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sun, 9 Feb 2020 19:39:13 +0100 Subject: [PATCH 07/87] feat(breadcrumb): customize breadcrumbs for patients and appointments fix #1770 --- src/HospitalRun.tsx | 2 +- src/__tests__/HospitalRun.test.tsx | 4 +- src/__tests__/components/Breadcrumb.test.tsx | 33 ---------- .../breadcrumb/Appointmentbreadcrumb.test.tsx | 31 ++++++++++ .../components/breadcrumb/Breadcrumb.test.tsx | 61 +++++++++++++++++++ .../breadcrumb/DefaultBreadcrumb.test.tsx | 54 ++++++++++++++++ .../breadcrumb/PatientBreadcrumb.test.tsx | 31 ++++++++++ src/components/Breadcrumb.tsx | 46 -------------- .../breadcrumb/AppointmentBreadcrumb.tsx | 33 ++++++++++ src/components/breadcrumb/Breadcrumb.tsx | 16 +++++ .../breadcrumb/DefaultBreadcrumb.tsx | 53 ++++++++++++++++ .../breadcrumb/PatientBreadcrumb.tsx | 27 ++++++++ src/index.css | 5 -- 13 files changed, 310 insertions(+), 86 deletions(-) delete mode 100644 src/__tests__/components/Breadcrumb.test.tsx create mode 100644 src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx create mode 100644 src/__tests__/components/breadcrumb/Breadcrumb.test.tsx create mode 100644 src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx create mode 100644 src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx delete mode 100644 src/components/Breadcrumb.tsx create mode 100644 src/components/breadcrumb/AppointmentBreadcrumb.tsx create mode 100644 src/components/breadcrumb/Breadcrumb.tsx create mode 100644 src/components/breadcrumb/DefaultBreadcrumb.tsx create mode 100644 src/components/breadcrumb/PatientBreadcrumb.tsx diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index f9bf8567d0..58985c424a 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@hospitalrun/components' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' -import Breadcrumb from 'components/Breadcrumb' +import Breadcrumb from 'components/breadcrumb/Breadcrumb' import Sidebar from './components/Sidebar' import Permissions from './model/Permissions' import Dashboard from './dashboard/Dashboard' diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 9d66e3a2e1..274f6a7867 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -77,7 +77,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.ReadPatients] }, - patient, + patient: { patient }, })} > @@ -95,6 +95,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + patient: { patient: {} }, })} > @@ -133,6 +134,7 @@ describe('HospitalRun', () => { title: 'test', user: { permissions: [] }, appointments: { appointments: [] }, + appointment: { appointment: {} }, })} > diff --git a/src/__tests__/components/Breadcrumb.test.tsx b/src/__tests__/components/Breadcrumb.test.tsx deleted file mode 100644 index 3ad64f2d5c..0000000000 --- a/src/__tests__/components/Breadcrumb.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import '../../__mocks__/matchMediaMock' -import React from 'react' -import { mount } from 'enzyme' -import { createMemoryHistory } from 'history' -import { Router } from 'react-router' -import Breadcrumb from 'components/Breadcrumb' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' - -describe('Breadcrumb', () => { - let history = createMemoryHistory() - const setup = (location: string) => { - history = createMemoryHistory() - history.push(location) - return mount( - - - , - ) - } - - it('should render the breadcrumb items', () => { - const wrapper = setup('/patients') - const breadcrumbItem = wrapper.find(HrBreadcrumbItem) - - expect(wrapper.find(HrBreadcrumb)).toHaveLength(1) - expect( - breadcrumbItem.matchesElement(patients), - ).toBeTruthy() - }) -}) diff --git a/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx new file mode 100644 index 0000000000..93a04fcc98 --- /dev/null +++ b/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx @@ -0,0 +1,31 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import { mount } from 'enzyme' +import configureMockStore from 'redux-mock-store' +import { createMemoryHistory } from 'history' +import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' +import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' + +const mockStore = configureMockStore() + +describe('Breadcrumb', () => { + const history = createMemoryHistory() + history.push('/appointments/1234') + const wrapper = mount( + + + + + , + ) + + it('should render 2 breadcrumb items', () => { + expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(2) + }) +}) diff --git a/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx b/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx new file mode 100644 index 0000000000..9d05a7f604 --- /dev/null +++ b/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx @@ -0,0 +1,61 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import { mount } from 'enzyme' +import configureMockStore from 'redux-mock-store' +import { createMemoryHistory } from 'history' +import DefaultBreadcrumb from 'components/breadcrumb/DefaultBreadcrumb' +import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' +import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' +import Breadcrumb from 'components/breadcrumb/Breadcrumb' + +const mockStore = configureMockStore() + +describe('Breadcrumb', () => { + const setup = (location: string) => { + const history = createMemoryHistory() + history.push(location) + return mount( + + + + + , + ) + } + it('should render the patient breadcrumb when /patients/:id is accessed', () => { + const wrapper = setup('/patients/1234') + expect(wrapper.find(PatientBreadcrumb)).toHaveLength(1) + }) + it('should render the appointment breadcrumb when /appointments/:id is accessed', () => { + const wrapper = setup('/appointments/1234') + expect(wrapper.find(AppointmentBreadcrumb)).toHaveLength(1) + }) + + it('should render the default breadcrumb when /patients/new is accessed', () => { + const wrapper = setup('/patients/new') + expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) + }) + + it('should render the default breadcrumb when /appointments/new is accessed', () => { + const wrapper = setup('/appointments/new') + expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) + }) + + it('should render the default breadcrumb when any other path is accessed', () => { + let wrapper = setup('/appointments') + expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) + + wrapper = setup('/patients') + expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) + + wrapper = setup('/') + expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) + }) +}) diff --git a/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx new file mode 100644 index 0000000000..0632dc3820 --- /dev/null +++ b/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx @@ -0,0 +1,54 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { Router } from 'react-router' +import DefaultBreadcrumb, { getItems } from 'components/breadcrumb/DefaultBreadcrumb' +import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' + +describe('DefaultBreadcrumb', () => { + describe('getItems', () => { + it('should return valid items for pathname /', () => { + expect(getItems('/')).toEqual([{ url: '/', active: true }]) + }) + + it('should return valid items for pathname /patients', () => { + expect(getItems('/patients')).toEqual([{ url: '/patients', active: true }]) + }) + + it('should return valid items for pathname /appointments', () => { + expect(getItems('/appointments')).toEqual([{ url: '/appointments', active: true }]) + }) + + it('should return valid items for pathname /patients/new', () => { + expect(getItems('/patients/new')).toEqual([ + { url: '/patients', active: false }, + { url: '/patients/new', active: true }, + ]) + }) + + it('should return valid items for pathname /appointments/new', () => { + expect(getItems('/appointments/new')).toEqual([ + { url: '/appointments', active: false }, + { url: '/appointments/new', active: true }, + ]) + }) + }) + + describe('rendering', () => { + const setup = (location: string) => { + const history = createMemoryHistory() + history.push(location) + return mount( + + + , + ) + } + + it('should render one breadcrumb item for the path /', () => { + const wrapper = setup('/') + expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(1) + }) + }) +}) diff --git a/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx new file mode 100644 index 0000000000..1a33139245 --- /dev/null +++ b/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx @@ -0,0 +1,31 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import { mount } from 'enzyme' +import configureMockStore from 'redux-mock-store' +import { createMemoryHistory } from 'history' +import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' +import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' + +const mockStore = configureMockStore() + +describe('Breadcrumb', () => { + const history = createMemoryHistory() + history.push('/patients/1234') + const wrapper = mount( + + + + + , + ) + + it('should render 2 breadcrumb items', () => { + expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(2) + }) +}) diff --git a/src/components/Breadcrumb.tsx b/src/components/Breadcrumb.tsx deleted file mode 100644 index e0de776d84..0000000000 --- a/src/components/Breadcrumb.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import { useLocation, useHistory } from 'react-router' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' - -interface Item { - name: string - url: string -} - -function getItems(pathname: string): Item[] { - if (!pathname || pathname === '/') { - return [{ name: 'dashboard', url: '/' }] - } - - return pathname - .substring(1) - .split('/') - .map((name) => ({ name, url: '/' })) -} - -const Breadcrumb = () => { - const { pathname } = useLocation() - const history = useHistory() - const items = getItems(pathname) - const lastIndex = items.length - 1 - - return ( - - {items.map((item, index) => { - const isLast = index === lastIndex - const onClick = !isLast ? () => history.push(item.url) : undefined - - return ( - - {item.name} - - ) - })} - - ) -} - -export default Breadcrumb diff --git a/src/components/breadcrumb/AppointmentBreadcrumb.tsx b/src/components/breadcrumb/AppointmentBreadcrumb.tsx new file mode 100644 index 0000000000..5c0aa386f3 --- /dev/null +++ b/src/components/breadcrumb/AppointmentBreadcrumb.tsx @@ -0,0 +1,33 @@ +import React from 'react' +import { useHistory } from 'react-router' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' +import { RootState } from '../../store' + +const AppointmentBreacrumb = () => { + const { t } = useTranslation() + const { appointment } = useSelector((state: RootState) => state.appointment) + const history = useHistory() + let appointmentLabel = '' + + if (appointment.startDateTime && appointment.endDateTime) { + const startDateLabel = new Date(appointment.startDateTime).toLocaleString() + const endDateLabel = new Date(appointment.endDateTime).toLocaleString() + appointmentLabel = `${startDateLabel} - ${endDateLabel}` + } + + return ( + + history.push('/appointments')}> + {t('scheduling.appointments.label')} + + {appointmentLabel} + + ) +} + +export default AppointmentBreacrumb diff --git a/src/components/breadcrumb/Breadcrumb.tsx b/src/components/breadcrumb/Breadcrumb.tsx new file mode 100644 index 0000000000..e2a53d6f77 --- /dev/null +++ b/src/components/breadcrumb/Breadcrumb.tsx @@ -0,0 +1,16 @@ +import React from 'react' +import { Switch, Route } from 'react-router' +import DefaultBreadcrumb from 'components/breadcrumb/DefaultBreadcrumb' +import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' +import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' + +const Breadcrumb = () => ( + + + + + + +) + +export default Breadcrumb diff --git a/src/components/breadcrumb/DefaultBreadcrumb.tsx b/src/components/breadcrumb/DefaultBreadcrumb.tsx new file mode 100644 index 0000000000..f9d1e54d6c --- /dev/null +++ b/src/components/breadcrumb/DefaultBreadcrumb.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { useLocation, useHistory } from 'react-router' +import { useTranslation } from 'react-i18next' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' + +interface Item { + url: string + active: boolean +} + +const urlToi18nKey: { [url: string]: string } = { + '/': 'dashboard.label', + '/patients': 'patients.label', + '/patients/new': 'patients.newPatient', + '/appointments': 'scheduling.appointments.label', + '/appointments/new': 'scheduling.appointments.new', +} + +export function getItems(pathname: string): Item[] { + let url = '' + const paths = pathname.substring(1).split('/') + + return paths.map((path, index) => { + url += `/${path}` + return { url, active: index === paths.length - 1 } + }) +} + +const DefaultBreadcrumb = () => { + const { t } = useTranslation() + const { pathname } = useLocation() + const history = useHistory() + const items = getItems(pathname) + + return ( + + {items.map((item) => { + const onClick = !item.active ? () => history.push(item.url) : undefined + + return ( + + {t(urlToi18nKey[item.url])} + + ) + })} + + ) +} + +export default DefaultBreadcrumb diff --git a/src/components/breadcrumb/PatientBreadcrumb.tsx b/src/components/breadcrumb/PatientBreadcrumb.tsx new file mode 100644 index 0000000000..32c5c71d98 --- /dev/null +++ b/src/components/breadcrumb/PatientBreadcrumb.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import { useHistory } from 'react-router' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' +import { getPatientFullName } from 'patients/util/patient-name-util' +import { RootState } from '../../store' + +const PatientBreacrumb = () => { + const { t } = useTranslation() + const { patient } = useSelector((state: RootState) => state.patient) + const history = useHistory() + + return ( + + history.push('/patients')}> + {t('patients.label')} + + {getPatientFullName(patient)} + + ) +} + +export default PatientBreacrumb diff --git a/src/index.css b/src/index.css index 4dba66ac57..9ad2e9496a 100644 --- a/src/index.css +++ b/src/index.css @@ -94,9 +94,4 @@ code { position: relative; padding: .2rem 1rem; background-color: white; - border-bottom: .1rem solid lightgray; -} - -.breadcrumb-item > span, .breadcrumb-item > a { - text-transform: capitalize; } From 63517e8a2a309788f9147107553d8bf1a26b601f Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sun, 9 Feb 2020 20:12:18 +0100 Subject: [PATCH 08/87] feat(breadcrumb): add a patient to the store in HispitalRun.tests.tsx fix #1770 --- src/__tests__/HospitalRun.test.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 6e8249249e..dc22f7a248 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -96,6 +96,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WritePatients] }, + patient: { patient: {} }, })} > @@ -113,6 +114,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.ReadPatients] }, + patient: { patient: {} }, })} > From 7f0fe7fa47d0705d585f7ca15d4796e1c90c2f16 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 9 Feb 2020 22:58:06 -0600 Subject: [PATCH 09/87] feat(env): adds hospitalrun server information --- .env.example | 1 + README.md | 9 +++++++++ src/config/pouchdb.ts | 15 ++++++--------- 3 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000..2ff53afe11 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +REACT_APP_HOSPITALRUN_SERVER=http://0.0.0.0:3001 \ No newline at end of file diff --git a/README.md b/README.md index 74ecf4c081..9046de1a16 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ Contributions are always welcome. Before contributing please read our [contribut 3. Install the dependencies: `yarn` 4. Run `yarn run start` to build and watch for code changes +## Connecting to HospitalRun Server + +__Note: The following instructions are for connecting to HospitalRun Server during development and are not intended to be for production use. For production deployments, see the deployment instructions.__ + +1. Configure [HospitalRun Server](https://github.com/HospitalRun/hospitalrun-server) +2. Start the HospitalRun Development Server +3. Copy the `.env.example` file to `.env.local` or `.env` +4. Change the `REACT_APP_HOSPITALRUN_SERVER` variable to point to the HospitalRun Development Server. + ## Working on an Issue In order to optimize the workflow and to prevent multiple contributors working on the same issue without interactions, a contributor must ask to be assigned to an issue by one of the core team members: it's enough to ask it inside the specific issue. diff --git a/src/config/pouchdb.ts b/src/config/pouchdb.ts index a4de771a4d..e8f189eb28 100644 --- a/src/config/pouchdb.ts +++ b/src/config/pouchdb.ts @@ -16,15 +16,12 @@ function createDb(name: string) { } const db = new PouchDB(name) - // db.sync( - // `https://a27fa3db-db4d-4456-8465-da953aee0f5b-bluemix:cd6f332d39f24d2b7cfc89d82f6836d46012ef3188698319b0d5fff177cb2ddc@a27fa3db-db4d-4456-8465-da953aee0f5b-bluemix.cloudantnosqldb.appdomain.cloud/${name}`, - // { - // live: true, - // retry: true, - // }, - // ).on('change', (info) => { - // console.log(info) - // }) + db.sync(`${process.env.REAC_APP_HOSPITALRUN_SERVER}/_db/${name}`, { + live: true, + retry: true, + }).on('change', (info) => { + console.log(info) + }) return db } From a4c1cfbcd6ab52579b469e1df54d44c0769c25bd Mon Sep 17 00:00:00 2001 From: oliv37 Date: Tue, 11 Feb 2020 20:39:56 +0100 Subject: [PATCH 10/87] feat(breadcrumb): use a single component for Breadcrumbs fix #1770 --- package.json | 1 - src/HospitalRun.tsx | 4 +- src/__tests__/HospitalRun.test.tsx | 17 ++++-- .../breadcrumb/Appointmentbreadcrumb.test.tsx | 31 ---------- .../components/breadcrumb/Breadcrumb.test.tsx | 61 ------------------- .../breadcrumb/DefaultBreadcrumb.test.tsx | 54 ---------------- .../breadcrumb/PatientBreadcrumb.test.tsx | 31 ---------- src/breadcrumbs/Breadcrumbs.tsx | 32 ++++++++++ src/breadcrumbs/breadcrumbs-slice.ts | 30 +++++++++ src/breadcrumbs/useAddBreadcrumb.ts | 16 +++++ src/breadcrumbs/useSetBreadcrumbs.ts | 16 +++++ .../breadcrumb/AppointmentBreadcrumb.tsx | 33 ---------- src/components/breadcrumb/Breadcrumb.tsx | 16 ----- .../breadcrumb/DefaultBreadcrumb.tsx | 53 ---------------- .../breadcrumb/PatientBreadcrumb.tsx | 27 -------- src/dashboard/Dashboard.tsx | 4 ++ src/index.css | 8 +-- src/model/Breadcrumb.ts | 5 ++ src/patients/edit/EditPatient.tsx | 13 +++- src/patients/list/Patients.tsx | 4 ++ src/patients/new/NewPatient.tsx | 7 +++ src/patients/view/ViewPatient.tsx | 12 +++- src/store/index.ts | 2 + 23 files changed, 156 insertions(+), 321 deletions(-) delete mode 100644 src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx delete mode 100644 src/__tests__/components/breadcrumb/Breadcrumb.test.tsx delete mode 100644 src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx delete mode 100644 src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx create mode 100644 src/breadcrumbs/Breadcrumbs.tsx create mode 100644 src/breadcrumbs/breadcrumbs-slice.ts create mode 100644 src/breadcrumbs/useAddBreadcrumb.ts create mode 100644 src/breadcrumbs/useSetBreadcrumbs.ts delete mode 100644 src/components/breadcrumb/AppointmentBreadcrumb.tsx delete mode 100644 src/components/breadcrumb/Breadcrumb.tsx delete mode 100644 src/components/breadcrumb/DefaultBreadcrumb.tsx delete mode 100644 src/components/breadcrumb/PatientBreadcrumb.tsx create mode 100644 src/model/Breadcrumb.ts diff --git a/package.json b/package.json index 677a3387ea..434fa429ca 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,6 @@ "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ "npm run lint:fix", - "npm run test:ci", "git add ." ] } diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 944dc40017..b9ccb22ffb 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@hospitalrun/components' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' -import Breadcrumb from 'components/breadcrumb/Breadcrumb' +import Breadcrumbs from 'breadcrumbs/Breadcrumbs' import Sidebar from './components/Sidebar' import Permissions from './model/Permissions' import Dashboard from './dashboard/Dashboard' @@ -23,7 +23,6 @@ const HospitalRun = () => { return (
-
@@ -31,6 +30,7 @@ const HospitalRun = () => {

{title}

+
diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index dc22f7a248..532d6cc395 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -29,6 +29,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WritePatients] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -46,6 +47,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -79,6 +81,7 @@ describe('HospitalRun', () => { title: 'test', user: { permissions: [Permissions.WritePatients, Permissions.ReadPatients] }, patient: { patient: {} as Patient }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -96,7 +99,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WritePatients] }, - patient: { patient: {} }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -114,7 +117,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.ReadPatients] }, - patient: { patient: {} }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -148,6 +151,7 @@ describe('HospitalRun', () => { title: 'test', user: { permissions: [Permissions.ReadPatients] }, patient: { patient }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -165,7 +169,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, - patient: { patient: {} }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -186,6 +190,7 @@ describe('HospitalRun', () => { title: 'test', user: { permissions: [Permissions.ReadAppointments] }, appointments: { appointments: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -203,8 +208,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, - appointments: { appointments: [] }, - appointment: { appointment: {} }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -225,6 +229,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WriteAppointments] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -242,6 +247,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, })} > @@ -261,6 +267,7 @@ describe('HospitalRun', () => { store={mockStore({ title: 'test', user: { permissions: [Permissions.WritePatients] }, + breadcrumbs: { breadcrumbs: [] }, })} > diff --git a/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx deleted file mode 100644 index 93a04fcc98..0000000000 --- a/src/__tests__/components/breadcrumb/Appointmentbreadcrumb.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import '../../../__mocks__/matchMediaMock' -import React from 'react' -import { Router } from 'react-router' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import configureMockStore from 'redux-mock-store' -import { createMemoryHistory } from 'history' -import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' -import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' - -const mockStore = configureMockStore() - -describe('Breadcrumb', () => { - const history = createMemoryHistory() - history.push('/appointments/1234') - const wrapper = mount( - - - - - , - ) - - it('should render 2 breadcrumb items', () => { - expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(2) - }) -}) diff --git a/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx b/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx deleted file mode 100644 index 9d05a7f604..0000000000 --- a/src/__tests__/components/breadcrumb/Breadcrumb.test.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import '../../../__mocks__/matchMediaMock' -import React from 'react' -import { Router } from 'react-router' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import configureMockStore from 'redux-mock-store' -import { createMemoryHistory } from 'history' -import DefaultBreadcrumb from 'components/breadcrumb/DefaultBreadcrumb' -import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' -import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' -import Breadcrumb from 'components/breadcrumb/Breadcrumb' - -const mockStore = configureMockStore() - -describe('Breadcrumb', () => { - const setup = (location: string) => { - const history = createMemoryHistory() - history.push(location) - return mount( - - - - - , - ) - } - it('should render the patient breadcrumb when /patients/:id is accessed', () => { - const wrapper = setup('/patients/1234') - expect(wrapper.find(PatientBreadcrumb)).toHaveLength(1) - }) - it('should render the appointment breadcrumb when /appointments/:id is accessed', () => { - const wrapper = setup('/appointments/1234') - expect(wrapper.find(AppointmentBreadcrumb)).toHaveLength(1) - }) - - it('should render the default breadcrumb when /patients/new is accessed', () => { - const wrapper = setup('/patients/new') - expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) - }) - - it('should render the default breadcrumb when /appointments/new is accessed', () => { - const wrapper = setup('/appointments/new') - expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) - }) - - it('should render the default breadcrumb when any other path is accessed', () => { - let wrapper = setup('/appointments') - expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) - - wrapper = setup('/patients') - expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) - - wrapper = setup('/') - expect(wrapper.find(DefaultBreadcrumb)).toHaveLength(1) - }) -}) diff --git a/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx deleted file mode 100644 index 0632dc3820..0000000000 --- a/src/__tests__/components/breadcrumb/DefaultBreadcrumb.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import '../../../__mocks__/matchMediaMock' -import React from 'react' -import { mount } from 'enzyme' -import { createMemoryHistory } from 'history' -import { Router } from 'react-router' -import DefaultBreadcrumb, { getItems } from 'components/breadcrumb/DefaultBreadcrumb' -import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' - -describe('DefaultBreadcrumb', () => { - describe('getItems', () => { - it('should return valid items for pathname /', () => { - expect(getItems('/')).toEqual([{ url: '/', active: true }]) - }) - - it('should return valid items for pathname /patients', () => { - expect(getItems('/patients')).toEqual([{ url: '/patients', active: true }]) - }) - - it('should return valid items for pathname /appointments', () => { - expect(getItems('/appointments')).toEqual([{ url: '/appointments', active: true }]) - }) - - it('should return valid items for pathname /patients/new', () => { - expect(getItems('/patients/new')).toEqual([ - { url: '/patients', active: false }, - { url: '/patients/new', active: true }, - ]) - }) - - it('should return valid items for pathname /appointments/new', () => { - expect(getItems('/appointments/new')).toEqual([ - { url: '/appointments', active: false }, - { url: '/appointments/new', active: true }, - ]) - }) - }) - - describe('rendering', () => { - const setup = (location: string) => { - const history = createMemoryHistory() - history.push(location) - return mount( - - - , - ) - } - - it('should render one breadcrumb item for the path /', () => { - const wrapper = setup('/') - expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(1) - }) - }) -}) diff --git a/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx b/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx deleted file mode 100644 index 1a33139245..0000000000 --- a/src/__tests__/components/breadcrumb/PatientBreadcrumb.test.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import '../../../__mocks__/matchMediaMock' -import React from 'react' -import { Router } from 'react-router' -import { Provider } from 'react-redux' -import { mount } from 'enzyme' -import configureMockStore from 'redux-mock-store' -import { createMemoryHistory } from 'history' -import { BreadcrumbItem as HrBreadcrumbItem } from '@hospitalrun/components' -import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' - -const mockStore = configureMockStore() - -describe('Breadcrumb', () => { - const history = createMemoryHistory() - history.push('/patients/1234') - const wrapper = mount( - - - - - , - ) - - it('should render 2 breadcrumb items', () => { - expect(wrapper.find(HrBreadcrumbItem)).toHaveLength(2) - }) -}) diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000000..a7116c57f1 --- /dev/null +++ b/src/breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,32 @@ +import React from 'react' +import { useHistory } from 'react-router' +import { useSelector } from 'react-redux' +import { useTranslation } from 'react-i18next' +import { + Breadcrumb as HrBreadcrumb, + BreadcrumbItem as HrBreadcrumbItem, +} from '@hospitalrun/components' +import { RootState } from '../store' + +const Breadcrumbs = () => { + const history = useHistory() + const { t } = useTranslation() + const { breadcrumbs } = useSelector((state: RootState) => state.breadcrumbs) + + return ( + + {breadcrumbs.map(({ i18nKey, text, location }, index) => { + const isLast = index === breadcrumbs.length - 1 + const onClick = !isLast ? () => history.push(location) : undefined + + return ( + + {i18nKey ? t(i18nKey) : text} + + ) + })} + + ) +} + +export default Breadcrumbs diff --git a/src/breadcrumbs/breadcrumbs-slice.ts b/src/breadcrumbs/breadcrumbs-slice.ts new file mode 100644 index 0000000000..80d0c7adef --- /dev/null +++ b/src/breadcrumbs/breadcrumbs-slice.ts @@ -0,0 +1,30 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit' +import Breadcrumb from 'model/Breadcrumb' + +interface BreadcrumbsState { + breadcrumbs: Breadcrumb[] +} + +const initialState: BreadcrumbsState = { + breadcrumbs: [], +} + +const breadcrumbsSlice = createSlice({ + name: 'breadcrumbs', + initialState, + reducers: { + setBreadcrumbs(state, { payload }: PayloadAction) { + state.breadcrumbs = payload + }, + addBreadcrumb(state, { payload }: PayloadAction) { + state.breadcrumbs = [...state.breadcrumbs, payload] + }, + removeBreadcrumb(state) { + state.breadcrumbs = state.breadcrumbs.slice(0, -1) + }, + }, +}) + +export const { setBreadcrumbs, addBreadcrumb, removeBreadcrumb } = breadcrumbsSlice.actions + +export default breadcrumbsSlice.reducer diff --git a/src/breadcrumbs/useAddBreadcrumb.ts b/src/breadcrumbs/useAddBreadcrumb.ts new file mode 100644 index 0000000000..7838e134af --- /dev/null +++ b/src/breadcrumbs/useAddBreadcrumb.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import Breadcrumb from 'model/Breadcrumb' +import { addBreadcrumb, removeBreadcrumb } from './breadcrumbs-slice' + +export default function useAddBreadcrumb(breadcrumb: Breadcrumb): void { + const dispatch = useDispatch() + + useEffect(() => { + dispatch(addBreadcrumb(breadcrumb)) + + return () => { + dispatch(removeBreadcrumb()) + } + }, [dispatch, breadcrumb]) +} diff --git a/src/breadcrumbs/useSetBreadcrumbs.ts b/src/breadcrumbs/useSetBreadcrumbs.ts new file mode 100644 index 0000000000..e48434cade --- /dev/null +++ b/src/breadcrumbs/useSetBreadcrumbs.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import Breadcrumb from 'model/Breadcrumb' +import { setBreadcrumbs } from './breadcrumbs-slice' + +export default function useSetBreadcrumbs(breadcrumbs: Breadcrumb[]): void { + const dispatch = useDispatch() + + useEffect(() => { + dispatch(setBreadcrumbs(breadcrumbs)) + + return () => { + dispatch(setBreadcrumbs([])) + } + }, [dispatch, breadcrumbs]) +} diff --git a/src/components/breadcrumb/AppointmentBreadcrumb.tsx b/src/components/breadcrumb/AppointmentBreadcrumb.tsx deleted file mode 100644 index 5c0aa386f3..0000000000 --- a/src/components/breadcrumb/AppointmentBreadcrumb.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import React from 'react' -import { useHistory } from 'react-router' -import { useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' -import { RootState } from '../../store' - -const AppointmentBreacrumb = () => { - const { t } = useTranslation() - const { appointment } = useSelector((state: RootState) => state.appointment) - const history = useHistory() - let appointmentLabel = '' - - if (appointment.startDateTime && appointment.endDateTime) { - const startDateLabel = new Date(appointment.startDateTime).toLocaleString() - const endDateLabel = new Date(appointment.endDateTime).toLocaleString() - appointmentLabel = `${startDateLabel} - ${endDateLabel}` - } - - return ( - - history.push('/appointments')}> - {t('scheduling.appointments.label')} - - {appointmentLabel} - - ) -} - -export default AppointmentBreacrumb diff --git a/src/components/breadcrumb/Breadcrumb.tsx b/src/components/breadcrumb/Breadcrumb.tsx deleted file mode 100644 index e2a53d6f77..0000000000 --- a/src/components/breadcrumb/Breadcrumb.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import React from 'react' -import { Switch, Route } from 'react-router' -import DefaultBreadcrumb from 'components/breadcrumb/DefaultBreadcrumb' -import PatientBreadcrumb from 'components/breadcrumb/PatientBreadcrumb' -import AppointmentBreadcrumb from 'components/breadcrumb/AppointmentBreadcrumb' - -const Breadcrumb = () => ( - - - - - - -) - -export default Breadcrumb diff --git a/src/components/breadcrumb/DefaultBreadcrumb.tsx b/src/components/breadcrumb/DefaultBreadcrumb.tsx deleted file mode 100644 index f9d1e54d6c..0000000000 --- a/src/components/breadcrumb/DefaultBreadcrumb.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React from 'react' -import { useLocation, useHistory } from 'react-router' -import { useTranslation } from 'react-i18next' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' - -interface Item { - url: string - active: boolean -} - -const urlToi18nKey: { [url: string]: string } = { - '/': 'dashboard.label', - '/patients': 'patients.label', - '/patients/new': 'patients.newPatient', - '/appointments': 'scheduling.appointments.label', - '/appointments/new': 'scheduling.appointments.new', -} - -export function getItems(pathname: string): Item[] { - let url = '' - const paths = pathname.substring(1).split('/') - - return paths.map((path, index) => { - url += `/${path}` - return { url, active: index === paths.length - 1 } - }) -} - -const DefaultBreadcrumb = () => { - const { t } = useTranslation() - const { pathname } = useLocation() - const history = useHistory() - const items = getItems(pathname) - - return ( - - {items.map((item) => { - const onClick = !item.active ? () => history.push(item.url) : undefined - - return ( - - {t(urlToi18nKey[item.url])} - - ) - })} - - ) -} - -export default DefaultBreadcrumb diff --git a/src/components/breadcrumb/PatientBreadcrumb.tsx b/src/components/breadcrumb/PatientBreadcrumb.tsx deleted file mode 100644 index 32c5c71d98..0000000000 --- a/src/components/breadcrumb/PatientBreadcrumb.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react' -import { useHistory } from 'react-router' -import { useSelector } from 'react-redux' -import { useTranslation } from 'react-i18next' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' -import { getPatientFullName } from 'patients/util/patient-name-util' -import { RootState } from '../../store' - -const PatientBreacrumb = () => { - const { t } = useTranslation() - const { patient } = useSelector((state: RootState) => state.patient) - const history = useHistory() - - return ( - - history.push('/patients')}> - {t('patients.label')} - - {getPatientFullName(patient)} - - ) -} - -export default PatientBreacrumb diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index bf295875dc..2b46dee8cf 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,10 +1,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' import useTitle from '../page-header/useTitle' +import useSetBreadcrumbs from '../breadcrumbs/useSetBreadcrumbs' + +const breadcrumbs = [{ i18nKey: 'dashboard.label', location: '/' }] const Dashboard: React.FC = () => { const { t } = useTranslation() useTitle(t('dashboard.label')) + useSetBreadcrumbs(breadcrumbs) return

Example

} diff --git a/src/index.css b/src/index.css index 9ad2e9496a..417dbe2316 100644 --- a/src/index.css +++ b/src/index.css @@ -24,7 +24,7 @@ code { bottom: 0; left: 0; z-index: 0; /* Behind the navbar */ - padding: 75px 0 0; /* Height of navbar */ + padding: 48px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, .1); } @@ -90,8 +90,6 @@ code { } .breadcrumb { - z-index: 1; - position: relative; - padding: .2rem 1rem; - background-color: white; + padding: 0; + background-color: white; } diff --git a/src/model/Breadcrumb.ts b/src/model/Breadcrumb.ts new file mode 100644 index 0000000000..b93f41ed41 --- /dev/null +++ b/src/model/Breadcrumb.ts @@ -0,0 +1,5 @@ +export default interface Breadcrumb { + i18nKey?: string + text?: string + location: string +} diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index b47de7491c..d68590e8ac 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useMemo } from 'react' import { useHistory, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -10,6 +10,7 @@ import Patient from '../../model/Patient' import { updatePatient, fetchPatient } from '../patient-slice' import { RootState } from '../../store' import { getPatientFullName, getPatientName } from '../util/patient-name-util' +import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' const getFriendlyId = (p: Patient): string => { if (p) { @@ -34,6 +35,16 @@ const EditPatient = () => { )})`, ) + const breadcrumbs = useMemo( + () => [ + { i18nKey: 'patients.label', location: '/patients' }, + { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, + { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, + ], + [patient], + ) + useSetBreadcrumbs(breadcrumbs) + useEffect(() => { setPatient(reduxPatient) }, [reduxPatient]) diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 307727e6ab..30d327ac73 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -6,11 +6,15 @@ import { Spinner, TextInput, Button, List, ListItem, Container, Row } from '@hos import { RootState } from '../../store' import { fetchPatients, searchPatients } from '../patients-slice' import useTitle from '../../page-header/useTitle' +import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' + +const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }] const Patients = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('patients.label')) + useSetBreadcrumbs(breadcrumbs) const dispatch = useDispatch() const { patients, isLoading } = useSelector((state: RootState) => state.patients) diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index 070470b7eb..ecfe2aa2d3 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -9,6 +9,12 @@ import useTitle from '../../page-header/useTitle' import Patient from '../../model/Patient' import { createPatient } from '../patient-slice' import { getPatientName } from '../util/patient-name-util' +import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' + +const breadcrumbs = [ + { i18nKey: 'patients.label', location: '/patients' }, + { i18nKey: 'patients.newPatient', location: '/patients/new' }, +] const NewPatient = () => { const { t } = useTranslation() @@ -19,6 +25,7 @@ const NewPatient = () => { const [errorMessage, setErrorMessage] = useState('') useTitle(t('patients.newPatient')) + useSetBreadcrumbs(breadcrumbs) const onCancel = () => { history.push('/patients') diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 9b0d53fa15..ca7105cdc8 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams, withRouter, Route, useHistory, useLocation } from 'react-router-dom' import { Panel, Spinner, TabsHeader, Tab, Button } from '@hospitalrun/components' @@ -11,6 +11,7 @@ import { getPatientFullName } from '../util/patient-name-util' import Patient from '../../model/Patient' import GeneralInformation from '../GeneralInformation' import RelatedPerson from '../related-persons/RelatedPersonTab' +import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' const getFriendlyId = (p: Patient): string => { if (p) { @@ -30,6 +31,15 @@ const ViewPatient = () => { useTitle(`${getPatientFullName(patient)} (${getFriendlyId(patient)})`) + const breadcrumbs = useMemo( + () => [ + { i18nKey: 'patients.label', location: '/patients' }, + { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, + ], + [patient], + ) + useSetBreadcrumbs(breadcrumbs) + const { id } = useParams() useEffect(() => { if (id) { diff --git a/src/store/index.ts b/src/store/index.ts index 60176dfc5c..b226815116 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -6,6 +6,7 @@ import appointment from '../scheduling/appointments/appointment-slice' import appointments from '../scheduling/appointments/appointments-slice' import title from '../page-header/title-slice' import user from '../user/user-slice' +import breadcrumbs from '../breadcrumbs/breadcrumbs-slice' const reducer = combineReducers({ patient, @@ -14,6 +15,7 @@ const reducer = combineReducers({ user, appointment, appointments, + breadcrumbs, }) const store = configureStore({ From a25fb2287720c7a69965557acec56f6fe534251c Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Tue, 11 Feb 2020 13:52:37 -0600 Subject: [PATCH 11/87] test(tests): improve coverage, fix issues and errors in tests, cleanup #1804 --- src/__mocks__/i18next.js | 12 + src/__tests__/HospitalRun.test.tsx | 7 +- src/__tests__/page-header/useTitle.test.tsx | 4 +- .../patients/GeneralInformation.test.tsx | 310 +++++++++++++++++- .../patients/edit/EditPatient.test.tsx | 35 +- .../patients/new/NewPatient.test.tsx | 116 ++++--- src/__tests__/patients/patient-slice.test.ts | 24 +- src/__tests__/patients/patients-slice.test.ts | 18 +- .../related-persons/RelatedPersons.test.tsx | 24 +- .../patients/view/ViewPatient.test.tsx | 32 +- .../appointments/Appointments.test.tsx | 10 +- .../appointments/appointment-slice.test.ts | 20 +- .../appointments/appointments-slice.test.ts | 16 +- .../view/ViewAppointment.test.tsx | 27 +- src/patients/GeneralInformation.tsx | 4 +- src/patients/patient-slice.ts | 12 +- src/patients/patients-slice.ts | 14 +- src/patients/view/ViewPatient.tsx | 2 - .../appointments/appointment-slice.ts | 10 +- .../appointments/appointments-slice.ts | 12 +- 20 files changed, 540 insertions(+), 169 deletions(-) create mode 100644 src/__mocks__/i18next.js diff --git a/src/__mocks__/i18next.js b/src/__mocks__/i18next.js new file mode 100644 index 0000000000..252034c7aa --- /dev/null +++ b/src/__mocks__/i18next.js @@ -0,0 +1,12 @@ +const i18next = jest.genMockFromModule('i18next') + +function use() { + return this +} + +const t = (k) => k + +i18next.use = use +i18next.t = t + +module.exports = i18next diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index dead62decd..569e241c02 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -7,6 +7,7 @@ import { mocked } from 'ts-jest/utils' import thunk from 'redux-thunk' import configureMockStore from 'redux-mock-store' import { Toaster } from '@hospitalrun/components' +import { act } from 'react-dom/test-utils' import Dashboard from 'dashboard/Dashboard' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' @@ -107,7 +108,7 @@ describe('HospitalRun', () => { expect(wrapper.find(Dashboard)).toHaveLength(1) }) - it('should render the Dashboard when the user does not have read patient privileges', () => { + it('should render the Dashboard when the user does not have write patient privileges', () => { const wrapper = mount( { , ) + await act(async () => { + wrapper.update() + }) + expect(wrapper.find(Appointments)).toHaveLength(1) }) diff --git a/src/__tests__/page-header/useTitle.test.tsx b/src/__tests__/page-header/useTitle.test.tsx index f05ca7b27f..a913147e81 100644 --- a/src/__tests__/page-header/useTitle.test.tsx +++ b/src/__tests__/page-header/useTitle.test.tsx @@ -1,12 +1,12 @@ import React from 'react' import { renderHook } from '@testing-library/react-hooks' -import createMockStore from 'redux-mock-store' +import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Provider } from 'react-redux' import useTitle from '../../page-header/useTitle' import * as titleSlice from '../../page-header/title-slice' -const store = createMockStore([thunk]) +const store = configureMockStore([thunk]) describe('useTitle', () => { it('should call the updateTitle with the correct data', () => { diff --git a/src/__tests__/patients/GeneralInformation.test.tsx b/src/__tests__/patients/GeneralInformation.test.tsx index 753a44defb..cb9dca5868 100644 --- a/src/__tests__/patients/GeneralInformation.test.tsx +++ b/src/__tests__/patients/GeneralInformation.test.tsx @@ -27,7 +27,7 @@ describe('Error handling', () => { }) }) -describe('General Information', () => { +describe('General Information, without isEditable', () => { const patient = { id: '123', prefix: 'prefix', @@ -42,7 +42,7 @@ describe('General Information', () => { email: 'email@email.com', address: 'address', friendlyId: 'P00001', - dateOfBirth: new Date().toISOString(), + dateOfBirth: '1957-06-14T05:00:00.000Z', isApproximateDateOfBirth: false, } as Patient @@ -163,6 +163,9 @@ describe('General Information', () => { }) it('should render the age and date of birth as approximate if patient.isApproximateDateOfBirth is true', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1987-06-14T05:00:00.000Z').valueOf()) patient.isApproximateDateOfBirth = true await act(async () => { wrapper = await mount( @@ -175,8 +178,309 @@ describe('General Information', () => { wrapper.update() const ageInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') - expect(ageInput.prop('value')).toEqual('0') + expect(ageInput.prop('value')).toEqual('30') expect(ageInput.prop('label')).toEqual('patient.approximateAge') expect(ageInput.prop('isEditable')).toBeFalsy() }) }) + +describe('General Information, isEditable', () => { + const patient = { + id: '123', + prefix: 'prefix', + givenName: 'givenName', + familyName: 'familyName', + suffix: 'suffix', + sex: 'male', + type: 'charity', + occupation: 'occupation', + preferredLanguage: 'preferredLanguage', + phoneNumber: 'phoneNumber', + email: 'email@email.com', + address: 'address', + friendlyId: 'P00001', + dateOfBirth: '1957-06-14T05:00:00.000Z', + isApproximateDateOfBirth: false, + } as Patient + + let wrapper: ReactWrapper + let history = createMemoryHistory() + + const onFieldChange = jest.fn() + + beforeEach(() => { + history = createMemoryHistory() + wrapper = mount( + + ) + , + ) + }) + + beforeEach(() => { + jest.restoreAllMocks() + }) + + const expectedPrefix = 'expectedPrefix' + const expectedGivenName = 'expectedGivenName' + const expectedFamilyName = 'expectedFamilyName' + const expectedSuffix = 'expectedSuffix' + const expectedSex = 'expectedSex' + const expectedType = 'expectedType' + const expectedOccupation = 'expectedOccupation' + const expectedPreferredLanguage = 'expectedPreferredLanguage' + const expectedPhoneNumber = 'expectedPhoneNumber' + const expectedEmail = 'expectedEmail' + const expectedAddress = 'expectedAddress' + const expectedDateOfBirth = '1937-06-14T05:00:00.000Z' + + it('should render the prefix', () => { + const prefixInput = wrapper.findWhere((w: any) => w.prop('name') === 'prefix') + const generalInformation = wrapper.find(GeneralInformation) + expect(prefixInput.prop('value')).toEqual(patient.prefix) + expect(prefixInput.prop('label')).toEqual('patient.prefix') + expect(prefixInput.prop('isEditable')).toBeTruthy() + + act(() => { + prefixInput.prop('onChange')({ target: { value: expectedPrefix } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('prefix', expectedPrefix) + }) + + it('should render the given name', () => { + const givenNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') + const generalInformation = wrapper.find(GeneralInformation) + expect(givenNameInput.prop('value')).toEqual(patient.givenName) + expect(givenNameInput.prop('label')).toEqual('patient.givenName') + expect(givenNameInput.prop('isEditable')).toBeTruthy() + + act(() => { + givenNameInput.prop('onChange')({ target: { value: expectedGivenName } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'givenName', + expectedGivenName, + ) + }) + + it('should render the family name', () => { + const familyNameInput = wrapper.findWhere((w: any) => w.prop('name') === 'familyName') + const generalInformation = wrapper.find(GeneralInformation) + + expect(familyNameInput.prop('value')).toEqual(patient.familyName) + expect(familyNameInput.prop('label')).toEqual('patient.familyName') + expect(familyNameInput.prop('isEditable')).toBeTruthy() + + act(() => { + familyNameInput.prop('onChange')({ target: { value: expectedFamilyName } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'familyName', + expectedFamilyName, + ) + }) + + it('should render the suffix', () => { + const suffixInput = wrapper.findWhere((w: any) => w.prop('name') === 'suffix') + const generalInformation = wrapper.find(GeneralInformation) + + expect(suffixInput.prop('value')).toEqual(patient.suffix) + expect(suffixInput.prop('label')).toEqual('patient.suffix') + expect(suffixInput.prop('isEditable')).toBeTruthy() + + act(() => { + suffixInput.prop('onChange')({ target: { value: expectedSuffix } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('suffix', expectedSuffix) + }) + + it('should render the sex select', () => { + const sexSelect = wrapper.findWhere((w: any) => w.prop('name') === 'sex') + const generalInformation = wrapper.find(GeneralInformation) + + expect(sexSelect.prop('value')).toEqual(patient.sex) + expect(sexSelect.prop('label')).toEqual('patient.sex') + expect(sexSelect.prop('isEditable')).toBeTruthy() + expect(sexSelect.prop('options')).toHaveLength(4) + expect(sexSelect.prop('options')[0].label).toEqual('sex.male') + expect(sexSelect.prop('options')[0].value).toEqual('male') + expect(sexSelect.prop('options')[1].label).toEqual('sex.female') + expect(sexSelect.prop('options')[1].value).toEqual('female') + expect(sexSelect.prop('options')[2].label).toEqual('sex.other') + expect(sexSelect.prop('options')[2].value).toEqual('other') + expect(sexSelect.prop('options')[3].label).toEqual('sex.unknown') + expect(sexSelect.prop('options')[3].value).toEqual('unknown') + + act(() => { + sexSelect.prop('onChange')({ target: { value: expectedSex } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('sex', expectedSex) + }) + + it('should render the patient type select', () => { + const typeSelect = wrapper.findWhere((w: any) => w.prop('name') === 'type') + const generalInformation = wrapper.find(GeneralInformation) + + expect(typeSelect.prop('value')).toEqual(patient.type) + expect(typeSelect.prop('label')).toEqual('patient.type') + expect(typeSelect.prop('isEditable')).toBeTruthy() + expect(typeSelect.prop('options')).toHaveLength(2) + expect(typeSelect.prop('options')[0].label).toEqual('patient.types.charity') + expect(typeSelect.prop('options')[0].value).toEqual('charity') + expect(typeSelect.prop('options')[1].label).toEqual('patient.types.private') + expect(typeSelect.prop('options')[1].value).toEqual('private') + + act(() => { + typeSelect.prop('onChange')({ target: { value: expectedType } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('type', expectedType) + }) + + it('should render the date of the birth of the patient', () => { + const dateOfBirthInput = wrapper.findWhere((w: any) => w.prop('name') === 'dateOfBirth') + const generalInformation = wrapper.find(GeneralInformation) + + expect(dateOfBirthInput.prop('value')).toEqual(new Date(patient.dateOfBirth)) + expect(dateOfBirthInput.prop('label')).toEqual('patient.dateOfBirth') + expect(dateOfBirthInput.prop('isEditable')).toBeTruthy() + + act(() => { + dateOfBirthInput.prop('onChange')(new Date(expectedDateOfBirth)) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'dateOfBirth', + expectedDateOfBirth, + ) + }) + + it('should render the occupation of the patient', () => { + const occupationInput = wrapper.findWhere((w: any) => w.prop('name') === 'occupation') + const generalInformation = wrapper.find(GeneralInformation) + + expect(occupationInput.prop('value')).toEqual(patient.occupation) + expect(occupationInput.prop('label')).toEqual('patient.occupation') + expect(occupationInput.prop('isEditable')).toBeTruthy() + + act(() => { + occupationInput.prop('onChange')({ target: { value: expectedOccupation } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'occupation', + expectedOccupation, + ) + }) + + it('should render the preferred language of the patient', () => { + const preferredLanguageInput = wrapper.findWhere( + (w: any) => w.prop('name') === 'preferredLanguage', + ) + const generalInformation = wrapper.find(GeneralInformation) + + expect(preferredLanguageInput.prop('value')).toEqual(patient.preferredLanguage) + expect(preferredLanguageInput.prop('label')).toEqual('patient.preferredLanguage') + expect(preferredLanguageInput.prop('isEditable')).toBeTruthy() + + act(() => { + preferredLanguageInput.prop('onChange')({ target: { value: expectedPreferredLanguage } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'preferredLanguage', + expectedPreferredLanguage, + ) + }) + + it('should render the phone number of the patient', () => { + const phoneNumberInput = wrapper.findWhere((w: any) => w.prop('name') === 'phoneNumber') + const generalInformation = wrapper.find(GeneralInformation) + + expect(phoneNumberInput.prop('value')).toEqual(patient.phoneNumber) + expect(phoneNumberInput.prop('label')).toEqual('patient.phoneNumber') + expect(phoneNumberInput.prop('isEditable')).toBeTruthy() + + act(() => { + phoneNumberInput.prop('onChange')({ target: { value: expectedPhoneNumber } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'phoneNumber', + expectedPhoneNumber, + ) + }) + + it('should render the email of the patient', () => { + const emailInput = wrapper.findWhere((w: any) => w.prop('name') === 'email') + const generalInformation = wrapper.find(GeneralInformation) + + expect(emailInput.prop('value')).toEqual(patient.email) + expect(emailInput.prop('label')).toEqual('patient.email') + expect(emailInput.prop('isEditable')).toBeTruthy() + + act(() => { + emailInput.prop('onChange')({ target: { value: expectedEmail } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith('email', expectedEmail) + }) + + it('should render the address of the patient', () => { + const addressInput = wrapper.findWhere((w: any) => w.prop('name') === 'address') + const generalInformation = wrapper.find(GeneralInformation) + + expect(addressInput.prop('value')).toEqual(patient.address) + expect(addressInput.prop('label')).toEqual('patient.address') + expect(addressInput.prop('isEditable')).toBeTruthy() + + act(() => { + addressInput.prop('onChange')({ currentTarget: { value: expectedAddress } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'address', + expectedAddress, + ) + }) + + it('should render the age and date of birth as approximate if patient.isApproximateDateOfBirth is true', async () => { + jest + .spyOn(global.Date, 'now') + .mockImplementation(() => new Date('1987-06-14T05:00:00.000Z').valueOf()) + + patient.isApproximateDateOfBirth = true + await act(async () => { + wrapper = await mount( + + ) + , + ) + }) + + wrapper.update() + + // original patient born in '57, Date.now() mocked to '87, so value should be initially + // set to 30 years. when user changes to 20 years, onFieldChange should be called to + // set dateOfBirth to '67. + const approximateAgeInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') + const generalInformation = wrapper.find(GeneralInformation) + expect(approximateAgeInput.prop('value')).toEqual('30') + expect(approximateAgeInput.prop('label')).toEqual('patient.approximateAge') + expect(approximateAgeInput.prop('isEditable')).toBeTruthy() + + act(() => { + approximateAgeInput.prop('onChange')({ target: { value: '20' } }) + }) + + expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( + 'dateOfBirth', + '1967-06-14T05:00:00.000Z', + ) + }) +}) diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index 22ece743f5..54679f6493 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -6,13 +6,14 @@ import { Provider } from 'react-redux' import { mocked } from 'ts-jest/utils' import { createMemoryHistory } from 'history' import { act } from 'react-dom/test-utils' -import configureMockStore from 'redux-mock-store' +import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { Button } from '@hospitalrun/components' import EditPatient from '../../../patients/edit/EditPatient' import GeneralInformation from '../../../patients/GeneralInformation' import Patient from '../../../model/Patient' import * as titleUtil from '../../../page-header/useTitle' +import * as patientSlice from '../../../patients/patient-slice' import PatientRepository from '../../../clients/db/PatientRepository' const mockStore = configureMockStore([thunk]) @@ -36,7 +37,9 @@ describe('Edit Patient', () => { dateOfBirth: new Date().toISOString(), } as Patient - let history = createMemoryHistory() + let history: any + let store: MockStore + const setup = () => { jest.spyOn(PatientRepository, 'saveOrUpdate') jest.spyOn(PatientRepository, 'find') @@ -45,15 +48,13 @@ describe('Edit Patient', () => { mockedPatientRepository.saveOrUpdate.mockResolvedValue(patient) history = createMemoryHistory() + store = mockStore({ patient: { patient } }) + history.push('/patients/edit/123') const wrapper = mount( - + - + @@ -77,6 +78,16 @@ describe('Edit Patient', () => { expect(wrapper.find(GeneralInformation)).toHaveLength(1) }) + it('should dispatch fetchPatient when component loads', async () => { + await act(async () => { + await setup() + }) + + expect(PatientRepository.find).toHaveBeenCalledWith(patient.id) + expect(store.getActions()).toContainEqual(patientSlice.fetchPatientStart()) + 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 () => { @@ -87,7 +98,7 @@ describe('Edit Patient', () => { ) }) - it('should call saveOrUpdate when save button is clicked', async () => { + it('should dispatch updatePatient when save button is clicked', async () => { let wrapper: any await act(async () => { wrapper = await setup() @@ -99,11 +110,13 @@ describe('Edit Patient', () => { const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') - act(() => { - onClick() + await act(async () => { + await onClick() }) expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(patient) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart()) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientSuccess(patient)) }) it('should navigate to /patients/:id when cancel is clicked', async () => { diff --git a/src/__tests__/patients/new/NewPatient.test.tsx b/src/__tests__/patients/new/NewPatient.test.tsx index 5ef0688942..8a7ccb659e 100644 --- a/src/__tests__/patients/new/NewPatient.test.tsx +++ b/src/__tests__/patients/new/NewPatient.test.tsx @@ -1,55 +1,83 @@ import '../../../__mocks__/matchMediaMock' import React from 'react' import { mount } from 'enzyme' -import { Router, MemoryRouter } from 'react-router-dom' +import { Router, Route } from 'react-router-dom' import { Provider } from 'react-redux' import { mocked } from 'ts-jest/utils' import { createMemoryHistory } from 'history' import { act } from 'react-dom/test-utils' import { Button } from '@hospitalrun/components' +import configureMockStore, { MockStore } from 'redux-mock-store' +import thunk from 'redux-thunk' import NewPatient from '../../../patients/new/NewPatient' import GeneralInformation from '../../../patients/GeneralInformation' -import store from '../../../store' import Patient from '../../../model/Patient' import * as patientSlice from '../../../patients/patient-slice' import * as titleUtil from '../../../page-header/useTitle' import PatientRepository from '../../../clients/db/PatientRepository' +const mockStore = configureMockStore([thunk]) + describe('New Patient', () => { - it('should render a general information form', () => { + const patient = { + givenName: 'first', + fullName: 'first', + } as Patient + + let history: any + let store: MockStore + + const setup = () => { + jest.spyOn(PatientRepository, 'save') + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.save.mockResolvedValue(patient) + + history = createMemoryHistory() + store = mockStore({ patient: { patient: {} as Patient } }) + + history.push('/patients/new') const wrapper = mount( - - - + + + + + , ) + wrapper.update() + return wrapper + } + + beforeEach(() => { + jest.restoreAllMocks() + }) + + it('should render a general information form', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + expect(wrapper.find(GeneralInformation)).toHaveLength(1) }) - it('should use "New Patient" as the header', () => { + it('should use "New Patient" as the header', async () => { jest.spyOn(titleUtil, 'default') - mount( - - - - - , - ) + await act(async () => { + await setup() + }) expect(titleUtil.default).toHaveBeenCalledWith('patients.newPatient') }) - it('should pass no given name error when form doesnt contain a given name on save button click', () => { - const wrapper = mount( - - - , - - , - ) + it('should pass no given name error when form doesnt contain a given name on save button click', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) const givenName = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') expect(givenName.prop('value')).toBe('') @@ -71,23 +99,11 @@ describe('New Patient', () => { ) }) - it('should call create patient when save button is clicked', async () => { - jest.spyOn(patientSlice, 'createPatient') - jest.spyOn(PatientRepository, 'save') - const mockedPatientRepository = mocked(PatientRepository, true) - const patient = { - givenName: 'first', - fullName: 'first', - } as Patient - mockedPatientRepository.save.mockResolvedValue(patient) - - const wrapper = mount( - - - - - , - ) + it('should dispatch createPatient when save button is clicked', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) const generalInformationForm = wrapper.find(GeneralInformation) @@ -101,22 +117,20 @@ describe('New Patient', () => { const onClick = saveButton.prop('onClick') as any expect(saveButton.text().trim()).toEqual('actions.save') - act(() => { - onClick() + await act(async () => { + await onClick() }) - expect(patientSlice.createPatient).toHaveBeenCalledWith(patient, expect.anything()) + expect(PatientRepository.save).toHaveBeenCalledWith(patient) + expect(store.getActions()).toContainEqual(patientSlice.createPatientStart()) + expect(store.getActions()).toContainEqual(patientSlice.createPatientSuccess()) }) - it('should navigate to /patients when cancel is clicked', () => { - const history = createMemoryHistory() - const wrapper = mount( - - - - - , - ) + it('should navigate to /patients when cancel is clicked', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) const cancelButton = wrapper.find(Button).at(1) const onClick = cancelButton.prop('onClick') as any diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index c50e293fdd..3548b9a398 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -5,8 +5,8 @@ import { createMemoryHistory } from 'history' import * as components from '@hospitalrun/components' import patient, { - getPatientStart, - getPatientSuccess, + fetchPatientStart, + fetchPatientSuccess, fetchPatient, createPatientStart, createPatientSuccess, @@ -30,15 +30,15 @@ describe('patients slice', () => { expect(patientStore.patient).toEqual({}) }) - it('should handle the GET_PATIENT_START action', () => { + it('should handle the FETCH_PATIENT_START action', () => { const patientStore = patient(undefined, { - type: getPatientStart.type, + type: fetchPatientStart.type, }) expect(patientStore.isLoading).toBeTruthy() }) - it('should handle the GET_PATIENT_SUCCESS actions', () => { + it('should handle the FETCH_PATIENT_SUCCESS actions', () => { const expectedPatient = { id: '123', rev: '123', @@ -48,7 +48,7 @@ describe('patients slice', () => { familyName: 'test', } const patientStore = patient(undefined, { - type: getPatientSuccess.type, + type: fetchPatientSuccess.type, payload: { ...expectedPatient, }, @@ -182,13 +182,13 @@ describe('patients slice', () => { expect(mockedComponents.Toast).toHaveBeenCalledWith( 'success', 'Success!', - `Successfully created patient ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, + `patients.successfullyCreated ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, ) }) }) describe('fetchPatient()', () => { - it('should dispatch the GET_PATIENT_START action', async () => { + it('should dispatch the FETCH_PATIENT_START action', async () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(PatientRepository, 'find') @@ -199,7 +199,7 @@ describe('patients slice', () => { await fetchPatient(expectedPatientId)(dispatch, getState, null) - expect(dispatch).toHaveBeenCalledWith({ type: getPatientStart.type }) + expect(dispatch).toHaveBeenCalledWith({ type: fetchPatientStart.type }) }) it('should call the PatientRepository find method with the correct patient id', async () => { @@ -217,7 +217,7 @@ describe('patients slice', () => { expect(PatientRepository.find).toHaveBeenCalledWith(expectedPatientId) }) - it('should dispatch the GET_PATIENT_SUCCESS action with the correct data', async () => { + it('should dispatch the FETCH_PATIENT_SUCCESS action with the correct data', async () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(PatientRepository, 'find') @@ -229,7 +229,7 @@ describe('patients slice', () => { await fetchPatient(expectedPatientId)(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ - type: getPatientSuccess.type, + type: fetchPatientSuccess.type, payload: { ...expectedPatient, }, @@ -307,7 +307,7 @@ describe('patients slice', () => { expect(mockedComponents.Toast).toHaveBeenCalledWith( 'success', 'Success!', - `Successfully updated patient ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, + `patients.successfullyUpdated ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, ) }) }) diff --git a/src/__tests__/patients/patients-slice.test.ts b/src/__tests__/patients/patients-slice.test.ts index 9fd9b09849..b0f32778a5 100644 --- a/src/__tests__/patients/patients-slice.test.ts +++ b/src/__tests__/patients/patients-slice.test.ts @@ -2,8 +2,8 @@ import '../../__mocks__/matchMediaMock' import { AnyAction } from 'redux' import { mocked } from 'ts-jest/utils' import patients, { - getPatientsStart, - getAllPatientsSuccess, + fetchPatientsStart, + fetchPatientsSuccess, searchPatients, } from '../../patients/patients-slice' import Patient from '../../model/Patient' @@ -21,10 +21,10 @@ describe('patients slice', () => { expect(patientsStore.patients).toHaveLength(0) }) - it('should handle the GET_ALL_PATIENTS_SUCCESS action', () => { + it('should handle the FETCH_PATIENTS_SUCCESS action', () => { const expectedPatients = [{ id: '1234' }] const patientsStore = patients(undefined, { - type: getAllPatientsSuccess.type, + type: fetchPatientsSuccess.type, payload: [{ id: '1234' }], }) @@ -34,13 +34,13 @@ describe('patients slice', () => { }) describe('searchPatients', () => { - it('should dispatch the GET_PATIENTS_START action', async () => { + it('should dispatch the FETCH_PATIENTS_START action', async () => { const dispatch = jest.fn() const getState = jest.fn() await searchPatients('search string')(dispatch, getState, null) - expect(dispatch).toHaveBeenCalledWith({ type: getPatientsStart.type }) + expect(dispatch).toHaveBeenCalledWith({ type: fetchPatientsStart.type }) }) it('should call the PatientRepository search method with the correct search criteria', async () => { @@ -54,7 +54,7 @@ describe('patients slice', () => { expect(PatientRepository.search).toHaveBeenCalledWith(expectedSearchString) }) - it('should call the PatientRepository findALl method if there is no string text', async () => { + it('should call the PatientRepository findAll method if there is no string text', async () => { const dispatch = jest.fn() const getState = jest.fn() jest.spyOn(PatientRepository, 'findAll') @@ -64,7 +64,7 @@ describe('patients slice', () => { expect(PatientRepository.findAll).toHaveBeenCalledTimes(1) }) - it('should dispatch the GET_ALL_PATIENTS_SUCCESS action', async () => { + it('should dispatch the FETCH_PATIENTS_SUCCESS action', async () => { const dispatch = jest.fn() const getState = jest.fn() @@ -80,7 +80,7 @@ describe('patients slice', () => { await searchPatients('search string')(dispatch, getState, null) expect(dispatch).toHaveBeenLastCalledWith({ - type: getAllPatientsSuccess.type, + type: fetchPatientsSuccess.type, payload: expectedPatients, }) }) diff --git a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx index 55c06cd7f8..149dcd3eed 100644 --- a/src/__tests__/patients/related-persons/RelatedPersons.test.tsx +++ b/src/__tests__/patients/related-persons/RelatedPersons.test.tsx @@ -2,14 +2,14 @@ import '../../../__mocks__/matchMediaMock' import React from 'react' import { Router } from 'react-router' import { createMemoryHistory } from 'history' -import { mount, ReactWrapper } from 'enzyme' +import { mount } from 'enzyme' import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' import { Button, List, ListItem } from '@hospitalrun/components' import NewRelatedPersonModal from 'patients/related-persons/NewRelatedPersonModal' import { act } from '@testing-library/react' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' -import createMockStore from 'redux-mock-store' +import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Provider } from 'react-redux' import Permissions from 'model/Permissions' @@ -17,10 +17,10 @@ import { mocked } from 'ts-jest/utils' import * as patientSlice from '../../../patients/patient-slice' -const mockStore = createMockStore([thunk]) +const mockStore = configureMockStore([thunk]) describe('Related Persons Tab', () => { - let wrapper: ReactWrapper + let wrapper: any let history = createMemoryHistory() describe('Add New Related Person', () => { @@ -91,18 +91,21 @@ describe('Related Persons Tab', () => { expect(newRelatedPersonModal.prop('show')).toBeTruthy() }) - it('should call update patient with the data from the modal', () => { - jest.spyOn(patientSlice, 'updatePatient') + it('should call update patient with the data from the modal', async () => { jest.spyOn(PatientRepository, 'saveOrUpdate') + const store = mockStore({ patient, user }) const expectedRelatedPerson = { patientId: '123', type: 'type' } const expectedPatient = { ...patient, relatedPersons: [expectedRelatedPerson], } + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedPatient) + act(() => { wrapper = mount( - + , @@ -115,15 +118,16 @@ describe('Related Persons Tab', () => { }) wrapper.update() - act(() => { + await act(async () => { const newRelatedPersonModal = wrapper.find(NewRelatedPersonModal) const onSave = newRelatedPersonModal.prop('onSave') as any onSave(expectedRelatedPerson) }) wrapper.update() - expect(patientSlice.updatePatient).toHaveBeenCalledTimes(1) - expect(patientSlice.updatePatient).toHaveBeenCalledWith(expectedPatient, history) + expect(PatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedPatient) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart()) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientSuccess(expectedPatient)) }) it('should close the modal when the save button is clicked', () => { diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 74b9437f0d..bbf2ca39c6 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -6,15 +6,19 @@ import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' import { Route, Router } from 'react-router-dom' import { TabsHeader, Tab, Button } from '@hospitalrun/components' +import configureMockStore, { MockStore } from 'redux-mock-store' +import thunk from 'redux-thunk' import GeneralInformation from 'patients/GeneralInformation' import { createMemoryHistory } from 'history' import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' import Patient from '../../../model/Patient' -import * as patientSlice from '../../../patients/patient-slice' import PatientRepository from '../../../clients/db/PatientRepository' import * as titleUtil from '../../../page-header/useTitle' import ViewPatient from '../../../patients/view/ViewPatient' -import store from '../../../store' +import * as patientSlice from '../../../patients/patient-slice' +import Permissions from '../../../model/Permissions' + +const mockStore = configureMockStore([thunk]) describe('ViewPatient', () => { const patient = { @@ -34,20 +38,20 @@ describe('ViewPatient', () => { dateOfBirth: new Date().toISOString(), } as Patient - let history = createMemoryHistory() + let history: any + let store: MockStore const setup = () => { jest.spyOn(PatientRepository, 'find') - jest.spyOn(patientSlice, 'fetchPatient') const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) - jest.mock('react-router-dom', () => ({ - useParams: () => ({ - id: '123', - }), - })) history = createMemoryHistory() + store = mockStore({ + patient: { patient }, + user: { permissions: [Permissions.ReadPatients] }, + }) + history.push('/patients/123') const wrapper = mount( @@ -86,6 +90,16 @@ describe('ViewPatient', () => { expect(history.location.pathname).toEqual('/patients/edit/123') }) + it('should dispatch fetchPatient when component loads', async () => { + await act(async () => { + await setup() + }) + + expect(PatientRepository.find).toHaveBeenCalledWith(patient.id) + expect(store.getActions()).toContainEqual(patientSlice.fetchPatientStart()) + expect(store.getActions()).toContainEqual(patientSlice.fetchPatientSuccess(patient)) + }) + it('should render a header with the patients given, family, and suffix', async () => { jest.spyOn(titleUtil, 'default') await act(async () => { diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 0594369e11..a6e40005de 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -4,7 +4,7 @@ import { mount } from 'enzyme' import { MemoryRouter } from 'react-router-dom' import { Provider } from 'react-redux' import Appointments from 'scheduling/appointments/Appointments' -import createMockStore from 'redux-mock-store' +import configureMockStore from 'redux-mock-store' import thunk from 'redux-thunk' import { Calendar } from '@hospitalrun/components' import { act } from '@testing-library/react' @@ -33,7 +33,7 @@ describe('Appointments', () => { id: '123', fullName: 'patient full name', } as Patient) - const mockStore = createMockStore([thunk]) + const mockStore = configureMockStore([thunk]) return mount( @@ -43,9 +43,11 @@ describe('Appointments', () => { ) } - it('should use "Appointments" as the header', () => { + it('should use "Appointments" as the header', async () => { jest.spyOn(titleUtil, 'default') - setup() + await act(async () => { + await setup() + }) expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') }) diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts index e9f7b8e2ae..7dada1d5a5 100644 --- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts @@ -5,8 +5,8 @@ import { mocked } from 'ts-jest/utils' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' import appointment, { - getAppointmentStart, - getAppointmentSuccess, + fetchAppointmentStart, + fetchAppointmentSuccess, fetchAppointment, } from '../../../scheduling/appointments/appointment-slice' @@ -18,14 +18,14 @@ describe('appointment slice', () => { expect(appointmentStore.appointment).toEqual({} as Appointment) expect(appointmentStore.isLoading).toBeFalsy() }) - it('should handle the GET_APPOINTMENT_START action', () => { + it('should handle the FETCH_APPOINTMENT_START action', () => { const appointmentStore = appointment(undefined, { - type: getAppointmentStart.type, + type: fetchAppointmentStart.type, }) expect(appointmentStore.isLoading).toBeTruthy() }) - it('should handle the GET_APPOINTMENT_SUCCESS action', () => { + it('should handle the FETCH_APPOINTMENT_SUCCESS action', () => { const expectedAppointment = { patientId: '1234', startDateTime: new Date().toISOString(), @@ -38,7 +38,7 @@ describe('appointment slice', () => { } const appointmentStore = appointment(undefined, { - type: getAppointmentSuccess.type, + type: fetchAppointmentSuccess.type, payload: { appointment: expectedAppointment, patient: expectedPatient }, }) @@ -75,12 +75,12 @@ describe('appointment slice', () => { mocked(PatientRepository, true).find.mockResolvedValue(expectedPatient) }) - it('should dispatch the GET_APPOINTMENT_START action', async () => { + it('should dispatch the FETCH_APPOINTMENT_START action', async () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointment('id')(dispatch, getState, null) - expect(dispatch).toHaveBeenCalledWith({ type: getAppointmentStart.type }) + expect(dispatch).toHaveBeenCalledWith({ type: fetchAppointmentStart.type }) }) it('should call appointment repository find', async () => { @@ -103,13 +103,13 @@ describe('appointment slice', () => { expect(findPatientSpy).toHaveBeenCalledWith(expectedId) }) - it('should dispatch the GET_APPOINTMENT_SUCCESS action', async () => { + it('should dispatch the FETCH_APPOINTMENT_SUCCESS action', async () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointment('id')(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ - type: getAppointmentSuccess.type, + type: fetchAppointmentSuccess.type, payload: { appointment: expectedAppointment, patient: expectedPatient }, }) }) diff --git a/src/__tests__/scheduling/appointments/appointments-slice.test.ts b/src/__tests__/scheduling/appointments/appointments-slice.test.ts index 4320810409..4b7345bbb4 100644 --- a/src/__tests__/scheduling/appointments/appointments-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointments-slice.test.ts @@ -7,8 +7,8 @@ import AppointmentRepository from 'clients/db/AppointmentsRepository' import appointments, { createAppointmentStart, createAppointment, - getAppointmentsStart, - getAppointmentsSuccess, + fetchAppointmentsStart, + fetchAppointmentsSuccess, fetchAppointments, } from '../../../scheduling/appointments/appointments-slice' @@ -29,7 +29,7 @@ describe('appointments slice', () => { it('should handle the GET_APPOINTMENTS_START action', () => { const appointmentsStore = appointments(undefined, { - type: getAppointmentsStart.type, + type: fetchAppointmentsStart.type, }) expect(appointmentsStore.isLoading).toBeTruthy() @@ -44,7 +44,7 @@ describe('appointments slice', () => { }, ] const appointmentsStore = appointments(undefined, { - type: getAppointmentsSuccess.type, + type: fetchAppointmentsSuccess.type, payload: expectedAppointments, }) @@ -75,12 +75,12 @@ describe('appointments slice', () => { ) }) - it('should dispatch the GET_APPOINTMENTS_START event', async () => { + it('should dispatch the FETCH_APPOINTMENTS_START event', async () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointments()(dispatch, getState, null) - expect(dispatch).toHaveBeenCalledWith({ type: getAppointmentsStart.type }) + expect(dispatch).toHaveBeenCalledWith({ type: fetchAppointmentsStart.type }) }) it('should call the AppointmentsRepository findAll() function', async () => { @@ -91,13 +91,13 @@ describe('appointments slice', () => { expect(findAllSpy).toHaveBeenCalled() }) - it('should dispatch the GET_APPOINTMENTS_SUCCESS event', async () => { + it('should dispatch the FETCH_APPOINTMENTS_SUCCESS event', async () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointments()(dispatch, getState, null) expect(dispatch).toHaveBeenCalledWith({ - type: getAppointmentsSuccess.type, + type: fetchAppointmentsSuccess.type, payload: expectedAppointments, }) }) diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 755c4a95f1..45f3f3545e 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -2,7 +2,7 @@ import '../../../../__mocks__/matchMediaMock' import React from 'react' import { mount } from 'enzyme' import { Provider } from 'react-redux' -import createMockStore from 'redux-mock-store' +import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import Appointment from 'model/Appointment' import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' @@ -16,6 +16,9 @@ import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' import * as titleUtil from '../../../../page-header/useTitle' +import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' + +const mockStore = configureMockStore([thunk]) const appointment = { id: '123', @@ -31,6 +34,9 @@ const patient = { } as Patient describe('View Appointment', () => { + let history: any + let store: MockStore + const setup = (isLoading: boolean) => { jest.spyOn(AppointmentRepository, 'find') const mockedAppointmentRepository = mocked(AppointmentRepository, true) @@ -40,17 +46,10 @@ describe('View Appointment', () => { const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.find.mockResolvedValue(patient) - jest.mock('react-router-dom', () => ({ - useParams: () => ({ - id: '123', - }), - })) - - const history = createMemoryHistory() + history = createMemoryHistory() history.push('/appointments/123') - const mockStore = createMockStore([thunk]) - const store = mockStore({ + store = mockStore({ appointment: { appointment, isLoading, @@ -85,10 +84,16 @@ describe('View Appointment', () => { expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.view') }) - it('should call the fetch appointment function if id is present', async () => { + it('should dispatch getAppointment if id is present', async () => { await act(async () => { await setup(true) }) + + expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id) + expect(store.getActions()).toContainEqual(appointmentSlice.fetchAppointmentStart()) + expect(store.getActions()).toContainEqual( + appointmentSlice.fetchAppointmentSuccess({ appointment, patient }), + ) }) it('should render a loading spinner', async () => { diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index 843499d247..c01525333a 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -40,7 +40,7 @@ const GeneralInformation = (props: Props) => { approximateAgeNumber = parseFloat(event.target.value) } - const approximateDateOfBirth = subYears(new Date(), approximateAgeNumber) + const approximateDateOfBirth = subYears(new Date(Date.now()), approximateAgeNumber) if (onFieldChange) { onFieldChange('dateOfBirth', startOfDay(approximateDateOfBirth).toISOString()) } @@ -137,7 +137,7 @@ const GeneralInformation = (props: Props) => { label={t('patient.approximateAge')} name="approximateAge" type="number" - value={`${differenceInYears(new Date(), new Date(patient.dateOfBirth))}`} + value={`${differenceInYears(new Date(Date.now()), new Date(patient.dateOfBirth))}`} isEditable={isEditable} onChange={onApproximateAgeChange} /> diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 15f93f0c10..0785e1e5c1 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -27,10 +27,10 @@ const patientSlice = createSlice({ name: 'patient', initialState, reducers: { - getPatientStart: startLoading, + fetchPatientStart: startLoading, createPatientStart: startLoading, updatePatientStart: startLoading, - getPatientSuccess(state, { payload }: PayloadAction) { + fetchPatientSuccess(state, { payload }: PayloadAction) { state.isLoading = false state.patient = payload }, @@ -45,8 +45,8 @@ const patientSlice = createSlice({ }) export const { - getPatientStart, - getPatientSuccess, + fetchPatientStart, + fetchPatientSuccess, createPatientStart, createPatientSuccess, updatePatientStart, @@ -54,9 +54,9 @@ export const { } = patientSlice.actions export const fetchPatient = (id: string): AppThunk => async (dispatch) => { - dispatch(getPatientStart()) + dispatch(fetchPatientStart()) const patient = await PatientRepository.find(id) - dispatch(getPatientSuccess(patient)) + dispatch(fetchPatientSuccess(patient)) } export const createPatient = (patient: Patient, history: any): AppThunk => async (dispatch) => { diff --git a/src/patients/patients-slice.ts b/src/patients/patients-slice.ts index e35e157b96..73cb93f3e6 100644 --- a/src/patients/patients-slice.ts +++ b/src/patients/patients-slice.ts @@ -21,23 +21,23 @@ const patientsSlice = createSlice({ name: 'patients', initialState, reducers: { - getPatientsStart: startLoading, - getAllPatientsSuccess(state, { payload }: PayloadAction) { + fetchPatientsStart: startLoading, + fetchPatientsSuccess(state, { payload }: PayloadAction) { state.isLoading = false state.patients = payload }, }, }) -export const { getPatientsStart, getAllPatientsSuccess } = patientsSlice.actions +export const { fetchPatientsStart, fetchPatientsSuccess } = patientsSlice.actions export const fetchPatients = (): AppThunk => async (dispatch) => { - dispatch(getPatientsStart()) + dispatch(fetchPatientsStart()) const patients = await PatientRepository.findAll() - dispatch(getAllPatientsSuccess(patients)) + dispatch(fetchPatientsSuccess(patients)) } export const searchPatients = (searchString: string): AppThunk => async (dispatch) => { - dispatch(getPatientsStart()) + dispatch(fetchPatientsStart()) let patients if (searchString.trim() === '') { @@ -46,7 +46,7 @@ export const searchPatients = (searchString: string): AppThunk => async (dispatc patients = await PatientRepository.search(searchString) } - dispatch(getAllPatientsSuccess(patients)) + dispatch(fetchPatientsSuccess(patients)) } export default patientsSlice.reducer diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 9b0d53fa15..bb3a6e935a 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -63,8 +63,6 @@ const ViewPatient = () => { color="success" outlined onClick={() => { - console.log('pushying to hsitory patient was:') - console.log(patient) history.push(`/patients/edit/${patient.id}`) }} > diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index ac2b7b6a51..aa710d76d4 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -21,10 +21,10 @@ const appointmentSlice = createSlice({ name: 'appointment', initialState: initialAppointmentState, reducers: { - getAppointmentStart: (state: AppointmentState) => { + fetchAppointmentStart: (state: AppointmentState) => { state.isLoading = true }, - getAppointmentSuccess: ( + fetchAppointmentSuccess: ( state, { payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>, ) => { @@ -35,14 +35,14 @@ const appointmentSlice = createSlice({ }, }) -export const { getAppointmentStart, getAppointmentSuccess } = appointmentSlice.actions +export const { fetchAppointmentStart, fetchAppointmentSuccess } = appointmentSlice.actions export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { - dispatch(getAppointmentStart()) + dispatch(fetchAppointmentStart()) const appointment = await AppointmentRepository.find(id) const patient = await PatientRepository.find(appointment.patientId) - dispatch(getAppointmentSuccess({ appointment, patient })) + dispatch(fetchAppointmentSuccess({ appointment, patient })) } export default appointmentSlice.reducer diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index 5152e75057..1e20e1dbb8 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -22,8 +22,8 @@ const appointmentsSlice = createSlice({ initialState, reducers: { createAppointmentStart: startLoading, - getAppointmentsStart: startLoading, - getAppointmentsSuccess: (state, { payload }: PayloadAction) => { + fetchAppointmentsStart: startLoading, + fetchAppointmentsSuccess: (state, { payload }: PayloadAction) => { state.isLoading = false state.appointments = payload }, @@ -32,14 +32,14 @@ const appointmentsSlice = createSlice({ export const { createAppointmentStart, - getAppointmentsStart, - getAppointmentsSuccess, + fetchAppointmentsStart, + fetchAppointmentsSuccess, } = appointmentsSlice.actions export const fetchAppointments = (): AppThunk => async (dispatch) => { - dispatch(getAppointmentsStart()) + dispatch(fetchAppointmentsStart()) const appointments = await AppointmentRepository.findAll() - dispatch(getAppointmentsSuccess(appointments)) + dispatch(fetchAppointmentsSuccess(appointments)) } export const createAppointment = (appointment: Appointment, history: any): AppThunk => async ( From 7cbc1576fd5f1d5d983cf243c2c785f7e237144a Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Tue, 11 Feb 2020 19:38:58 -0600 Subject: [PATCH 12/87] test(tests): stopped mocking Date() due to timezone issues --- .../patients/GeneralInformation.test.tsx | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/src/__tests__/patients/GeneralInformation.test.tsx b/src/__tests__/patients/GeneralInformation.test.tsx index cb9dca5868..ed6034153c 100644 --- a/src/__tests__/patients/GeneralInformation.test.tsx +++ b/src/__tests__/patients/GeneralInformation.test.tsx @@ -4,6 +4,7 @@ import { Router } from 'react-router' import { mount, ReactWrapper } from 'enzyme' import GeneralInformation from 'patients/GeneralInformation' import { createMemoryHistory } from 'history' +import { startOfDay, subYears } from 'date-fns' import { Alert } from '@hospitalrun/components' import { act } from '@testing-library/react' import Patient from '../../model/Patient' @@ -42,7 +43,7 @@ describe('General Information, without isEditable', () => { email: 'email@email.com', address: 'address', friendlyId: 'P00001', - dateOfBirth: '1957-06-14T05:00:00.000Z', + dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), isApproximateDateOfBirth: false, } as Patient @@ -162,10 +163,7 @@ describe('General Information, without isEditable', () => { expect(addressInput.prop('isEditable')).toBeFalsy() }) - it('should render the age and date of birth as approximate if patient.isApproximateDateOfBirth is true', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('1987-06-14T05:00:00.000Z').valueOf()) + it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { patient.isApproximateDateOfBirth = true await act(async () => { wrapper = await mount( @@ -178,6 +176,7 @@ describe('General Information, without isEditable', () => { wrapper.update() const ageInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') + expect(ageInput.prop('value')).toEqual('30') expect(ageInput.prop('label')).toEqual('patient.approximateAge') expect(ageInput.prop('isEditable')).toBeFalsy() @@ -199,7 +198,7 @@ describe('General Information, isEditable', () => { email: 'email@email.com', address: 'address', friendlyId: 'P00001', - dateOfBirth: '1957-06-14T05:00:00.000Z', + dateOfBirth: startOfDay(subYears(new Date(), 30)).toISOString(), isApproximateDateOfBirth: false, } as Patient @@ -449,11 +448,7 @@ describe('General Information, isEditable', () => { ) }) - it('should render the age and date of birth as approximate if patient.isApproximateDateOfBirth is true', async () => { - jest - .spyOn(global.Date, 'now') - .mockImplementation(() => new Date('1987-06-14T05:00:00.000Z').valueOf()) - + it('should render the approximate age if patient.isApproximateDateOfBirth is true', async () => { patient.isApproximateDateOfBirth = true await act(async () => { wrapper = await mount( @@ -465,9 +460,6 @@ describe('General Information, isEditable', () => { wrapper.update() - // original patient born in '57, Date.now() mocked to '87, so value should be initially - // set to 30 years. when user changes to 20 years, onFieldChange should be called to - // set dateOfBirth to '67. const approximateAgeInput = wrapper.findWhere((w: any) => w.prop('name') === 'approximateAge') const generalInformation = wrapper.find(GeneralInformation) expect(approximateAgeInput.prop('value')).toEqual('30') @@ -480,7 +472,7 @@ describe('General Information, isEditable', () => { expect(generalInformation.prop('onFieldChange')).toHaveBeenCalledWith( 'dateOfBirth', - '1967-06-14T05:00:00.000Z', + startOfDay(subYears(new Date(Date.now()), 20)).toISOString(), ) }) }) From cddc812ae5bd1f1b05e7310b5a504b51ecee1981 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Tue, 11 Feb 2020 20:56:27 -0600 Subject: [PATCH 13/87] feat(env): change env variable names --- .env.example | 2 +- README.md | 4 ++-- src/config/pouchdb.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.env.example b/.env.example index 2ff53afe11..8e0036ab76 100644 --- a/.env.example +++ b/.env.example @@ -1 +1 @@ -REACT_APP_HOSPITALRUN_SERVER=http://0.0.0.0:3001 \ No newline at end of file +REACT_APP_HOSPITALRUN_API=http://0.0.0.0:3001 \ No newline at end of file diff --git a/README.md b/README.md index 9046de1a16..6e112a3740 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,8 @@ __Note: The following instructions are for connecting to HospitalRun Server duri 1. Configure [HospitalRun Server](https://github.com/HospitalRun/hospitalrun-server) 2. Start the HospitalRun Development Server -3. Copy the `.env.example` file to `.env.local` or `.env` -4. Change the `REACT_APP_HOSPITALRUN_SERVER` variable to point to the HospitalRun Development Server. +3. Copy the `.env.example` file to `.env` +4. Change the `REACT_APP_HOSPITALRUN_API` variable to point to the HospitalRun Development Server. ## Working on an Issue diff --git a/src/config/pouchdb.ts b/src/config/pouchdb.ts index e8f189eb28..dcf9a4311f 100644 --- a/src/config/pouchdb.ts +++ b/src/config/pouchdb.ts @@ -16,7 +16,7 @@ function createDb(name: string) { } const db = new PouchDB(name) - db.sync(`${process.env.REAC_APP_HOSPITALRUN_SERVER}/_db/${name}`, { + db.sync(`${process.env.REACT_APP_HOSPITALRUN_API}/_db/${name}`, { live: true, retry: true, }).on('change', (info) => { From 3407e0663e40447f4580afab5da8939a1b1335f9 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2020 10:41:32 +0000 Subject: [PATCH 14/87] chore(deps): bump @hospitalrun/components from 0.32.5 to 0.33.0 Bumps [@hospitalrun/components](https://github.com/HospitalRun/components) from 0.32.5 to 0.33.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/v0.32.5...v0.33.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 677a3387ea..77a4098bba 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "^0.32.4", + "@hospitalrun/components": "^0.33.0", "@reduxjs/toolkit": "~1.2.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", From a6eaf2a9547875a4a0947f4e9a7e21e41add11c8 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Wed, 12 Feb 2020 20:22:46 +0100 Subject: [PATCH 15/87] feat(breadcrumb): display the breadcrumb in the appointment components fix #1770 --- src/scheduling/appointments/Appointments.tsx | 4 ++++ .../appointments/new/NewAppointment.tsx | 8 ++++++- .../appointments/view/ViewAppointment.tsx | 21 ++++++++++++++++++- 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 7676b1beb9..78fde52f9b 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -6,6 +6,7 @@ import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useHistory } from 'react-router' import PatientRepository from 'clients/db/PatientRepository' +import useSetBreadcrumbs from 'breadcrumbs/useSetBreadcrumbs' import { fetchAppointments } from './appointments-slice' interface Event { @@ -16,10 +17,13 @@ interface Event { allDay: boolean } +const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/patients' }] + const Appointments = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('scheduling.appointments.label')) + useSetBreadcrumbs(breadcrumbs) const dispatch = useDispatch() const { appointments } = useSelector((state: RootState) => state.appointments) const [events, setEvents] = useState([]) diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 538ae33f98..650b682b3d 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -1,7 +1,6 @@ import React, { useState } from 'react' import useTitle from 'page-header/useTitle' import { useTranslation } from 'react-i18next' - import roundToNearestMinutes from 'date-fns/roundToNearestMinutes' import { useHistory } from 'react-router' import { useDispatch } from 'react-redux' @@ -9,14 +8,21 @@ import Appointment from 'model/Appointment' import addMinutes from 'date-fns/addMinutes' import { isBefore } from 'date-fns' import { Button, Alert } from '@hospitalrun/components' +import useSetBreadcrumbs from '../../../breadcrumbs/useSetBreadcrumbs' import { createAppointment } from '../appointments-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, +] + const NewAppointment = () => { const { t } = useTranslation() const history = useHistory() const dispatch = useDispatch() useTitle(t('scheduling.appointments.new')) + useSetBreadcrumbs(breadcrumbs) 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 c2e78947ac..45cf9b000d 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -1,12 +1,22 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useMemo } from 'react' import useTitle from 'page-header/useTitle' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useParams } from 'react-router' import { Spinner } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' +import Appointment from 'model/Appointment' import { fetchAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +import useSetBreadcrumbs from '../../../breadcrumbs/useSetBreadcrumbs' + +function getAppointmentLabel(appointment: Appointment) { + const { id, startDateTime, endDateTime } = appointment + + return startDateTime && endDateTime + ? `${new Date(startDateTime).toLocaleString()} - ${new Date(endDateTime).toLocaleString()}` + : id +} const ViewAppointment = () => { const { t } = useTranslation() @@ -15,6 +25,15 @@ const ViewAppointment = () => { const { id } = useParams() const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) + const breadcrumbs = useMemo( + () => [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, + ], + [appointment], + ) + useSetBreadcrumbs(breadcrumbs) + useEffect(() => { if (id) { dispatch(fetchAppointment(id)) From 9bdbb343f3b1a524344b44dd19a85afcff4a36b3 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Wed, 12 Feb 2020 20:30:46 +0100 Subject: [PATCH 16/87] docs(readme): improve 'How to commit' section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 74ecf4c081..e778c7da69 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ In order to optimize the workflow and to prevent multiple contributors working o ## How to commit -This repo uses Conventional Commits. Commitizen is mandatory for making proper commits. Once you have staged your changes, can run npm run commit or yarn commit from the root directory in order to commit following our standards. +This repo uses Conventional Commits. Commitizen is mandatory for making proper commits. Once you have staged your changes, can run `npm run commit` or `yarn commit` from the root directory in order to commit following our standards.
From d20e294340a155cb90138ea9bf5210e6e697ea71 Mon Sep 17 00:00:00 2001 From: Ignacio Gea Date: Wed, 12 Feb 2020 14:35:07 -0600 Subject: [PATCH 17/87] fix(patient-slice.ts): conditionally render family name and suffix fix #1818 --- src/__tests__/patients/patient-slice.test.ts | 24 ++++++++++++++++++++ src/patients/patient-slice.ts | 12 +++++----- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index c50e293fdd..4a13026db6 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -310,5 +310,29 @@ describe('patients slice', () => { `Successfully updated patient ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, ) }) + + it('should call the Toaster with message only including given name', async () => { + jest.spyOn(components, 'Toast') + const expectedPatientId = '12345' + const expectedGivenName = 'John' + const expectedPatient = { + id: expectedPatientId, + givenName: expectedGivenName, + } as Patient + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedPatient) + const mockedComponents = mocked(components, true) + const history = createMemoryHistory() + const dispatch = jest.fn() + const getState = jest.fn() + + await updatePatient(expectedPatient, history)(dispatch, getState, null) + + expect(mockedComponents.Toast).toHaveBeenCalledWith( + 'success', + 'Success!', + `Successfully updated patient ${expectedGivenName}`, + ) + }) }) }) diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index 15f93f0c10..fd36ad8add 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -67,9 +67,9 @@ export const createPatient = (patient: Patient, history: any): AppThunk => async Toast( 'success', il8n.t('Success!'), - `${il8n.t('patients.successfullyCreated')} ${patient.givenName} ${patient.familyName} ${ - patient.suffix - }`, + `${il8n.t('patients.successfullyCreated')} ${patient.givenName} ${ + patient.familyName ? patient.familyName : '' + } ${patient.suffix ? patient.suffix : ''}`.trimEnd(), ) } @@ -81,9 +81,9 @@ export const updatePatient = (patient: Patient, history: any): AppThunk => async Toast( 'success', il8n.t('Success!'), - `${il8n.t('patients.successfullyUpdated')} ${patient.givenName} ${patient.familyName} ${ - patient.suffix - }`, + `${il8n.t('patients.successfullyUpdated')} ${patient.givenName} ${ + patient.familyName ? patient.familyName : '' + } ${patient.suffix ? patient.suffix : ''}`.trimEnd(), ) } From b82c180a6f94c6e07528111b5634ed4c746c55ec Mon Sep 17 00:00:00 2001 From: Ignacio Gea Date: Thu, 13 Feb 2020 09:21:15 -0600 Subject: [PATCH 18/87] refactor: addressing comment: use patient.fullName --- src/patients/patient-slice.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/patients/patient-slice.ts b/src/patients/patient-slice.ts index fd36ad8add..e1d8c1c406 100644 --- a/src/patients/patient-slice.ts +++ b/src/patients/patient-slice.ts @@ -67,9 +67,7 @@ export const createPatient = (patient: Patient, history: any): AppThunk => async Toast( 'success', il8n.t('Success!'), - `${il8n.t('patients.successfullyCreated')} ${patient.givenName} ${ - patient.familyName ? patient.familyName : '' - } ${patient.suffix ? patient.suffix : ''}`.trimEnd(), + `${il8n.t('patients.successfullyCreated')} ${patient.fullName}`, ) } @@ -81,9 +79,7 @@ export const updatePatient = (patient: Patient, history: any): AppThunk => async Toast( 'success', il8n.t('Success!'), - `${il8n.t('patients.successfullyUpdated')} ${patient.givenName} ${ - patient.familyName ? patient.familyName : '' - } ${patient.suffix ? patient.suffix : ''}`.trimEnd(), + `${il8n.t('patients.successfullyUpdated')} ${patient.fullName}`, ) } From 3bdd62d3361626c7b949b209986ce717791e3d4f Mon Sep 17 00:00:00 2001 From: Ignacio Gea Date: Thu, 13 Feb 2020 10:02:07 -0600 Subject: [PATCH 19/87] refactor patient-slice tests for Toast message --- src/__tests__/patients/patient-slice.test.ts | 44 +++----------------- 1 file changed, 6 insertions(+), 38 deletions(-) diff --git a/src/__tests__/patients/patient-slice.test.ts b/src/__tests__/patients/patient-slice.test.ts index f05c47e805..65f83e5185 100644 --- a/src/__tests__/patients/patient-slice.test.ts +++ b/src/__tests__/patients/patient-slice.test.ts @@ -161,14 +161,10 @@ describe('patients slice', () => { it('should call the Toaster function with the correct data', async () => { jest.spyOn(components, 'Toast') const expectedPatientId = '12345' - const expectedGivenName = 'given' - const expectedFamilyName = 'family' - const expectedSuffix = 'suffix' + const expectedFullName = 'John Doe II' const expectedPatient = { id: expectedPatientId, - givenName: expectedGivenName, - familyName: expectedFamilyName, - suffix: expectedSuffix, + fullName: expectedFullName, } as Patient const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.save.mockResolvedValue(expectedPatient) @@ -182,7 +178,7 @@ describe('patients slice', () => { expect(mockedComponents.Toast).toHaveBeenCalledWith( 'success', 'Success!', - `patients.successfullyCreated ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, + `patients.successfullyCreated ${expectedFullName}`, ) }) }) @@ -286,14 +282,10 @@ describe('patients slice', () => { it('should call the Toaster function with the correct data', async () => { jest.spyOn(components, 'Toast') const expectedPatientId = '12345' - const expectedGivenName = 'given' - const expectedFamilyName = 'family' - const expectedSuffix = 'suffix' + const fullName = 'John Doe II' const expectedPatient = { id: expectedPatientId, - givenName: expectedGivenName, - familyName: expectedFamilyName, - suffix: expectedSuffix, + fullName, } as Patient const mockedPatientRepository = mocked(PatientRepository, true) mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedPatient) @@ -307,31 +299,7 @@ describe('patients slice', () => { expect(mockedComponents.Toast).toHaveBeenCalledWith( 'success', 'Success!', - `patients.successfullyUpdated ${expectedGivenName} ${expectedFamilyName} ${expectedSuffix}`, - ) - }) - - it('should call the Toaster with message only including given name', async () => { - jest.spyOn(components, 'Toast') - const expectedPatientId = '12345' - const expectedGivenName = 'John' - const expectedPatient = { - id: expectedPatientId, - givenName: expectedGivenName, - } as Patient - const mockedPatientRepository = mocked(PatientRepository, true) - mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedPatient) - const mockedComponents = mocked(components, true) - const history = createMemoryHistory() - const dispatch = jest.fn() - const getState = jest.fn() - - await updatePatient(expectedPatient, history)(dispatch, getState, null) - - expect(mockedComponents.Toast).toHaveBeenCalledWith( - 'success', - 'Success!', - `Successfully updated patient ${expectedGivenName}`, + `patients.successfullyUpdated ${fullName}`, ) }) }) From deee00e52f2bab2fec8518d4e8cdfa6f67c2cf75 Mon Sep 17 00:00:00 2001 From: Ignacio Gea Date: Thu, 13 Feb 2020 17:39:10 -0600 Subject: [PATCH 20/87] feat(appointmentslist): add an appointments tab to the patient view add an appointments tab to the patient view which shows appointments list for corresponding patient. Uses same layout as patient list view and also implements fuzzy search #1769 --- package.json | 2 +- .../patients/view/ViewPatient.test.tsx | 5 +- src/clients/db/AppointmentsRepository.ts | 32 ++++++++ .../appointments/AppointmentsList.tsx | 75 +++++++++++++++++++ src/patients/view/ViewPatient.tsx | 9 +++ .../appointments/appointments-slice.ts | 17 +++++ 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/patients/appointments/AppointmentsList.tsx diff --git a/package.json b/package.json index 77a4098bba..1e471bccc5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "^0.33.0", + "@hospitalrun/components": "^0.33.1", "@reduxjs/toolkit": "~1.2.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index bbf2ca39c6..01eed0a4b5 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -79,7 +79,7 @@ describe('ViewPatient', () => { wrapper.update() - const editButton = wrapper.find(Button).at(2) + const editButton = wrapper.find(Button).at(3) const onClick = editButton.prop('onClick') as any expect(editButton.text().trim()).toEqual('actions.edit') @@ -120,9 +120,10 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(2) + expect(tabs).toHaveLength(3) 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') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { diff --git a/src/clients/db/AppointmentsRepository.ts b/src/clients/db/AppointmentsRepository.ts index ebfd685786..7fe1433af5 100644 --- a/src/clients/db/AppointmentsRepository.ts +++ b/src/clients/db/AppointmentsRepository.ts @@ -6,6 +6,38 @@ export class AppointmentRepository extends Repository { constructor() { super(appointments) } + + // Fuzzy search for patient appointments. Used for patient appointment search bar + async searchPatientAppointments(patientId: string, text: string): Promise { + return super.search({ + selector: { + $and: [ + { + patientId, + }, + { + $or: [ + { + location: { + $regex: RegExp(text, 'i'), + }, + }, + { + reason: { + $regex: RegExp(text, 'i'), + }, + }, + { + type: { + $regex: RegExp(text, 'i'), + }, + }, + ], + }, + ], + }, + }) + } } export default new AppointmentRepository() diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx new file mode 100644 index 0000000000..1ec79ae83a --- /dev/null +++ b/src/patients/appointments/AppointmentsList.tsx @@ -0,0 +1,75 @@ +import React, { useEffect, useState } from 'react' +import { useSelector, useDispatch } from 'react-redux' +import { useHistory } from 'react-router' +import { useTranslation } from 'react-i18next' +import { TextInput, Button, List, ListItem, Container, Row } from '@hospitalrun/components' +import { RootState } from '../../store' +import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice' + +interface Props { + patientId: string +} + +const AppointmentsList = (props: Props) => { + const dispatch = useDispatch() + const history = useHistory() + const { t } = useTranslation() + + const { patientId } = props + const { appointments } = useSelector((state: RootState) => state.appointments) + const [searchText, setSearchText] = useState('') + + useEffect(() => { + dispatch(fetchPatientAppointments(patientId)) + }, [dispatch, patientId]) + + const list = ( + // inline style added to pick up on newlines for string literal +
    + {appointments.map((a) => ( + history.push(`/appointments/${a.id}`)}> + {`${t('scheduling.appointment.location')}: ${a.location} + ${t('scheduling.appointment.reason')}: ${a.reason} + ${t('scheduling.appointment.type')}: ${a.type} + ${t('scheduling.appointment.startDate')}: ${new Date(a.startDateTime).toLocaleString()} + ${t('scheduling.appointment.endDate')}: ${new Date(a.endDateTime).toLocaleString()}`} + + ))} +
+ ) + + const onSearchBoxChange = (event: React.ChangeEvent) => { + setSearchText(event.target.value) + } + + const onSearchFormSubmit = (event: React.FormEvent | React.MouseEvent) => { + event.preventDefault() + dispatch(fetchPatientAppointments(patientId, searchText)) + } + + return ( + +
+
+ +
+ +
+
+
+ + + + {list} + + +
+ ) +} + +export default AppointmentsList diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index bb3a6e935a..814ef09840 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -11,6 +11,7 @@ import { getPatientFullName } from '../util/patient-name-util' import Patient from '../../model/Patient' import GeneralInformation from '../GeneralInformation' import RelatedPerson from '../related-persons/RelatedPersonTab' +import AppointmentsList from '../appointments/AppointmentsList' const getFriendlyId = (p: Patient): string => { if (p) { @@ -54,6 +55,11 @@ const ViewPatient = () => { label={t('patient.relatedPersons.label')} onClick={() => history.push(`/patients/${patient.id}/relatedpersons`)} /> + history.push(`/patients/${patient.id}/appointments`)} + /> @@ -76,6 +82,9 @@ const ViewPatient = () => { + + +
) diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index 1e20e1dbb8..fd90b05537 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -42,6 +42,23 @@ export const fetchAppointments = (): AppThunk => async (dispatch) => { dispatch(fetchAppointmentsSuccess(appointments)) } +export const fetchPatientAppointments = ( + patientId: string, + searchString?: string, +): AppThunk => async (dispatch) => { + dispatch(fetchAppointmentsStart()) + + let appointments + if (searchString === undefined || searchString.trim() === '') { + const query = { selector: { patientId } } + appointments = await AppointmentRepository.search(query) + } else { + appointments = await AppointmentRepository.searchPatientAppointments(patientId, searchString) + } + + dispatch(fetchAppointmentsSuccess(appointments)) +} + export const createAppointment = (appointment: Appointment, history: any): AppThunk => async ( dispatch, ) => { From b715358f3e1c0d5c25ffd987e9f0e0561ceeca6b Mon Sep 17 00:00:00 2001 From: Ignacio Gea Date: Fri, 14 Feb 2020 10:43:42 -0600 Subject: [PATCH 21/87] adress review comments --- src/components/Sidebar.tsx | 7 ++++--- src/patients/appointments/AppointmentsList.tsx | 6 +----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index c87f07602b..c3782baa80 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -7,6 +7,7 @@ const Sidebar = () => { const { t } = useTranslation() const path = useLocation() const history = useHistory() + const pathname = path.pathname const navigateTo = (location: string) => { history.push(location) @@ -21,7 +22,7 @@ const Sidebar = () => {
navigateTo('/')} className="nav-item" style={listItemStyle} @@ -29,7 +30,7 @@ const Sidebar = () => { {t('dashboard.label')} navigateTo('/patients')} className="nav-item" style={listItemStyle} @@ -37,7 +38,7 @@ const Sidebar = () => { {t('patients.label')} navigateTo('/appointments')} className="nav-item" style={listItemStyle} diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx index 1ec79ae83a..feb0f9189c 100644 --- a/src/patients/appointments/AppointmentsList.tsx +++ b/src/patients/appointments/AppointmentsList.tsx @@ -28,11 +28,7 @@ const AppointmentsList = (props: Props) => {
    {appointments.map((a) => ( history.push(`/appointments/${a.id}`)}> - {`${t('scheduling.appointment.location')}: ${a.location} - ${t('scheduling.appointment.reason')}: ${a.reason} - ${t('scheduling.appointment.type')}: ${a.type} - ${t('scheduling.appointment.startDate')}: ${new Date(a.startDateTime).toLocaleString()} - ${t('scheduling.appointment.endDate')}: ${new Date(a.endDateTime).toLocaleString()}`} + {new Date(a.startDateTime).toLocaleString()} ))}
From 72c24f9756faaa223656e51c23f75c72fcd47656 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Fri, 14 Feb 2020 18:32:00 +0100 Subject: [PATCH 22/87] build(docker): improves dockerfile and updates dockerignore --- .dockerignore | 20 +++++++++++++++++++- .travis.yml | 2 +- Dockerfile | 36 ++++++------------------------------ nginx.conf | 15 +++++++++++++++ package.json | 1 - 5 files changed, 41 insertions(+), 33 deletions(-) create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore index 3c3629e647..cb6ae5b604 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,19 @@ -node_modules +# Items that don't need to be in a Docker image. +# Anything not used by the build system should go here. +.gitignore +*.md +*.yml +.git +*.js +azure +.editorconfig +.github +.npmrc +.nvmrc +.prettierrc +.replit +.vscode +LICENSE +docs +*.log +.env* diff --git a/.travis.yml b/.travis.yml index e07ec5f55c..49e8f2df19 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: node_js node_js: - - "lts/*" + - 'lts/*' branches: only: diff --git a/Dockerfile b/Dockerfile index 708546b211..0be4980bdd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,41 +1,17 @@ -FROM node:10-alpine as build -LABEL maintainer="Michael Feher, Matteo Vivona, Maksim Sinik" +FROM node:12-alpine as build -# set app basepath ENV HOME=/home/app - -# copy all app files COPY . $HOME/node/ -# change workgin dir and install deps in quiet mode WORKDIR $HOME/node RUN npm ci -q -# compile typescript and build all production stuff RUN npm run build - -# remove dev dependencies that are not needed in production RUN npm prune --production -# start new image for lower size -FROM node:10-alpine - -# create use with no permissions -RUN addgroup -g 101 -S app && adduser -u 100 -S -G app -s /bin/false app - -# set app basepath -ENV HOME=/home/app - -# copy production complied node app to the new image -COPY --from=build $HOME/node/ $HOME/node/ -RUN chown -R app:app $HOME/* - -# run app with low permissions level user -USER app -WORKDIR $HOME/node - -EXPOSE 3000 - -ENV NODE_ENV=production +FROM nginx:stable-alpine -CMD [ "yarn", "start" ] +COPY --from=build /home/app/node/build/ /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000000..2400141c1e --- /dev/null +++ b/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + server_name localhost; + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri /index.html; + } + + error_page 500 502 503 504 /50x.html; + + location = /50x.html { + root /usr/share/nginx/html; + } +} diff --git a/package.json b/package.json index 77a4098bba..f653727bf7 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,6 @@ "commit": "npx git-cz", "start": "react-scripts start", "build": "react-scripts build", - "start:serve": "npm run build && serve -s build", "prepublishOnly": "npm run build", "test": "react-scripts test --detectOpenHandles", "test:ci": "cross-env CI=true react-scripts test", From d7906d4a4fd710e51b4ec3833bf1010b77ac166c Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 11:04:55 +0100 Subject: [PATCH 23/87] ci(build): removes azure pipeline and updates github ci --- .github/workflows/ci.yml | 26 +++++++++++++++++++------- azure-pipeline.yml | 22 ---------------------- azure/azure-pipelines-npm.yml | 22 ---------------------- azure/azure-pipelines-yarn.yml | 22 ---------------------- 4 files changed, 19 insertions(+), 73 deletions(-) delete mode 100644 azure-pipeline.yml delete mode 100644 azure/azure-pipelines-npm.yml delete mode 100644 azure/azure-pipelines-yarn.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 943df5a20e..254e5dbe70 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,16 +9,17 @@ jobs: strategy: matrix: node-version: [12.x, 13.x] - os: [ubuntu-18.04] + os: [ubuntu-latest, windows-latest, macOS-latest] + exclude: + - os: windows-latest + node-version: 13.x steps: - uses: actions/checkout@v1 - - name: Use Node.js uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - name: Install with npm run: | npm install @@ -31,22 +32,24 @@ jobs: - name: Run tests run: | npm run test:ci + # - name: Coveralls Parallel + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.GH_TOKEN }} + # parallel: true + yarn: runs-on: ${{ matrix.os }} - strategy: matrix: node-version: [12.x, 13.x] os: [ubuntu-18.04] - steps: - uses: actions/checkout@v1 - - name: Use Node.js uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - - name: Install with yarn run: | curl -o- -L https://yarnpkg.com/install.sh | bash @@ -60,3 +63,12 @@ jobs: - name: Run tests run: | yarn test:ci + # coverage: + # needs: test + # runs-on: ubuntu-latest + # steps: + # - name: Coveralls Finished + # uses: coverallsapp/github-action@master + # with: + # github-token: ${{ secrets.GH_TOKEN }} + # parallel-finished: true diff --git a/azure-pipeline.yml b/azure-pipeline.yml deleted file mode 100644 index 7b38dfedfa..0000000000 --- a/azure-pipeline.yml +++ /dev/null @@ -1,22 +0,0 @@ -trigger: - branches: - include: - - master - -jobs: - - template: azure/azure-pipelines-npm.yml - parameters: - name: Windows_npm - vmImage: vs2017-win2016 - - template: azure/azure-pipelines-npm.yml - parameters: - name: macOs_npm - vmImage: macOS-10.14 - - template: azure/azure-pipelines-yarn.yml - parameters: - name: Windows_yarn - vmImage: vs2017-win2016 - - template: azure/azure-pipelines-yarn.yml - parameters: - name: macOS_yarn - vmImage: macOS-10.14 diff --git a/azure/azure-pipelines-npm.yml b/azure/azure-pipelines-npm.yml deleted file mode 100644 index c503c9e0c9..0000000000 --- a/azure/azure-pipelines-npm.yml +++ /dev/null @@ -1,22 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - - strategy: - matrix: - node_12_x: - node_version: 12.x - maxParallel: 5 - - steps: - - task: NodeTool@0 - inputs: - versionSpec: $(node_version) - displayName: Install Node.js - - - bash: npm i - displayName: Install dependencies - - - bash: npm run build - displayName: Lint and Build diff --git a/azure/azure-pipelines-yarn.yml b/azure/azure-pipelines-yarn.yml deleted file mode 100644 index c220d502fd..0000000000 --- a/azure/azure-pipelines-yarn.yml +++ /dev/null @@ -1,22 +0,0 @@ -jobs: - - job: ${{ parameters.name }} - pool: - vmImage: ${{ parameters.vmImage }} - - strategy: - matrix: - node_12_x: - node_version: 12.x - maxParallel: 5 - - steps: - - task: NodeTool@0 - inputs: - versionSpec: $(node_version) - displayName: Install Node.js - - - bash: yarn install - displayName: Install dependencies - - - bash: yarn build - displayName: Lint and Build From 1fbd9658b4869d7f98b4ae918e731012e4b4b9a3 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 11:13:14 +0100 Subject: [PATCH 24/87] fix(prettier): changes endofline option --- .github/workflows/ci.yml | 6 +++--- .prettierrc | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 254e5dbe70..9ebd32b7a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,9 +10,9 @@ jobs: matrix: node-version: [12.x, 13.x] os: [ubuntu-latest, windows-latest, macOS-latest] - exclude: - - os: windows-latest - node-version: 13.x + # exclude: + # - os: windows-latest + # node-version: 13.x steps: - uses: actions/checkout@v1 diff --git a/.prettierrc b/.prettierrc index 8231751c8b..b05e85d771 100644 --- a/.prettierrc +++ b/.prettierrc @@ -7,5 +7,5 @@ "bracketSpacing": true, "jsxBracketSameLine": false, "arrowParens": "always", - "endOfLine": "lf" + "endOfLine": "auto" } From 8796c7f803663edf36a54a652fb0154e1d37a120 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 11:29:36 +0100 Subject: [PATCH 25/87] build(build): updates github ci strategy --- .github/workflows/ci.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9ebd32b7a7..7cb8362604 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,7 +5,6 @@ on: [push, pull_request] jobs: npm: runs-on: ${{ matrix.os }} - strategy: matrix: node-version: [12.x, 13.x] @@ -13,7 +12,6 @@ jobs: # exclude: # - os: windows-latest # node-version: 13.x - steps: - uses: actions/checkout@v1 - name: Use Node.js @@ -43,7 +41,10 @@ jobs: strategy: matrix: node-version: [12.x, 13.x] - os: [ubuntu-18.04] + os: [ubuntu-latest, windows-latest, macOS-latest] + # exclude: + # - os: windows-latest + # node-version: 13.x steps: - uses: actions/checkout@v1 - name: Use Node.js From e89265a37d3edeab51ef17549ee75ea24bfef331 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 12:46:40 +0100 Subject: [PATCH 26/87] docs(readme): removes azure pipeline badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index afa127194b..3237cfa5f6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,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) [![Build Status](https://dev.azure.com/HospitalRun/hospitalrun-frontend/_apis/build/status/HospitalRun.hospitalrun-frontend?branchName=master)](https://dev.azure.com/HospitalRun/hospitalrun-frontend/_build/latest?definitionId=3&branchName=master) [![Build Status](https://travis-ci.com/HospitalRun/hospitalrun-frontend.svg?branch=master)](https://travis-ci.com/HospitalRun/hospitalrun-frontend) [![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) [![Documentation Status](https://readthedocs.org/projects/hospitalrun-frontend/badge/?version=latest)](https://hospitalrun-frontend.readthedocs.io) +[![GitHub CI](https://github.com/HospitalRun/frontend/workflows/GitHub%20CI/badge.svg)](https://github.com/HospitalRun/frontend/actions) [![Build Status](https://travis-ci.com/HospitalRun/hospitalrun-frontend.svg?branch=master)](https://travis-ci.com/HospitalRun/hospitalrun-frontend) [![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) [![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) [![Join the community on Spectrum](https://withspectrum.github.io/badge/badge.svg)](https://spectrum.chat/hospitalrun) [![Run on Repl.it](https://repl.it/badge/github/HospitalRun/hospitalrun-frontend)](https://repl.it/github/HospitalRun/hospitalrun-frontend) From 76f57e3d4ddd9328eb849b8f424675bd689b8ff3 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 13:15:57 +0100 Subject: [PATCH 27/87] ci(azure): adds new azure pipeline yaml --- azure.yaml | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 azure.yaml diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000000..54eefc3aa4 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,80 @@ +trigger: + branches: + include: + - refs/tags/v* + +jobs: + - job: npm_job + continueOnError: false + pool: + vmImage: ubuntu-latest + strategy: + matrix: + node_12_x: + node_version: 12.x + maxParallel: 5 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - bash: npm install + displayName: 'Install dependencies' + - bash: npm lint + displayName: 'Lint code' + - bash: npm run build + displayName: 'Build code' + - bash: npm run test:ci + displayName: 'Test compiled code' + + - job: yarn_job + continueOnError: false + pool: + vmImage: ubuntu-latest + strategy: + matrix: + node_12_x: + node_version: 12.x + maxParallel: 5 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - bash: yarn install + displayName: 'Install dependencies' + - bash: yarn lint + displayName: 'Lint code' + - bash: yarn build + displayName: 'Build code' + - bash: yarn test:ci + displayName: 'Test compiled code' + + - job: docker_job + dependsOn: [npm_job, yarn_job] + continueOnError: false + pool: + vmImage: ubuntu-latest + steps: + - script: | + echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json + sudo service docker restart + displayName: 'Enable Docker Engine experimental ' + - script: | + GIT_TAG=`git describe --tags` && VERSION_TAG="$(cut -d'-' -f1 <<<"$GIT_TAG")" && echo "##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG" + displayName: 'Get Git Tag' + - task: Docker@0 + displayName: 'Build an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: Docker + dockerFile: ./Dockerfile + buildArguments: '--rm --squash' + imageName: '$(Build.Repository.Name):$(VERSION_TAG)' + - task: Docker@0 + displayName: 'Push an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: Docker + action: 'Push an image' + imageName: '$(Build.Repository.Name):$(VERSION_TAG)' From 93d8c4df510c49542c8cac3b5e56cf6b11b169a7 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 13:22:27 +0100 Subject: [PATCH 28/87] ci(azure): updates azure pipeline yaml --- azure.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/azure.yaml b/azure.yaml index 54eefc3aa4..9ae7fdfbdf 100644 --- a/azure.yaml +++ b/azure.yaml @@ -3,6 +3,8 @@ trigger: include: - refs/tags/v* +pr: none + jobs: - job: npm_job continueOnError: false @@ -20,7 +22,7 @@ jobs: displayName: 'Install Node.js' - bash: npm install displayName: 'Install dependencies' - - bash: npm lint + - bash: npm run lint displayName: 'Lint code' - bash: npm run build displayName: 'Build code' @@ -67,7 +69,7 @@ jobs: displayName: 'Build an image' inputs: containerregistrytype: 'Container Registry' - dockerRegistryConnection: Docker + dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal dockerFile: ./Dockerfile buildArguments: '--rm --squash' imageName: '$(Build.Repository.Name):$(VERSION_TAG)' @@ -75,6 +77,6 @@ jobs: displayName: 'Push an image' inputs: containerregistrytype: 'Container Registry' - dockerRegistryConnection: Docker + dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal action: 'Push an image' imageName: '$(Build.Repository.Name):$(VERSION_TAG)' From 7907fbb6616c41a0cc6583575b9c8ae52f9003ce Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 13:48:18 +0100 Subject: [PATCH 29/87] chore(release): 2.0.0-alpha.2 --- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ package.json | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32f339281f..31772c6db3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,35 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.0.0-alpha.2](https://github.com/HospitalRun/hospitalrun-frontend/compare/v2.0.0-alpha.1...v2.0.0-alpha.2) (2020-02-15) + + +### Features + +* **appointmentslist:** add an appointments tab to the patient view ([deee00e](https://github.com/HospitalRun/hospitalrun-frontend/commit/deee00e52f2bab2fec8518d4e8cdfa6f67c2cf75)), closes [#1769](https://github.com/HospitalRun/hospitalrun-frontend/issues/1769) +* **edit patient:** implement Edit Patient functionality ([8e3355f](https://github.com/HospitalRun/hospitalrun-frontend/commit/8e3355f2124186b6ead1a710abb3010695e51abf)) +* **edit patient:** moved buttons out of GeneralInformation ([403e49f](https://github.com/HospitalRun/hospitalrun-frontend/commit/403e49feb130d16718fb014e1a27ea218915bb7e)) +* **env:** adds hospitalrun server information ([7f0fe7f](https://github.com/HospitalRun/hospitalrun-frontend/commit/7f0fe7fa47d0705d585f7ca15d4796e1c90c2f16)) +* **env:** change env variable names ([cddc812](https://github.com/HospitalRun/hospitalrun-frontend/commit/cddc812ae5bd1f1b05e7310b5a504b51ecee1981)) +* add documentation folder ([d22300e](https://github.com/HospitalRun/hospitalrun-frontend/commit/d22300e8a56be56c7edef6f914d7b9c5381aea7f)) +* **navigation:** navigate to patients profile on related person click ([c6acecc](https://github.com/HospitalRun/hospitalrun-frontend/commit/c6acecc3c89b0aeb96fa6c6fea15b316ee0669a2)), closes [#1763](https://github.com/HospitalRun/hospitalrun-frontend/issues/1763) +* **relatedpersontab:** add cursor icon to related persons list ([ef7e19c](https://github.com/HospitalRun/hospitalrun-frontend/commit/ef7e19cabf596afd08d4e15449a96285541149d1)), closes [#1792](https://github.com/HospitalRun/hospitalrun-frontend/issues/1792) +* **test:** add navigate to related person profile onclick test ([29fbffe](https://github.com/HospitalRun/hospitalrun-frontend/commit/29fbffec0848f0e0fed54b133513cfb1471ddacb)), closes [#1792](https://github.com/HospitalRun/hospitalrun-frontend/issues/1792) + + +### Bug Fixes + +* **patient-slice.ts:** conditionally render family name and suffix ([d20e294](https://github.com/HospitalRun/hospitalrun-frontend/commit/d20e294340a155cb90138ea9bf5210e6e697ea71)), closes [#1818](https://github.com/HospitalRun/hospitalrun-frontend/issues/1818) +* **patients:** add test for displaying No Related Persons warning ([da6bdb1](https://github.com/HospitalRun/hospitalrun-frontend/commit/da6bdb19e3609e1d8fca6d08349c71814162f536)), closes [#1789](https://github.com/HospitalRun/hospitalrun-frontend/issues/1789) +* **patients:** internationalize No Related Persons warning & loading ([099e50d](https://github.com/HospitalRun/hospitalrun-frontend/commit/099e50d846e1574dff2b28aafd77b7d01c4da955)), closes [#1789](https://github.com/HospitalRun/hospitalrun-frontend/issues/1789) +* **patients:** replace "Loading..." text with Spinner component ([e6ce4cb](https://github.com/HospitalRun/hospitalrun-frontend/commit/e6ce4cb979d3f3cd7c3704490ad4251955f4922f)), closes [#1789](https://github.com/HospitalRun/hospitalrun-frontend/issues/1789) +* **patients:** stop "Loading..." when patient has no related persons ([e513b17](https://github.com/HospitalRun/hospitalrun-frontend/commit/e513b172e25f24126969564a0e7d4f421758e626)), closes [#1789](https://github.com/HospitalRun/hospitalrun-frontend/issues/1789) +* **persons:** replace "No related persons" message with a warning ([c156b5b](https://github.com/HospitalRun/hospitalrun-frontend/commit/c156b5bba25b008be3bd7d11cd393e2f12fb49cb)), closes [#1789](https://github.com/HospitalRun/hospitalrun-frontend/issues/1789) +* **prettier:** changes endofline option ([1fbd965](https://github.com/HospitalRun/hospitalrun-frontend/commit/1fbd9658b4869d7f98b4ae918e731012e4b4b9a3)) +* **test:** add related test, and revert test changes ([9bead70](https://github.com/HospitalRun/hospitalrun-frontend/commit/9bead7078f8ea94680f24d865f576ae786ce5178)), closes [#1792](https://github.com/HospitalRun/hospitalrun-frontend/issues/1792) +* **test:** remove extra whitespace ([155b4e9](https://github.com/HospitalRun/hospitalrun-frontend/commit/155b4e939151beb994808bae22dd2cd74845da39)), closes [#1792](https://github.com/HospitalRun/hospitalrun-frontend/issues/1792) +* **test:** remove unused import ([fc3a78d](https://github.com/HospitalRun/hospitalrun-frontend/commit/fc3a78d70f285a120a9d0f690320d6c6fa5e5696)), closes [#1792](https://github.com/HospitalRun/hospitalrun-frontend/issues/1792) + ## 2.0.0-alpha.1 (2020-02-07) diff --git a/package.json b/package.json index 92b7e3378c..cc8817b34b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hospitalrun/frontend", - "version": "2.0.0-alpha.1", + "version": "2.0.0-alpha.2", "description": "React frontend for HospitalRun", "private": false, "license": "MIT", From 814e712be03fa7ccbe587a9c7ef8b8cda4a12f7f Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 13:54:44 +0100 Subject: [PATCH 30/87] fix(build): fixes yarn install on github ci --- .github/workflows/ci.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7cb8362604..d577ead329 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,9 +51,11 @@ jobs: uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} + - name: Init yarn + run: | + npm install -g yarn - name: Install with yarn run: | - curl -o- -L https://yarnpkg.com/install.sh | bash yarn install - name: Lint code run: | @@ -61,6 +63,9 @@ jobs: - name: Build run: | yarn build + - name: Storybook build + run: | + yarn build-storybook - name: Run tests run: | yarn test:ci From 12ca8dea33e954de31a83b1f2d94564587b3297b Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 14:01:05 +0100 Subject: [PATCH 31/87] build(docker): updates install method removing c option on npm i command --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 0be4980bdd..cf8345253b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ ENV HOME=/home/app COPY . $HOME/node/ WORKDIR $HOME/node -RUN npm ci -q +RUN npm install -q RUN npm run build RUN npm prune --production From eaf28e429e29e2cc79b34b8936e1077d69abb650 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 14:06:42 +0100 Subject: [PATCH 32/87] build(azure): adds stages on azure pipeline yaml --- azure.yaml | 151 +++++++++++++++++++++++++++-------------------------- 1 file changed, 77 insertions(+), 74 deletions(-) diff --git a/azure.yaml b/azure.yaml index 9ae7fdfbdf..f5cdfa2cc4 100644 --- a/azure.yaml +++ b/azure.yaml @@ -5,78 +5,81 @@ trigger: pr: none -jobs: - - job: npm_job - continueOnError: false - pool: - vmImage: ubuntu-latest - strategy: - matrix: - node_12_x: - node_version: 12.x - maxParallel: 5 - steps: - - task: NodeTool@0 - inputs: - versionSpec: $(node_version) - displayName: 'Install Node.js' - - bash: npm install - displayName: 'Install dependencies' - - bash: npm run lint - displayName: 'Lint code' - - bash: npm run build - displayName: 'Build code' - - bash: npm run test:ci - displayName: 'Test compiled code' +stages: + - stage: Build, lint and test code + jobs: + - job: npm_job + continueOnError: false + pool: + vmImage: ubuntu-latest + strategy: + matrix: + node_12_x: + node_version: 12.x + maxParallel: 5 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - bash: npm install + displayName: 'Install dependencies' + - bash: npm run lint + displayName: 'Lint code' + - bash: npm run build + displayName: 'Build code' + - bash: npm run test:ci + displayName: 'Test compiled code' + - job: yarn_job + continueOnError: false + pool: + vmImage: ubuntu-latest + strategy: + matrix: + node_12_x: + node_version: 12.x + maxParallel: 5 + steps: + - task: NodeTool@0 + inputs: + versionSpec: $(node_version) + displayName: 'Install Node.js' + - bash: yarn install + displayName: 'Install dependencies' + - bash: yarn lint + displayName: 'Lint code' + - bash: yarn build + displayName: 'Build code' + - bash: yarn test:ci + displayName: 'Test compiled code' - - job: yarn_job - continueOnError: false - pool: - vmImage: ubuntu-latest - strategy: - matrix: - node_12_x: - node_version: 12.x - maxParallel: 5 - steps: - - task: NodeTool@0 - inputs: - versionSpec: $(node_version) - displayName: 'Install Node.js' - - bash: yarn install - displayName: 'Install dependencies' - - bash: yarn lint - displayName: 'Lint code' - - bash: yarn build - displayName: 'Build code' - - bash: yarn test:ci - displayName: 'Test compiled code' - - - job: docker_job - dependsOn: [npm_job, yarn_job] - continueOnError: false - pool: - vmImage: ubuntu-latest - steps: - - script: | - echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json - sudo service docker restart - displayName: 'Enable Docker Engine experimental ' - - script: | - GIT_TAG=`git describe --tags` && VERSION_TAG="$(cut -d'-' -f1 <<<"$GIT_TAG")" && echo "##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG" - displayName: 'Get Git Tag' - - task: Docker@0 - displayName: 'Build an image' - inputs: - containerregistrytype: 'Container Registry' - dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal - dockerFile: ./Dockerfile - buildArguments: '--rm --squash' - imageName: '$(Build.Repository.Name):$(VERSION_TAG)' - - task: Docker@0 - displayName: 'Push an image' - inputs: - containerregistrytype: 'Container Registry' - dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal - action: 'Push an image' - imageName: '$(Build.Repository.Name):$(VERSION_TAG)' + - stage: Docker image build and push + jobs: + - job: docker_job + dependsOn: [npm_job, yarn_job] + continueOnError: false + pool: + vmImage: ubuntu-latest + steps: + - script: | + echo '{ "experimental": true }' | sudo tee /etc/docker/daemon.json + sudo service docker restart + displayName: 'Enable Docker Engine experimental ' + - script: | + GIT_TAG=`git describe --tags` && VERSION_TAG="$(cut -d'-' -f1 <<<"$GIT_TAG")" && echo "##vso[task.setvariable variable=VERSION_TAG]$VERSION_TAG" + displayName: 'Get Git Tag' + - task: Docker@0 + displayName: 'Build an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal + dockerFile: ./Dockerfile + buildArguments: '--rm --squash' + imageName: '$(Build.Repository.Name):$(VERSION_TAG)' + - task: Docker@0 + displayName: 'Push an image' + inputs: + containerregistrytype: 'Container Registry' + dockerRegistryConnection: Docker # it is necessary to create a new "service connection" via Azure DevOps portal + action: 'Push an image' + imageName: '$(Build.Repository.Name):$(VERSION_TAG)' From eb55decad73ecd0e2afbc2a1049747fbe6074ce1 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 14:10:22 +0100 Subject: [PATCH 33/87] build(azure): fixes stage name on azure pipeline yaml --- azure.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure.yaml b/azure.yaml index f5cdfa2cc4..4930c93cae 100644 --- a/azure.yaml +++ b/azure.yaml @@ -6,7 +6,7 @@ trigger: pr: none stages: - - stage: Build, lint and test code + - stage: Build jobs: - job: npm_job continueOnError: false @@ -53,7 +53,7 @@ stages: - bash: yarn test:ci displayName: 'Test compiled code' - - stage: Docker image build and push + - stage: Docker jobs: - job: docker_job dependsOn: [npm_job, yarn_job] From d57539a5fa2453ec7af7d32edb79c21d4a33c3ac Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 14:16:02 +0100 Subject: [PATCH 34/87] build(azure): improves azure pipeline stage conditions --- azure.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure.yaml b/azure.yaml index 4930c93cae..fd12bc8a1d 100644 --- a/azure.yaml +++ b/azure.yaml @@ -56,7 +56,8 @@ stages: - stage: Docker jobs: - job: docker_job - dependsOn: [npm_job, yarn_job] + dependsOn: Build + condition: succeeded('Build') continueOnError: false pool: vmImage: ubuntu-latest From f5f3b29eb2cb373cff160ab64bc640cdf39243e7 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sat, 15 Feb 2020 14:22:27 +0100 Subject: [PATCH 35/87] build(azure): fixes docker stage --- azure.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/azure.yaml b/azure.yaml index fd12bc8a1d..febd24e57c 100644 --- a/azure.yaml +++ b/azure.yaml @@ -56,7 +56,6 @@ stages: - stage: Docker jobs: - job: docker_job - dependsOn: Build condition: succeeded('Build') continueOnError: false pool: From 5beda756d134439911cd4a412147a66e1b222176 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 15 Feb 2020 14:58:00 +0100 Subject: [PATCH 36/87] feat(breadcrumb): add hook useAddBreadcrumbs / sort breadcrumbs create a single hook useAddBreadcrumbs for breadcrumbs, sor breadcrumbs by their location's length fix #1770 --- package.json | 3 ++- .../breadcrumbs/Breadcrumbs.test.tsx | 5 +++++ .../breadcrumbs/breadcrumbs-slice.test.ts | 5 +++++ .../breadcrumbs/useAddBreadcrumbs.test.ts | 5 +++++ src/breadcrumbs/Breadcrumbs.tsx | 21 +++++++++++-------- src/breadcrumbs/breadcrumbs-slice.ts | 16 +++++++------- src/breadcrumbs/useAddBreadcrumb.ts | 16 -------------- src/breadcrumbs/useAddBreadcrumbs.ts | 16 ++++++++++++++ src/breadcrumbs/useSetBreadcrumbs.ts | 16 -------------- src/dashboard/Dashboard.tsx | 4 ++-- .../appointments/AppointmentsList.tsx | 9 ++++++++ src/patients/edit/EditPatient.tsx | 19 +++++++---------- src/patients/list/Patients.tsx | 4 ++-- src/patients/new/NewPatient.tsx | 4 ++-- .../related-persons/RelatedPersonTab.tsx | 9 ++++++++ src/patients/view/ViewPatient.tsx | 17 +++++++-------- src/scheduling/appointments/Appointments.tsx | 4 ++-- .../appointments/new/NewAppointment.tsx | 4 ++-- .../appointments/view/ViewAppointment.tsx | 17 +++++++-------- 19 files changed, 103 insertions(+), 91 deletions(-) create mode 100644 src/__tests__/breadcrumbs/Breadcrumbs.test.tsx create mode 100644 src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts create mode 100644 src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts delete mode 100644 src/breadcrumbs/useAddBreadcrumb.ts create mode 100644 src/breadcrumbs/useAddBreadcrumbs.ts delete mode 100644 src/breadcrumbs/useSetBreadcrumbs.ts diff --git a/package.json b/package.json index b838c42ba3..643e7e271f 100644 --- a/package.json +++ b/package.json @@ -126,7 +126,8 @@ }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ - "npm run lint:fix", + "npm run lint:fix", + "npm run test:ci", "git add ." ] } diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx new file mode 100644 index 0000000000..98529d829a --- /dev/null +++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx @@ -0,0 +1,5 @@ +import '../../__mocks__/matchMediaMock' + +it('should return true', () => { + expect(true).toBeTruthy() +}) diff --git a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts new file mode 100644 index 0000000000..98529d829a --- /dev/null +++ b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts @@ -0,0 +1,5 @@ +import '../../__mocks__/matchMediaMock' + +it('should return true', () => { + expect(true).toBeTruthy() +}) diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts new file mode 100644 index 0000000000..98529d829a --- /dev/null +++ b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts @@ -0,0 +1,5 @@ +import '../../__mocks__/matchMediaMock' + +it('should return true', () => { + expect(true).toBeTruthy() +}) diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx index a7116c57f1..99ae7a0648 100644 --- a/src/breadcrumbs/Breadcrumbs.tsx +++ b/src/breadcrumbs/Breadcrumbs.tsx @@ -15,16 +15,19 @@ const Breadcrumbs = () => { return ( - {breadcrumbs.map(({ i18nKey, text, location }, index) => { - const isLast = index === breadcrumbs.length - 1 - const onClick = !isLast ? () => history.push(location) : undefined + {breadcrumbs + .slice() + .sort((b1, b2) => b1.location.length - b2.location.length) + .map(({ i18nKey, text, location }, index) => { + const isLast = index === breadcrumbs.length - 1 + const onClick = !isLast ? () => history.push(location) : undefined - return ( - - {i18nKey ? t(i18nKey) : text} - - ) - })} + return ( + + {i18nKey ? t(i18nKey) : text} + + ) + })} ) } diff --git a/src/breadcrumbs/breadcrumbs-slice.ts b/src/breadcrumbs/breadcrumbs-slice.ts index 80d0c7adef..2cc55424a3 100644 --- a/src/breadcrumbs/breadcrumbs-slice.ts +++ b/src/breadcrumbs/breadcrumbs-slice.ts @@ -13,18 +13,18 @@ const breadcrumbsSlice = createSlice({ name: 'breadcrumbs', initialState, reducers: { - setBreadcrumbs(state, { payload }: PayloadAction) { - state.breadcrumbs = payload + addBreadcrumbs(state, { payload }: PayloadAction) { + state.breadcrumbs = [...state.breadcrumbs, ...payload] }, - addBreadcrumb(state, { payload }: PayloadAction) { - state.breadcrumbs = [...state.breadcrumbs, payload] - }, - removeBreadcrumb(state) { - state.breadcrumbs = state.breadcrumbs.slice(0, -1) + removeBreadcrumbs(state, { payload }: PayloadAction) { + const locations = payload.map((b) => b.location) + state.breadcrumbs = state.breadcrumbs.filter( + (breadcrumb) => !locations.includes(breadcrumb.location), + ) }, }, }) -export const { setBreadcrumbs, addBreadcrumb, removeBreadcrumb } = breadcrumbsSlice.actions +export const { addBreadcrumbs, removeBreadcrumbs } = breadcrumbsSlice.actions export default breadcrumbsSlice.reducer diff --git a/src/breadcrumbs/useAddBreadcrumb.ts b/src/breadcrumbs/useAddBreadcrumb.ts deleted file mode 100644 index 7838e134af..0000000000 --- a/src/breadcrumbs/useAddBreadcrumb.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' -import Breadcrumb from 'model/Breadcrumb' -import { addBreadcrumb, removeBreadcrumb } from './breadcrumbs-slice' - -export default function useAddBreadcrumb(breadcrumb: Breadcrumb): void { - const dispatch = useDispatch() - - useEffect(() => { - dispatch(addBreadcrumb(breadcrumb)) - - return () => { - dispatch(removeBreadcrumb()) - } - }, [dispatch, breadcrumb]) -} diff --git a/src/breadcrumbs/useAddBreadcrumbs.ts b/src/breadcrumbs/useAddBreadcrumbs.ts new file mode 100644 index 0000000000..b5e7f521d7 --- /dev/null +++ b/src/breadcrumbs/useAddBreadcrumbs.ts @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { useDispatch } from 'react-redux' +import Breadcrumb from 'model/Breadcrumb' +import { addBreadcrumbs, removeBreadcrumbs } from './breadcrumbs-slice' + +export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[]): void { + const dispatch = useDispatch() + + useEffect(() => { + dispatch(addBreadcrumbs(breadcrumbs)) + + return () => { + dispatch(removeBreadcrumbs(breadcrumbs)) + } + }, [breadcrumbs, dispatch, JSON.stringify(breadcrumbs)]) +} diff --git a/src/breadcrumbs/useSetBreadcrumbs.ts b/src/breadcrumbs/useSetBreadcrumbs.ts deleted file mode 100644 index e48434cade..0000000000 --- a/src/breadcrumbs/useSetBreadcrumbs.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useEffect } from 'react' -import { useDispatch } from 'react-redux' -import Breadcrumb from 'model/Breadcrumb' -import { setBreadcrumbs } from './breadcrumbs-slice' - -export default function useSetBreadcrumbs(breadcrumbs: Breadcrumb[]): void { - const dispatch = useDispatch() - - useEffect(() => { - dispatch(setBreadcrumbs(breadcrumbs)) - - return () => { - dispatch(setBreadcrumbs([])) - } - }, [dispatch, breadcrumbs]) -} diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 2b46dee8cf..1324a028df 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,14 +1,14 @@ import React from 'react' import { useTranslation } from 'react-i18next' import useTitle from '../page-header/useTitle' -import useSetBreadcrumbs from '../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' const breadcrumbs = [{ i18nKey: 'dashboard.label', location: '/' }] const Dashboard: React.FC = () => { const { t } = useTranslation() useTitle(t('dashboard.label')) - useSetBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs) return

Example

} diff --git a/src/patients/appointments/AppointmentsList.tsx b/src/patients/appointments/AppointmentsList.tsx index feb0f9189c..7224bce5f8 100644 --- a/src/patients/appointments/AppointmentsList.tsx +++ b/src/patients/appointments/AppointmentsList.tsx @@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next' import { TextInput, Button, List, ListItem, Container, Row } from '@hospitalrun/components' import { RootState } from '../../store' import { fetchPatientAppointments } from '../../scheduling/appointments/appointments-slice' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' interface Props { patientId: string @@ -19,6 +20,14 @@ const AppointmentsList = (props: Props) => { const { appointments } = useSelector((state: RootState) => state.appointments) const [searchText, setSearchText] = useState('') + const breadcrumbs = [ + { + i18nKey: 'scheduling.appointments.label', + location: `/patients/${patientId}/appointments`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + useEffect(() => { dispatch(fetchPatientAppointments(patientId)) }, [dispatch, patientId]) diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index d68590e8ac..f6f2e455ac 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState } from 'react' import { useHistory, useParams } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' @@ -10,7 +10,7 @@ import Patient from '../../model/Patient' import { updatePatient, fetchPatient } from '../patient-slice' import { RootState } from '../../store' import { getPatientFullName, getPatientName } from '../util/patient-name-util' -import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' const getFriendlyId = (p: Patient): string => { if (p) { @@ -35,15 +35,12 @@ const EditPatient = () => { )})`, ) - const breadcrumbs = useMemo( - () => [ - { i18nKey: 'patients.label', location: '/patients' }, - { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, - { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, - ], - [patient], - ) - useSetBreadcrumbs(breadcrumbs) + const breadcrumbs = [ + { i18nKey: 'patients.label', location: '/patients' }, + { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, + { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, + ] + useAddBreadcrumbs(breadcrumbs) useEffect(() => { setPatient(reduxPatient) diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 30d327ac73..9696661830 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -6,7 +6,7 @@ import { Spinner, TextInput, Button, List, ListItem, Container, Row } from '@hos import { RootState } from '../../store' import { fetchPatients, searchPatients } from '../patients-slice' import useTitle from '../../page-header/useTitle' -import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' const breadcrumbs = [{ i18nKey: 'patients.label', location: '/patients' }] @@ -14,7 +14,7 @@ const Patients = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('patients.label')) - useSetBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs) const dispatch = useDispatch() const { patients, isLoading } = useSelector((state: RootState) => state.patients) diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index ecfe2aa2d3..971eafff32 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -9,7 +9,7 @@ import useTitle from '../../page-header/useTitle' import Patient from '../../model/Patient' import { createPatient } from '../patient-slice' import { getPatientName } from '../util/patient-name-util' -import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' const breadcrumbs = [ { i18nKey: 'patients.label', location: '/patients' }, @@ -25,7 +25,7 @@ const NewPatient = () => { const [errorMessage, setErrorMessage] = useState('') useTitle(t('patients.newPatient')) - useSetBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs) const onCancel = () => { history.push('/patients') diff --git a/src/patients/related-persons/RelatedPersonTab.tsx b/src/patients/related-persons/RelatedPersonTab.tsx index 8eecc152da..f3451dae19 100644 --- a/src/patients/related-persons/RelatedPersonTab.tsx +++ b/src/patients/related-persons/RelatedPersonTab.tsx @@ -10,6 +10,7 @@ import { useDispatch, useSelector } from 'react-redux' import { RootState } from 'store' import Permissions from 'model/Permissions' import PatientRepository from 'clients/db/PatientRepository' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' interface Props { patient: Patient @@ -28,6 +29,14 @@ const RelatedPersonTab = (props: Props) => { const [showNewRelatedPersonModal, setShowRelatedPersonModal] = useState(false) const [relatedPersons, setRelatedPersons] = useState(undefined) + const breadcrumbs = [ + { + i18nKey: 'patient.relatedPersons.label', + location: `/patients/${patient.id}/relatedpersons`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + useEffect(() => { const fetchRelatedPersons = async () => { const fetchedRelatedPersons: Patient[] = [] diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 9e0d0e24c9..aac11f0b42 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react' +import React, { useEffect } from 'react' import { useDispatch, useSelector } from 'react-redux' import { useParams, withRouter, Route, useHistory, useLocation } from 'react-router-dom' import { Panel, Spinner, TabsHeader, Tab, Button } from '@hospitalrun/components' @@ -11,7 +11,7 @@ import { getPatientFullName } from '../util/patient-name-util' import Patient from '../../model/Patient' import GeneralInformation from '../GeneralInformation' import RelatedPerson from '../related-persons/RelatedPersonTab' -import useSetBreadcrumbs from '../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' import AppointmentsList from '../appointments/AppointmentsList' const getFriendlyId = (p: Patient): string => { @@ -32,14 +32,11 @@ const ViewPatient = () => { useTitle(`${getPatientFullName(patient)} (${getFriendlyId(patient)})`) - const breadcrumbs = useMemo( - () => [ - { i18nKey: 'patients.label', location: '/patients' }, - { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, - ], - [patient], - ) - useSetBreadcrumbs(breadcrumbs) + const breadcrumbs = [ + { i18nKey: 'patients.label', location: '/patients' }, + { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, + ] + useAddBreadcrumbs(breadcrumbs) const { id } = useParams() useEffect(() => { diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 78fde52f9b..b52194b922 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -6,7 +6,7 @@ import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useHistory } from 'react-router' import PatientRepository from 'clients/db/PatientRepository' -import useSetBreadcrumbs from 'breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' import { fetchAppointments } from './appointments-slice' interface Event { @@ -23,7 +23,7 @@ const Appointments = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('scheduling.appointments.label')) - useSetBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs) const dispatch = useDispatch() const { appointments } = useSelector((state: RootState) => state.appointments) const [events, setEvents] = useState([]) diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 650b682b3d..c705352fb9 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -8,7 +8,7 @@ import Appointment from 'model/Appointment' import addMinutes from 'date-fns/addMinutes' import { isBefore } from 'date-fns' import { Button, Alert } from '@hospitalrun/components' -import useSetBreadcrumbs from '../../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' import { createAppointment } from '../appointments-slice' import AppointmentDetailForm from '../AppointmentDetailForm' @@ -22,7 +22,7 @@ const NewAppointment = () => { const history = useHistory() const dispatch = useDispatch() useTitle(t('scheduling.appointments.new')) - useSetBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs) 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 45cf9b000d..e25dc8825c 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo } from 'react' +import React, { useEffect } from 'react' import useTitle from 'page-header/useTitle' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' @@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next' import Appointment from 'model/Appointment' import { fetchAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' -import useSetBreadcrumbs from '../../../breadcrumbs/useSetBreadcrumbs' +import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' function getAppointmentLabel(appointment: Appointment) { const { id, startDateTime, endDateTime } = appointment @@ -25,14 +25,11 @@ const ViewAppointment = () => { const { id } = useParams() const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) - const breadcrumbs = useMemo( - () => [ - { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, - { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, - ], - [appointment], - ) - useSetBreadcrumbs(breadcrumbs) + const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, + ] + useAddBreadcrumbs(breadcrumbs) useEffect(() => { if (id) { From a44ac9f9dcc38ff98c1eb85476e25be4cc6598ac Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 15 Feb 2020 16:25:11 +0100 Subject: [PATCH 37/87] feat(breadcrumb): add Breadcrumbs unit tests (component/slice/hook) fix #1770 --- .../breadcrumbs/Breadcrumbs.test.tsx | 47 ++++++++++++++- .../breadcrumbs/breadcrumbs-slice.test.ts | 59 ++++++++++++++++++- .../breadcrumbs/useAddBreadcrumbs.test.ts | 5 -- .../breadcrumbs/useAddBreadcrumbs.test.tsx | 46 +++++++++++++++ src/breadcrumbs/Breadcrumbs.tsx | 13 ++-- src/breadcrumbs/useAddBreadcrumbs.ts | 8 ++- 6 files changed, 158 insertions(+), 20 deletions(-) delete mode 100644 src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts create mode 100644 src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx index 98529d829a..e36f64ceea 100644 --- a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx +++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx @@ -1,5 +1,48 @@ import '../../__mocks__/matchMediaMock' +import React from 'react' +import { Provider } from 'react-redux' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { Router } from 'react-router-dom' +import configureMockStore from 'redux-mock-store' +import { BreadcrumbItem } from '@hospitalrun/components' -it('should return true', () => { - expect(true).toBeTruthy() +import Breadcrumbs from 'breadcrumbs/Breadcrumbs' +import Breadcrumb from 'model/Breadcrumb' + +const mockStore = configureMockStore() + +describe('Breadcrumbs', () => { + const setup = (breadcrumbs: Breadcrumb[]) => { + const history = createMemoryHistory() + const store = mockStore({ + breadcrumbs: { breadcrumbs }, + }) + + const wrapper = mount( + + + + + , + ) + + return wrapper + } + + it('should render breadcrumbs items', () => { + const breadcrumbs = [ + { text: 'Edit Patient', location: '/patient/1/edit' }, + { i18nKey: 'patient.label', location: '/patient' }, + { text: 'Bob', location: '/patient/1' }, + ] + const wrapper = setup(breadcrumbs) + + const items = wrapper.find(BreadcrumbItem) + + expect(items).toHaveLength(3) + expect(items.at(0).text()).toEqual('patient.label') + expect(items.at(1).text()).toEqual('Bob') + expect(items.at(2).text()).toEqual('Edit Patient') + }) }) diff --git a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts index 98529d829a..f3b6f7da35 100644 --- a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts +++ b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts @@ -1,5 +1,60 @@ import '../../__mocks__/matchMediaMock' +import { AnyAction } from 'redux' +import breadcrumbs, { addBreadcrumbs, removeBreadcrumbs } from '../../breadcrumbs/breadcrumbs-slice' -it('should return true', () => { - expect(true).toBeTruthy() +describe('breadcrumbs slice', () => { + describe('breadcrumbs reducer', () => { + it('should create the proper initial state with empty patients array', () => { + const breadcrumbsStore = breadcrumbs(undefined, {} as AnyAction) + + expect(breadcrumbsStore.breadcrumbs).toEqual([]) + }) + + it('should handle the ADD_BREADCRUMBS action', () => { + const breadcrumbsToAdd = [ + { text: 'user', location: '/user' }, + { text: 'Bob', location: '/user/1' }, + ] + + const breadcrumbsStore = breadcrumbs(undefined, { + type: addBreadcrumbs.type, + payload: breadcrumbsToAdd, + }) + + expect(breadcrumbsStore.breadcrumbs).toEqual(breadcrumbsToAdd) + }) + + it('should handle the ADD_BREADCRUMBS action with existing breadcreumbs', () => { + const breadcrumbsToAdd = [{ text: 'Bob', location: '/user/1' }] + + const state = { + breadcrumbs: [{ text: 'user', location: '/user' }], + } + + const breadcrumbsStore = breadcrumbs(state, { + type: addBreadcrumbs.type, + payload: breadcrumbsToAdd, + }) + + expect(breadcrumbsStore.breadcrumbs).toEqual([...state.breadcrumbs, ...breadcrumbsToAdd]) + }) + + it('should handle the REMOVE_BREADCRUMBS action', () => { + const breadcrumbsToRemove = [{ text: 'Bob', location: '/user/1' }] + + const state = { + breadcrumbs: [ + { text: 'user', location: '/user' }, + { text: 'Bob', location: '/user/1' }, + ], + } + + const breadcrumbsStore = breadcrumbs(state, { + type: removeBreadcrumbs.type, + payload: breadcrumbsToRemove, + }) + + expect(breadcrumbsStore.breadcrumbs).toEqual([{ text: 'user', location: '/user' }]) + }) + }) }) diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts deleted file mode 100644 index 98529d829a..0000000000 --- a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import '../../__mocks__/matchMediaMock' - -it('should return true', () => { - expect(true).toBeTruthy() -}) diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx new file mode 100644 index 0000000000..7a118f01a1 --- /dev/null +++ b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { renderHook } from '@testing-library/react-hooks' +import configureMockStore from 'redux-mock-store' +import { Provider } from 'react-redux' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' +import * as breadcrumbsSlice from '../../breadcrumbs/breadcrumbs-slice' + +const store = configureMockStore() + +describe('useAddBreadcrumbs', () => { + beforeEach(() => jest.clearAllMocks()) + + it('should call addBreadcrumbs with the correct data', () => { + const wrapper = ({ children }: any) => {children} + + jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs') + const breadcrumbs = [ + { + text: 'Patients', + location: '/patients', + }, + ] + + renderHook(() => useAddBreadcrumbs(breadcrumbs), { wrapper } as any) + expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledTimes(1) + expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledWith(breadcrumbs) + }) + + it('should call removeBreadcrumbs with the correct data after unmount', () => { + const wrapper = ({ children }: any) => {children} + + jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs') + jest.spyOn(breadcrumbsSlice, 'removeBreadcrumbs') + const breadcrumbs = [ + { + text: 'Patients', + location: '/patients', + }, + ] + + const { unmount } = renderHook(() => useAddBreadcrumbs(breadcrumbs), { wrapper } as any) + unmount() + expect(breadcrumbsSlice.removeBreadcrumbs).toHaveBeenCalledTimes(1) + expect(breadcrumbsSlice.removeBreadcrumbs).toHaveBeenCalledWith(breadcrumbs) + }) +}) diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx index 99ae7a0648..ba666ece17 100644 --- a/src/breadcrumbs/Breadcrumbs.tsx +++ b/src/breadcrumbs/Breadcrumbs.tsx @@ -2,10 +2,7 @@ import React from 'react' import { useHistory } from 'react-router' import { useSelector } from 'react-redux' import { useTranslation } from 'react-i18next' -import { - Breadcrumb as HrBreadcrumb, - BreadcrumbItem as HrBreadcrumbItem, -} from '@hospitalrun/components' +import { Breadcrumb, BreadcrumbItem } from '@hospitalrun/components' import { RootState } from '../store' const Breadcrumbs = () => { @@ -14,7 +11,7 @@ const Breadcrumbs = () => { const { breadcrumbs } = useSelector((state: RootState) => state.breadcrumbs) return ( - + {breadcrumbs .slice() .sort((b1, b2) => b1.location.length - b2.location.length) @@ -23,12 +20,12 @@ const Breadcrumbs = () => { const onClick = !isLast ? () => history.push(location) : undefined return ( - + {i18nKey ? t(i18nKey) : text} - + ) })} - +
) } diff --git a/src/breadcrumbs/useAddBreadcrumbs.ts b/src/breadcrumbs/useAddBreadcrumbs.ts index b5e7f521d7..135e3fe5f2 100644 --- a/src/breadcrumbs/useAddBreadcrumbs.ts +++ b/src/breadcrumbs/useAddBreadcrumbs.ts @@ -5,12 +5,14 @@ import { addBreadcrumbs, removeBreadcrumbs } from './breadcrumbs-slice' export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[]): void { const dispatch = useDispatch() + const breadcrumbsStringified = JSON.stringify(breadcrumbs) useEffect(() => { - dispatch(addBreadcrumbs(breadcrumbs)) + const breadcrumbsParsed = JSON.parse(breadcrumbsStringified) + dispatch(addBreadcrumbs(breadcrumbsParsed)) return () => { - dispatch(removeBreadcrumbs(breadcrumbs)) + dispatch(removeBreadcrumbs(breadcrumbsParsed)) } - }, [breadcrumbs, dispatch, JSON.stringify(breadcrumbs)]) + }, [breadcrumbsStringified, dispatch]) } From 6d450c981d843fa6bbb63cbf27cde46910ce8658 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 15 Feb 2020 16:32:10 +0100 Subject: [PATCH 38/87] style(package.json): reset lint-staged formatting --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index ab9a921932..cc8817b34b 100644 --- a/package.json +++ b/package.json @@ -125,8 +125,8 @@ }, "lint-staged": { "**/*.{js,jsx,ts,tsx}": [ - "npm run lint:fix", - "npm run test:ci", + "npm run lint:fix", + "npm run test:ci", "git add ." ] } From 0da4043afeb495281e04f4c5c2dcff512d285ea3 Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Sat, 15 Feb 2020 18:31:55 -0600 Subject: [PATCH 39/87] feat(view patient): add 'edit' icon to Edit button --- src/patients/view/ViewPatient.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 814ef09840..e54ff562a5 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -68,6 +68,7 @@ const ViewPatient = () => { , + ]) + const [searchText, setSearchText] = useState('') useEffect(() => { dispatch(fetchPatients()) - }, [dispatch]) + + return () => { + setButtonToolBar([]) + } + }, [dispatch, setButtonToolBar]) if (isLoading) { return diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 7676b1beb9..e45b105bd6 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -1,11 +1,12 @@ import React, { useEffect, useState } from 'react' -import { Calendar } from '@hospitalrun/components' +import { Calendar, Button } from '@hospitalrun/components' import useTitle from 'page-header/useTitle' import { useTranslation } from 'react-i18next' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useHistory } from 'react-router' import PatientRepository from 'clients/db/PatientRepository' +import { useButtonToolbarSetter } from 'page-header/button-bar-context' import { fetchAppointments } from './appointments-slice' interface Event { @@ -23,10 +24,25 @@ const Appointments = () => { const dispatch = useDispatch() const { appointments } = useSelector((state: RootState) => state.appointments) const [events, setEvents] = useState([]) + const setButtonToolBar = useButtonToolbarSetter() + setButtonToolBar([ + , + ]) useEffect(() => { dispatch(fetchAppointments()) - }, [dispatch]) + + return () => { + setButtonToolBar([]) + } + }, [dispatch, setButtonToolBar]) useEffect(() => { const getAppointments = async () => { From 0e56e3c0398480235cecc226409e83a40ff4b3da Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 17 Feb 2020 23:42:29 -0600 Subject: [PATCH 57/87] feat(toolbar): add tests for button toolbar --- src/HospitalRun.tsx | 2 +- .../page-header/ButtonBarProvider.test.tsx | 27 +++++++++++++++++++ .../page-header/ButtonToolBar.test.tsx | 24 +++++++++++++++++ src/__tests__/patients/list/Patients.test.tsx | 16 +++++++++++ .../appointments/Appointments.test.tsx | 14 ++++++++++ ...-bar-context.tsx => ButtonBarProvider.tsx} | 9 ++++--- src/page-header/ButtonToolBar.tsx | 2 +- src/patients/list/Patients.tsx | 2 +- src/scheduling/appointments/Appointments.tsx | 4 +-- 9 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/__tests__/page-header/ButtonBarProvider.test.tsx create mode 100644 src/__tests__/page-header/ButtonToolBar.test.tsx rename src/page-header/{button-bar-context.tsx => ButtonBarProvider.tsx} (78%) diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 0cd8e7799a..3af081530d 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@hospitalrun/components' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' import ViewAppointment from 'scheduling/appointments/view/ViewAppointment' -import { ButtonBarProvider } from 'page-header/button-bar-context' +import { ButtonBarProvider } from 'page-header/ButtonBarProvider' import ButtonToolBar from 'page-header/ButtonToolBar' import Sidebar from './components/Sidebar' import Permissions from './model/Permissions' diff --git a/src/__tests__/page-header/ButtonBarProvider.test.tsx b/src/__tests__/page-header/ButtonBarProvider.test.tsx new file mode 100644 index 0000000000..f718bcf075 --- /dev/null +++ b/src/__tests__/page-header/ButtonBarProvider.test.tsx @@ -0,0 +1,27 @@ +import '../../__mocks__/matchMediaMock' +import React from 'react' +import { renderHook } from '@testing-library/react-hooks' +import { + ButtonBarProvider, + useButtons, + useButtonToolbarSetter, +} from 'page-header/ButtonBarProvider' +import { Button } from '@hospitalrun/components' + +describe('Button Bar Provider', () => { + it('should update and fetch data from the button bar provider', () => { + const expectedButtons = [] + const wrapper = ({ children }: any) => {children} + + const { result } = renderHook( + () => { + const update = useButtonToolbarSetter() + update(expectedButtons) + return useButtons() + }, + { wrapper }, + ) + + expect(result.current).toEqual(expectedButtons) + }) +}) diff --git a/src/__tests__/page-header/ButtonToolBar.test.tsx b/src/__tests__/page-header/ButtonToolBar.test.tsx new file mode 100644 index 0000000000..6f32b51e84 --- /dev/null +++ b/src/__tests__/page-header/ButtonToolBar.test.tsx @@ -0,0 +1,24 @@ +import '../../__mocks__/matchMediaMock' +import React from 'react' +import { Button } from '@hospitalrun/components' +import { mocked } from 'ts-jest/utils' +import { mount } from 'enzyme' +import * as ButtonBarProvider from '../../page-header/ButtonBarProvider' +import ButtonToolBar from '../../page-header/ButtonToolBar' + +describe('Button Tool Bar', () => { + beforeEach(() => { + jest.resetAllMocks() + }) + + it('should render the buttons in the provider', () => { + const buttons: React.ReactNode[] = [, ] + jest.spyOn(ButtonBarProvider, 'useButtons') + mocked(ButtonBarProvider).useButtons.mockReturnValue(buttons) + + const wrapper = mount() + + expect(wrapper.childAt(0).getElement()).toEqual(buttons[0]) + expect(wrapper.childAt(1).getElement()).toEqual(buttons[1]) + }) +}) diff --git a/src/__tests__/patients/list/Patients.test.tsx b/src/__tests__/patients/list/Patients.test.tsx index 2145ad3dac..98fcab49c0 100644 --- a/src/__tests__/patients/list/Patients.test.tsx +++ b/src/__tests__/patients/list/Patients.test.tsx @@ -8,6 +8,7 @@ import thunk from 'redux-thunk' import configureStore from 'redux-mock-store' import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' import Patients from '../../../patients/list/Patients' import PatientRepository from '../../../clients/db/PatientRepository' import * as patientSlice from '../../../patients/patients-slice' @@ -42,6 +43,10 @@ describe('Patients', () => { }) describe('layout', () => { + afterEach(() => { + jest.restoreAllMocks() + }) + it('should render a search input with button', () => { const wrapper = setup() const searchInput = wrapper.find(TextInput) @@ -66,6 +71,17 @@ describe('Patients', () => { `${patients[0].fullName} (${patients[0].friendlyId})`, ) }) + + it('should add a "New Patient" button to the button tool bar', () => { + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + const setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + + setup() + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('patients.newPatient') + }) }) describe('search functionality', () => { diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index a6e40005de..4c3b73282e 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -11,6 +11,7 @@ import { act } from '@testing-library/react' import PatientRepository from 'clients/db/PatientRepository' import { mocked } from 'ts-jest/utils' import Patient from 'model/Patient' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' import * as titleUtil from '../../../page-header/useTitle' describe('Appointments', () => { @@ -51,6 +52,19 @@ describe('Appointments', () => { expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label') }) + it('should add a "New Appointment" button to the button tool bar', async () => { + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + const setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + + await act(async () => { + await setup() + }) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointments.new') + }) + it('should render a calendar with the proper events', async () => { let wrapper: any await act(async () => { diff --git a/src/page-header/button-bar-context.tsx b/src/page-header/ButtonBarProvider.tsx similarity index 78% rename from src/page-header/button-bar-context.tsx rename to src/page-header/ButtonBarProvider.tsx index dcb076f584..5cf8068342 100644 --- a/src/page-header/button-bar-context.tsx +++ b/src/page-header/ButtonBarProvider.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' type Props = { - children: React.ReactNode + children?: React.ReactNode } type ButtonUpdater = (buttons: React.ReactNode[]) => void @@ -19,16 +19,19 @@ function ButtonBarProvider(props: Props) { ) } function useButtons() { + console.log('use buttons') const context = React.useContext(ButtonBarStateContext) + console.log('bug') + console.log(context) if (context === undefined) { - throw new Error('useCountState must be used within a CountProvider') + throw new Error('useButtons must be used within a Button Bar Context') } return context } function useButtonToolbarSetter() { const context = React.useContext(ButtonBarUpdateContext) if (context === undefined) { - throw new Error('useCountDispatch must be used within a CountProvider') + throw new Error('useButtonToolBarSetter must be used within a Button Bar Context') } return context } diff --git a/src/page-header/ButtonToolBar.tsx b/src/page-header/ButtonToolBar.tsx index 701ade5e85..05d8be02f0 100644 --- a/src/page-header/ButtonToolBar.tsx +++ b/src/page-header/ButtonToolBar.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useButtons } from './button-bar-context' +import { useButtons } from './ButtonBarProvider' const ButtonToolBar = () => { const buttons = useButtons() diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 2ee672b656..54ff755bb2 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from 'react-redux' import { useHistory } from 'react-router' import { useTranslation } from 'react-i18next' import { Spinner, TextInput, Button, List, ListItem, Container, Row } from '@hospitalrun/components' -import { useButtonToolbarSetter } from 'page-header/button-bar-context' +import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import { RootState } from '../../store' import { fetchPatients, searchPatients } from '../patients-slice' import useTitle from '../../page-header/useTitle' diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index e45b105bd6..35cd8da272 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -6,7 +6,7 @@ import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' import { useHistory } from 'react-router' import PatientRepository from 'clients/db/PatientRepository' -import { useButtonToolbarSetter } from 'page-header/button-bar-context' +import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import { fetchAppointments } from './appointments-slice' interface Event { @@ -32,7 +32,7 @@ const Appointments = () => { icon="appointment-add" onClick={() => history.push('/appointments/new')} > - New Appointment + {t('scheduling.appointments.new')} , ]) From 450b8cd2f864ce25068f97aef0f6b4616a8b0a2b Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 17 Feb 2020 23:54:40 -0600 Subject: [PATCH 58/87] feat(toolbar): change edit patient to toolbar button --- .../page-header/ButtonToolBar.test.tsx | 5 ++- .../patients/view/ViewPatient.test.tsx | 31 ++++++---------- src/page-header/ButtonBarProvider.tsx | 7 ++-- src/patients/list/Patients.tsx | 1 + src/patients/view/ViewPatient.tsx | 37 +++++++++++-------- src/scheduling/appointments/Appointments.tsx | 1 + 6 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/__tests__/page-header/ButtonToolBar.test.tsx b/src/__tests__/page-header/ButtonToolBar.test.tsx index 6f32b51e84..5c79e2f337 100644 --- a/src/__tests__/page-header/ButtonToolBar.test.tsx +++ b/src/__tests__/page-header/ButtonToolBar.test.tsx @@ -12,7 +12,10 @@ describe('Button Tool Bar', () => { }) it('should render the buttons in the provider', () => { - const buttons: React.ReactNode[] = [, ] + const buttons: React.ReactNode[] = [ + , + , + ] jest.spyOn(ButtonBarProvider, 'useButtons') mocked(ButtonBarProvider).useButtons.mockReturnValue(buttons) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 01eed0a4b5..15bc7f1810 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -11,6 +11,7 @@ import thunk from 'redux-thunk' import GeneralInformation from 'patients/GeneralInformation' import { createMemoryHistory } from 'history' import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' import Patient from '../../../model/Patient' import PatientRepository from '../../../clients/db/PatientRepository' import * as titleUtil from '../../../page-header/useTitle' @@ -71,25 +72,6 @@ describe('ViewPatient', () => { jest.restoreAllMocks() }) - it('should navigate to /patients/edit/:id when edit is clicked', async () => { - let wrapper: any - await act(async () => { - wrapper = await setup() - }) - - wrapper.update() - - const editButton = wrapper.find(Button).at(3) - const onClick = editButton.prop('onClick') as any - expect(editButton.text().trim()).toEqual('actions.edit') - - act(() => { - onClick() - }) - - expect(history.location.pathname).toEqual('/patients/edit/123') - }) - it('should dispatch fetchPatient when component loads', async () => { await act(async () => { await setup() @@ -110,6 +92,17 @@ describe('ViewPatient', () => { ) }) + it('should add a "Edit Patient" button to the button tool bar', () => { + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + const setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + + setup() + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('actions.edit') + }) + it('should render a tabs header with the correct tabs', async () => { let wrapper: any await act(async () => { diff --git a/src/page-header/ButtonBarProvider.tsx b/src/page-header/ButtonBarProvider.tsx index 5cf8068342..9d5d4698fe 100644 --- a/src/page-header/ButtonBarProvider.tsx +++ b/src/page-header/ButtonBarProvider.tsx @@ -7,7 +7,9 @@ type Props = { type ButtonUpdater = (buttons: React.ReactNode[]) => void const ButtonBarStateContext = React.createContext([]) -const ButtonBarUpdateContext = React.createContext(() => {}) +const ButtonBarUpdateContext = React.createContext(() => { + // empty initial state +}) function ButtonBarProvider(props: Props) { const { children } = props @@ -19,10 +21,7 @@ function ButtonBarProvider(props: Props) { ) } function useButtons() { - console.log('use buttons') const context = React.useContext(ButtonBarStateContext) - console.log('bug') - console.log(context) if (context === undefined) { throw new Error('useButtons must be used within a Button Bar Context') } diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 54ff755bb2..1a8373eee4 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -18,6 +18,7 @@ const Patients = () => { const setButtonToolBar = useButtonToolbarSetter() setButtonToolBar([ , + ]) + const { id } = useParams() useEffect(() => { if (id) { dispatch(fetchPatient(id)) } - }, [dispatch, id]) + + return () => { + setButtonToolBar([]) + } + }, [dispatch, id, setButtonToolBar]) if (isLoading || !patient) { return @@ -63,21 +83,6 @@ const ViewPatient = () => { -
-
- -
-
-
diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 35cd8da272..55d618d98a 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -27,6 +27,7 @@ const Appointments = () => { const setButtonToolBar = useButtonToolbarSetter() setButtonToolBar([
@@ -70,8 +70,8 @@ const AppointmentDetailForm = (props: Props) => { label={t('scheduling.appointment.endDate')} value={new Date(appointment.endDateTime)} isEditable={isEditable} - onChange={(date) => { - onAppointmentChange({ ...appointment, endDateTime: date.toISOString() }) + onChange={(date: Date) => { + onDateChange(date, 'endDateTime') }} />
@@ -84,7 +84,7 @@ const AppointmentDetailForm = (props: Props) => { value={appointment.location} isEditable={isEditable} onChange={(event) => { - onAppointmentChange({ ...appointment, location: event?.target.value }) + onInputElementChange(event, 'location') }} />
@@ -104,7 +104,7 @@ const AppointmentDetailForm = (props: Props) => { { label: t('scheduling.appointment.types.walkUp'), value: 'walk up' }, ]} onChange={(event: React.ChangeEvent) => { - onAppointmentChange({ ...appointment, type: event.currentTarget.value }) + onSelectChange(event, 'type') }} />
@@ -117,9 +117,11 @@ const AppointmentDetailForm = (props: Props) => { label={t('scheduling.appointment.reason')} value={appointment.reason} isEditable={isEditable} - onChange={(event) => { - onAppointmentChange({ ...appointment, reason: event?.target.value }) - }} + onChange={ + (event: React.ChangeEvent) => + onFieldChange && onFieldChange('reason', event.currentTarget.value) + // eslint-disable-next-line react/jsx-curly-newline + } />
diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index aa710d76d4..7966ae3bc8 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import Appointment from 'model/Appointment' import { AppThunk } from 'store' -import AppointmentRepository from 'clients/db/AppointmentsRepository' +import AppointmentRepository from 'clients/db/AppointmentRepository' import Patient from 'model/Patient' import PatientRepository from 'clients/db/PatientRepository' @@ -17,13 +17,17 @@ const initialAppointmentState = { isLoading: false, } +function startLoading(state: AppointmentState) { + state.isLoading = true +} + const appointmentSlice = createSlice({ name: 'appointment', initialState: initialAppointmentState, reducers: { - fetchAppointmentStart: (state: AppointmentState) => { - state.isLoading = true - }, + fetchAppointmentStart: startLoading, + createAppointmentStart: startLoading, + updateAppointmentStart: startLoading, fetchAppointmentSuccess: ( state, { payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>, @@ -32,10 +36,24 @@ const appointmentSlice = createSlice({ state.appointment = payload.appointment state.patient = payload.patient }, + createAppointmentSuccess(state) { + state.isLoading = false + }, + updateAppointmentSuccess(state, { payload }: PayloadAction) { + state.isLoading = false + state.appointment = payload + }, }, }) -export const { fetchAppointmentStart, fetchAppointmentSuccess } = appointmentSlice.actions +export const { + createAppointmentStart, + createAppointmentSuccess, + updateAppointmentStart, + updateAppointmentSuccess, + fetchAppointmentStart, + fetchAppointmentSuccess, +} = appointmentSlice.actions export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentStart()) @@ -45,4 +63,22 @@ export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentSuccess({ appointment, patient })) } +export const createAppointment = (appointment: Appointment, history: any): AppThunk => async ( + dispatch, +) => { + dispatch(createAppointmentStart()) + await AppointmentRepository.save(appointment) + dispatch(createAppointmentSuccess()) + history.push('/appointments') +} + +export const updateAppointment = (appointment: Appointment, history: any): AppThunk => async ( + dispatch, +) => { + dispatch(updateAppointmentStart()) + const updatedAppointment = await AppointmentRepository.saveOrUpdate(appointment) + dispatch(updateAppointmentSuccess(updatedAppointment)) + history.push(`/appointments/${updatedAppointment.id}`) +} + export default appointmentSlice.reducer diff --git a/src/scheduling/appointments/appointments-slice.ts b/src/scheduling/appointments/appointments-slice.ts index fd90b05537..7290275aac 100644 --- a/src/scheduling/appointments/appointments-slice.ts +++ b/src/scheduling/appointments/appointments-slice.ts @@ -1,7 +1,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import Appointment from 'model/Appointment' import { AppThunk } from 'store' -import AppointmentRepository from 'clients/db/AppointmentsRepository' +import AppointmentRepository from 'clients/db/AppointmentRepository' interface AppointmentsState { isLoading: boolean diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx new file mode 100644 index 0000000000..1cca80243f --- /dev/null +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -0,0 +1,89 @@ +import React, { useEffect, useState } from 'react' +import { useHistory, useParams } from 'react-router-dom' +import { useTranslation } from 'react-i18next' +import { useDispatch, useSelector } from 'react-redux' +import { Spinner, Button } from '@hospitalrun/components' +import { isBefore } from 'date-fns' + +import AppointmentDetailForm from '../AppointmentDetailForm' +import useTitle from '../../../page-header/useTitle' +import Appointment from '../../../model/Appointment' +import { updateAppointment, fetchAppointment } from '../appointment-slice' +import { RootState } from '../../../store' + +const EditAppointment = () => { + const { t } = useTranslation() + useTitle(t('scheduling.appointments.editAppointment')) + const history = useHistory() + const dispatch = useDispatch() + + const [appointment, setAppointment] = useState({} as Appointment) + const [errorMessage, setErrorMessage] = useState('') + const { appointment: reduxAppointment, patient, isLoading } = useSelector( + (state: RootState) => state.appointment, + ) + + useEffect(() => { + setAppointment(reduxAppointment) + }, [reduxAppointment]) + + const { id } = useParams() + useEffect(() => { + if (id) { + dispatch(fetchAppointment(id)) + } + }, [id, dispatch]) + + const onCancel = () => { + history.push(`/appointments/${appointment.id}`) + } + + const onSave = () => { + let newErrorMessage = '' + if (isBefore(new Date(appointment.endDateTime), new Date(appointment.startDateTime))) { + newErrorMessage += ` ${t('scheduling.appointment.errors.startDateMustBeBeforeEndDate')}` + } + + if (newErrorMessage) { + setErrorMessage(newErrorMessage.trim()) + return + } + + dispatch(updateAppointment(appointment as Appointment, history)) + } + + const onFieldChange = (key: string, value: string | boolean) => { + setAppointment({ + ...appointment, + [key]: value, + }) + } + + if (isLoading || !appointment.id || !patient.id) { + return + } + + return ( +
+ +
+
+ + +
+
+
+ ) +} + +export default EditAppointment diff --git a/src/scheduling/appointments/new/NewAppointment.tsx b/src/scheduling/appointments/new/NewAppointment.tsx index 538ae33f98..4adc7e2982 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -8,15 +8,15 @@ import { useDispatch } from 'react-redux' import Appointment from 'model/Appointment' import addMinutes from 'date-fns/addMinutes' import { isBefore } from 'date-fns' -import { Button, Alert } from '@hospitalrun/components' -import { createAppointment } from '../appointments-slice' +import { Button } from '@hospitalrun/components' +import { createAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' const NewAppointment = () => { const { t } = useTranslation() const history = useHistory() const dispatch = useDispatch() - useTitle(t('scheduling.appointments.new')) + useTitle(t('scheduling.appointments.newAppointment')) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) @@ -34,7 +34,7 @@ const NewAppointment = () => { history.push('/appointments') } - const onSaveClick = () => { + const onSave = () => { let newErrorMessage = '' if (!appointment.patientId) { newErrorMessage += t('scheduling.appointment.errors.patientRequired') @@ -51,24 +51,25 @@ const NewAppointment = () => { dispatch(createAppointment(appointment as Appointment, history)) } + const onFieldChange = (key: string, value: string | boolean) => { + setAppointment({ + ...appointment, + [key]: value, + }) + } + return (
- {errorMessage && ( - - )}
- +
+
+
) } From c22d02b15d596289ebd7e71ab7ca8b8cae0d8b26 Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Fri, 21 Feb 2020 17:26:33 -0600 Subject: [PATCH 65/87] feat(edit appointment): fix lint errors and typo --- src/HospitalRun.tsx | 2 +- .../scheduling/appointments/appointments-slice.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 9955aa63b6..206b4f8c07 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -84,7 +84,7 @@ const HospitalRun = () => { exact path="/appointments/edit/:id" component={EditAppointment} - /> + /> { expect(dispatch).toHaveBeenCalledWith({ type: fetchAppointmentsStart.type }) }) - it('should call the AppointmentsRepository findAll() function', async () => { + it('should call the AppointmentRepository findAll() function', async () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointments()(dispatch, getState, null) From 4744152317b7a550d5eeda2ff6959b7fa2c160ee Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Sat, 22 Feb 2020 14:51:29 -0600 Subject: [PATCH 66/87] feat(edit appointment): use button toolbar, address other PR issues --- .../patients/view/ViewPatient.test.tsx | 2 +- .../appointments/Appointments.test.tsx | 4 +- .../view/ViewAppointment.test.tsx | 14 +++++- src/patients/edit/EditPatient.tsx | 4 +- src/patients/new/NewPatient.tsx | 4 +- src/scheduling/appointments/Appointments.tsx | 2 +- .../appointments/edit/EditAppointment.tsx | 4 +- .../appointments/view/ViewAppointment.tsx | 45 ++++++++++--------- 8 files changed, 49 insertions(+), 30 deletions(-) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 15bc7f1810..5b56f8df0b 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -5,7 +5,7 @@ import { mount } from 'enzyme' import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' import { Route, Router } from 'react-router-dom' -import { TabsHeader, Tab, Button } from '@hospitalrun/components' +import { TabsHeader, Tab } from '@hospitalrun/components' import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import GeneralInformation from 'patients/GeneralInformation' diff --git a/src/__tests__/scheduling/appointments/Appointments.test.tsx b/src/__tests__/scheduling/appointments/Appointments.test.tsx index 4c3b73282e..8e6f49108d 100644 --- a/src/__tests__/scheduling/appointments/Appointments.test.tsx +++ b/src/__tests__/scheduling/appointments/Appointments.test.tsx @@ -62,7 +62,9 @@ describe('Appointments', () => { }) const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] - expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointments.new') + expect((actualButtons[0] as any).props.children).toEqual( + 'scheduling.appointments.newAppointment', + ) }) it('should render a calendar with the proper events', async () => { diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 6316dc2d30..85e475bb5a 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -15,6 +15,7 @@ import { Spinner } from '@hospitalrun/components' import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' import * as titleUtil from '../../../../page-header/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' @@ -72,7 +73,7 @@ describe('View Appointment', () => { } beforeEach(() => { - jest.resetAllMocks() + jest.restoreAllMocks() }) it('should use the correct title', async () => { @@ -84,6 +85,17 @@ describe('View Appointment', () => { expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.viewAppointment') }) + it('should add a "Edit Appointment" button to the button tool bar', () => { + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + const setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + + setup(true) + + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('actions.edit') + }) + it('should dispatch getAppointment if id is present', async () => { await act(async () => { await setup(true) diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index b47de7491c..5ce7b57957 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -86,10 +86,10 @@ const EditPatient = () => { />
- -
diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index 070470b7eb..f80930511b 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -57,10 +57,10 @@ const NewPatient = () => { />
- -
diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 55d618d98a..4490ff7960 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -33,7 +33,7 @@ const Appointments = () => { icon="appointment-add" onClick={() => history.push('/appointments/new')} > - {t('scheduling.appointments.new')} + {t('scheduling.appointments.newAppointment')} , ]) diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index 1cca80243f..ba88a17720 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -74,10 +74,10 @@ const EditAppointment = () => { />
- -
diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index 476dc3d88c..ce1b182023 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -5,6 +5,8 @@ import { RootState } from 'store' import { useParams, useHistory } from 'react-router' import { Spinner, Button } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' + +import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import { fetchAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' @@ -16,35 +18,38 @@ const ViewAppointment = () => { const history = useHistory() const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) + const setButtonToolBar = useButtonToolbarSetter() + console.log('setButtonToolBar was: ') + console.log(setButtonToolBar) + setButtonToolBar([ + , + ]) + useEffect(() => { if (id) { dispatch(fetchAppointment(id)) } - }, [dispatch, id]) + + return () => { + setButtonToolBar([]) + } + }, [dispatch, id, setButtonToolBar]) if (!appointment.id || isLoading) { return } - return ( -
-
-
- -
-
- -
- ) + return } export default ViewAppointment From 58117b4e245a5fe14bd4e18c44a95e925bcab46f Mon Sep 17 00:00:00 2001 From: Matthew Dorner Date: Sat, 22 Feb 2020 22:21:19 -0600 Subject: [PATCH 67/87] feat(edit appointment): add tests for Edit Appointment route --- src/__tests__/HospitalRun.test.tsx | 132 ++++++++++++++++++++++------- 1 file changed, 102 insertions(+), 30 deletions(-) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 569e241c02..9cd65ce7e3 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -11,11 +11,14 @@ import { act } from 'react-dom/test-utils' import Dashboard from 'dashboard/Dashboard' import Appointments from 'scheduling/appointments/Appointments' import NewAppointment from 'scheduling/appointments/new/NewAppointment' +import EditAppointment from 'scheduling/appointments/edit/EditAppointment' import NewPatient from '../patients/new/NewPatient' import EditPatient from '../patients/edit/EditPatient' import ViewPatient from '../patients/view/ViewPatient' import PatientRepository from '../clients/db/PatientRepository' +import AppointmentRepository from '../clients/db/AppointmentRepository' import Patient from '../model/Patient' +import Appointment from '../model/Appointment' import HospitalRun from '../HospitalRun' import Permissions from '../model/Permissions' @@ -217,41 +220,110 @@ describe('HospitalRun', () => { expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) - }) - describe('/appointments/new', () => { - it('should render the new appointment screen when /appointments/new is accessed', async () => { - const wrapper = mount( - - - - - , - ) + describe('/appointments/new', () => { + it('should render the new appointment screen when /appointments/new is accessed', async () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(NewAppointment)).toHaveLength(1) + }) - expect(wrapper.find(NewAppointment)).toHaveLength(1) + it('should render the Dashboard when the user does not have read appointment privileges', () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) }) - it('should render the Dashboard when the user does not have read appointment privileges', () => { - const wrapper = mount( - - - - - , - ) + describe('/appointments/edit/:id', () => { + it('should render the edit appointment screen when /appointments/edit/:id is accessed', () => { + jest.spyOn(AppointmentRepository, 'find') + const mockedAppointmentRepository = mocked(AppointmentRepository, true) + const mockedPatientRepository = mocked(PatientRepository, true) + const appointment = { + id: '123', + patientId: '456', + } as Appointment + + const patient = { + id: '456', + } as Patient + + mockedAppointmentRepository.find.mockResolvedValue(appointment) + mockedPatientRepository.find.mockResolvedValue(patient) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(EditAppointment)).toHaveLength(1) + }) + + it('should render the Dashboard when the user does not have read appointment privileges', () => { + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) + + it('should render the Dashboard when the user does not have write appointment privileges', () => { + const wrapper = mount( + + + + + , + ) - expect(wrapper.find(Dashboard)).toHaveLength(1) + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) }) }) From a0d756849ad8481e04beac4eca48943d95990ec2 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 23 Feb 2020 05:17:02 +0000 Subject: [PATCH 68/87] chore(deps-dev): bump eslint-plugin-jest from 23.7.0 to 23.8.0 Bumps [eslint-plugin-jest](https://github.com/jest-community/eslint-plugin-jest) from 23.7.0 to 23.8.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.7.0...v23.8.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 bd1ba2f194..bfd5b89927 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "eslint-config-airbnb": "~18.0.1", "eslint-config-prettier": "~6.10.0", "eslint-plugin-import": "~2.20.0", - "eslint-plugin-jest": "~23.7.0", + "eslint-plugin-jest": "~23.8.0", "eslint-plugin-jsx-a11y": "~6.2.3", "eslint-plugin-prettier": "~3.1.1", "eslint-plugin-react": "~7.18.0", From dcb46b89acb562e982a99531ca006e976f922d17 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 23 Feb 2020 00:25:57 -0600 Subject: [PATCH 69/87] feat(delete appointment): adds ability to delete appointment --- .../appointments/appointment-slice.test.ts | 103 ++++++++++++++- .../view/ViewAppointment.test.tsx | 125 +++++++++++++++++- src/locales/en-US/translation.json | 11 +- src/model/Permissions.ts | 1 + .../appointments/appointment-slice.ts | 29 +++- .../appointments/view/ViewAppointment.tsx | 56 +++++++- src/user/user-slice.ts | 1 + 7 files changed, 311 insertions(+), 15 deletions(-) diff --git a/src/__tests__/scheduling/appointments/appointment-slice.test.ts b/src/__tests__/scheduling/appointments/appointment-slice.test.ts index 7dada1d5a5..9dafe22139 100644 --- a/src/__tests__/scheduling/appointments/appointment-slice.test.ts +++ b/src/__tests__/scheduling/appointments/appointment-slice.test.ts @@ -1,13 +1,19 @@ +import '../../../__mocks__/matchMediaMock' import { AnyAction } from 'redux' import Appointment from 'model/Appointment' import AppointmentRepository from 'clients/db/AppointmentsRepository' +import * as components from '@hospitalrun/components' import { mocked } from 'ts-jest/utils' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' +import { createMemoryHistory } from 'history' import appointment, { fetchAppointmentStart, fetchAppointmentSuccess, fetchAppointment, + deleteAppointment, + deleteAppointmentStart, + deleteAppointmentSuccess, } from '../../../scheduling/appointments/appointment-slice' describe('appointment slice', () => { @@ -46,6 +52,22 @@ describe('appointment slice', () => { expect(appointmentStore.appointment).toEqual(expectedAppointment) expect(appointmentStore.patient).toEqual(expectedPatient) }) + + it('should handle the DELETE_APPOINTMENT_START action', () => { + const appointmentStore = appointment(undefined, { + type: deleteAppointmentStart.type, + }) + + expect(appointmentStore.isLoading).toBeTruthy() + }) + + it('should handle the DELETE_APPOINTMENT_SUCCESS action', () => { + const appointmentStore = appointment(undefined, { + type: deleteAppointmentSuccess.type, + }) + + expect(appointmentStore.isLoading).toBeFalsy() + }) }) describe('fetchAppointment()', () => { @@ -107,11 +129,84 @@ describe('appointment slice', () => { const dispatch = jest.fn() const getState = jest.fn() await fetchAppointment('id')(dispatch, getState, null) + }) + }) - expect(dispatch).toHaveBeenCalledWith({ - type: fetchAppointmentSuccess.type, - payload: { appointment: expectedAppointment, patient: expectedPatient }, - }) + describe('deleteAppointment()', () => { + let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') + let toastSpy = jest.spyOn(components, 'Toast') + beforeEach(() => { + jest.resetAllMocks() + deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') + toastSpy = jest.spyOn(components, 'Toast') + }) + + it('should dispatch the DELETE_APPOINTMENT_START action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( + dispatch, + getState, + null, + ) + + expect(dispatch).toHaveBeenCalledWith({ type: deleteAppointmentStart.type }) + }) + + it('should call the AppointmentRepository delete function with the appointment', async () => { + const expectedAppointment = { id: 'appointmentId1' } as Appointment + + const dispatch = jest.fn() + const getState = jest.fn() + + await deleteAppointment(expectedAppointment, createMemoryHistory())(dispatch, getState, null) + + expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1) + expect(deleteAppointmentSpy).toHaveBeenCalledWith(expectedAppointment) + }) + + it('should navigate to /appointments after deleting', async () => { + const history = createMemoryHistory() + const expectedAppointment = { id: 'appointmentId1' } as Appointment + + const dispatch = jest.fn() + const getState = jest.fn() + + await deleteAppointment(expectedAppointment, history)(dispatch, getState, null) + + expect(history.location.pathname).toEqual('/appointments') + }) + + it('should create a toast with a success message', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( + dispatch, + getState, + null, + ) + + expect(toastSpy).toHaveBeenCalledTimes(1) + expect(toastSpy).toHaveBeenLastCalledWith( + 'success', + 'states.success', + 'scheduling.appointments.successfullyDeleted', + ) + }) + + it('should dispatch the DELETE_APPOINTMENT_SUCCESS action', async () => { + const dispatch = jest.fn() + const getState = jest.fn() + + await deleteAppointment({ id: 'test1' } as Appointment, createMemoryHistory())( + dispatch, + getState, + null, + ) + + expect(dispatch).toHaveBeenCalledWith({ type: deleteAppointmentSuccess.type }) }) }) }) diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 45f3f3545e..66d64e4b85 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -11,10 +11,12 @@ import { createMemoryHistory } from 'history' import AppointmentRepository from 'clients/db/AppointmentsRepository' import { mocked } from 'ts-jest/utils' import { act } from 'react-dom/test-utils' -import { Spinner } from '@hospitalrun/components' +import { Spinner, Modal } from '@hospitalrun/components' import AppointmentDetailForm from 'scheduling/appointments/AppointmentDetailForm' import PatientRepository from 'clients/db/PatientRepository' import Patient from 'model/Patient' +import * as ButtonBarProvider from 'page-header/ButtonBarProvider' +import Permissions from 'model/Permissions' import * as titleUtil from '../../../../page-header/useTitle' import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice' @@ -37,7 +39,7 @@ describe('View Appointment', () => { let history: any let store: MockStore - const setup = (isLoading: boolean) => { + const setup = (isLoading: boolean, permissions = [Permissions.DeleteAppointment]) => { jest.spyOn(AppointmentRepository, 'find') const mockedAppointmentRepository = mocked(AppointmentRepository, true) mockedAppointmentRepository.find.mockResolvedValue(appointment) @@ -50,6 +52,9 @@ describe('View Appointment', () => { history.push('/appointments/123') store = mockStore({ + user: { + permissions, + }, appointment: { appointment, isLoading, @@ -115,4 +120,120 @@ describe('View Appointment', () => { expect(appointmentDetailForm.prop('appointment')).toEqual(appointment) expect(appointmentDetailForm.prop('isEditable')).toBeFalsy() }) + + it('should render a modal for delete confirmation', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup(false) + }) + + const deleteAppointmentConfirmationModal = wrapper.find(Modal) + expect(deleteAppointmentConfirmationModal).toHaveLength(1) + expect(deleteAppointmentConfirmationModal.prop('closeButton').children).toEqual( + 'actions.delete', + ) + expect(deleteAppointmentConfirmationModal.prop('body')).toEqual( + 'scheduling.appointment.deleteConfirmationMessage', + ) + expect(deleteAppointmentConfirmationModal.prop('title')).toEqual('actions.confirmDelete') + }) + + describe('delete appointment', () => { + let setButtonToolBarSpy = jest.fn() + let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter') + deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete') + setButtonToolBarSpy = jest.fn() + mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy) + }) + + it('should render a delete appointment button in the button toolbar', async () => { + await act(async () => { + await setup(false) + }) + + expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointment.delete') + }) + + it('should pop up the modal when on delete appointment click', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup(false) + }) + + expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + + act(() => { + const { onClick } = (actualButtons[0] as any).props + onClick({ preventDefault: jest.fn() }) + }) + wrapper.update() + + const deleteConfirmationModal = wrapper.find(Modal) + expect(deleteConfirmationModal.prop('show')).toEqual(true) + }) + + it('should close the modal when the toggle button is clicked', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup(false) + }) + + expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + + act(() => { + const { onClick } = (actualButtons[0] as any).props + onClick({ preventDefault: jest.fn() }) + }) + wrapper.update() + + act(() => { + const deleteConfirmationModal = wrapper.find(Modal) + deleteConfirmationModal.prop('toggle')() + }) + wrapper.update() + + const deleteConfirmationModal = wrapper.find(Modal) + expect(deleteConfirmationModal.prop('show')).toEqual(false) + }) + + it('should dispatch DELETE_APPOINTMENT action when modal confirmation button is clicked', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup(false) + }) + + const deleteConfirmationModal = wrapper.find(Modal) + + await act(async () => { + await deleteConfirmationModal.prop('closeButton').onClick() + }) + wrapper.update() + + expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1) + expect(deleteAppointmentSpy).toHaveBeenCalledWith(appointment) + + expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentStart()) + expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentSuccess()) + }) + + it('should not add delete appointment button to toolbar if the user does not have delete appointment permissions', async () => { + await act(async () => { + await setup(false, []) + }) + + expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1) + const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0] + + expect( + actualButtons.filter((b: any) => b.props.children === 'scheduling.appointment.delete'), + ).toHaveLength(0) + }) + }) }) diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index a6632e20c6..59dd7f1e29 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -66,7 +66,9 @@ "cancel": "Cancel", "new": "New", "list": "List", - "search": "Search" + "search": "Search", + "delete": "Delete", + "confirmDelete": "Confirm Delete" }, "states": { "success": "Success!", @@ -77,7 +79,8 @@ "appointments": { "label": "Appointments", "new": "New Appointment", - "view": "View Appointment" + "view": "View Appointment", + "successfullyDeleted": "Successfully deleted appointment!" }, "appointment": { "startDate": "Start Date", @@ -97,7 +100,9 @@ "startDateMustBeBeforeEndDate": "Start Time must be before End Time." }, "reason": "Reason", - "patient": "Patient" + "patient": "Patient", + "delete": "Delete Appointment", + "deleteConfirmationMessage": "Are you sure you want to delete this appointment?" } } } diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts index 5640a99292..fdb910cc63 100644 --- a/src/model/Permissions.ts +++ b/src/model/Permissions.ts @@ -3,6 +3,7 @@ enum Permissions { WritePatients = 'write:patients', ReadAppointments = 'read:appointments', WriteAppointments = 'write:appointments', + DeleteAppointment = 'delete:appointment', } export default Permissions diff --git a/src/scheduling/appointments/appointment-slice.ts b/src/scheduling/appointments/appointment-slice.ts index aa710d76d4..a4ecced154 100644 --- a/src/scheduling/appointments/appointment-slice.ts +++ b/src/scheduling/appointments/appointment-slice.ts @@ -1,9 +1,11 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit' import Appointment from 'model/Appointment' import { AppThunk } from 'store' +import { Toast } from '@hospitalrun/components' import AppointmentRepository from 'clients/db/AppointmentsRepository' import Patient from 'model/Patient' import PatientRepository from 'clients/db/PatientRepository' +import il8n from '../../i18n' interface AppointmentState { appointment: Appointment @@ -24,6 +26,12 @@ const appointmentSlice = createSlice({ fetchAppointmentStart: (state: AppointmentState) => { state.isLoading = true }, + deleteAppointmentStart: (state: AppointmentState) => { + state.isLoading = true + }, + deleteAppointmentSuccess: (state: AppointmentState) => { + state.isLoading = false + }, fetchAppointmentSuccess: ( state, { payload }: PayloadAction<{ appointment: Appointment; patient: Patient }>, @@ -35,7 +43,12 @@ const appointmentSlice = createSlice({ }, }) -export const { fetchAppointmentStart, fetchAppointmentSuccess } = appointmentSlice.actions +export const { + fetchAppointmentStart, + fetchAppointmentSuccess, + deleteAppointmentStart, + deleteAppointmentSuccess, +} = appointmentSlice.actions export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentStart()) @@ -45,4 +58,18 @@ export const fetchAppointment = (id: string): AppThunk => async (dispatch) => { dispatch(fetchAppointmentSuccess({ appointment, patient })) } +export const deleteAppointment = (appointment: Appointment, history: any): AppThunk => async ( + dispatch, +) => { + dispatch(deleteAppointmentStart()) + await AppointmentRepository.delete(appointment) + history.push('/appointments') + Toast( + 'success', + il8n.t('states.success'), + `${il8n.t('scheduling.appointments.successfullyDeleted')}`, + ) + dispatch(deleteAppointmentSuccess()) +} + export default appointmentSlice.reducer diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index c2e78947ac..bc837b9500 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -1,11 +1,13 @@ -import React, { useEffect } from 'react' +import React, { useEffect, useState } from 'react' import useTitle from 'page-header/useTitle' import { useSelector, useDispatch } from 'react-redux' import { RootState } from 'store' -import { useParams } from 'react-router' -import { Spinner } from '@hospitalrun/components' +import { useParams, useHistory } from 'react-router' +import { Spinner, Button, Modal } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' -import { fetchAppointment } from '../appointment-slice' +import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' +import Permissions from 'model/Permissions' +import { fetchAppointment, deleteAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' const ViewAppointment = () => { @@ -13,13 +15,45 @@ const ViewAppointment = () => { useTitle(t('scheduling.appointments.view')) const dispatch = useDispatch() const { id } = useParams() + const history = useHistory() const { appointment, patient, isLoading } = useSelector((state: RootState) => state.appointment) + const { permissions } = useSelector((state: RootState) => state.user) + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false) + + const setButtons = useButtonToolbarSetter() + + const onAppointmentDeleteButtonClick = (event: React.MouseEvent) => { + event.preventDefault() + setShowDeleteConfirmation(true) + } + + const onDeleteConfirmationButtonClick = () => { + dispatch(deleteAppointment(appointment, history)) + setShowDeleteConfirmation(false) + } + + const buttons = [] + if (permissions.includes(Permissions.DeleteAppointment)) { + buttons.push( + , + ) + } + + setButtons(buttons) useEffect(() => { if (id) { dispatch(fetchAppointment(id)) } - }, [dispatch, id]) + return () => setButtons([]) + }, [dispatch, id, setButtons]) if (!appointment.id || isLoading) { return @@ -35,6 +69,18 @@ const ViewAppointment = () => { // not editable }} /> + setShowDeleteConfirmation(false)} + />
) } diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index dce26fc088..ea24efd27a 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -11,6 +11,7 @@ const initialState: UserState = { Permissions.WritePatients, Permissions.ReadAppointments, Permissions.WriteAppointments, + Permissions.DeleteAppointment, ], } From 73c662a0dd479cfeb3e6306d9bb309a11c3254cd Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Sun, 23 Feb 2020 14:47:21 +0100 Subject: [PATCH 70/87] Update ViewAppointment.tsx --- src/scheduling/appointments/view/ViewAppointment.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index 2840beddde..5de18b8c2b 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -7,8 +7,8 @@ import { Spinner, Button, Modal } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import Permissions from 'model/Permissions' -import { fetchAppointment, deleteAppointment } from '../appointment-slice' import Appointment from 'model/Appointment' +import { fetchAppointment, deleteAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' From 04a21604d449f18351d15ae5c3eafea2e47b7e62 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 23 Feb 2020 21:05:29 -0600 Subject: [PATCH 71/87] feat(allergies): adds ability to add allergy to appointment --- .../patients/allergies/Allergies.test.tsx | 132 ++++++++++++++++++ .../allergies/NewAllergyModal.test.tsx | 90 ++++++++++++ .../patients/view/ViewPatient.test.tsx | 28 +++- src/locales/en-US/translation.json | 11 ++ src/model/Allergy.ts | 4 + src/model/Patient.ts | 2 + src/model/Permissions.ts | 1 + src/patients/allergies/Allergies.tsx | 86 ++++++++++++ src/patients/allergies/NewAllergyModal.tsx | 85 +++++++++++ src/patients/util/timestamp-id-generator.ts | 5 + src/patients/view/ViewPatient.tsx | 9 ++ src/user/user-slice.ts | 1 + 12 files changed, 453 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/patients/allergies/Allergies.test.tsx create mode 100644 src/__tests__/patients/allergies/NewAllergyModal.test.tsx create mode 100644 src/model/Allergy.ts create mode 100644 src/patients/allergies/Allergies.tsx create mode 100644 src/patients/allergies/NewAllergyModal.tsx create mode 100644 src/patients/util/timestamp-id-generator.ts diff --git a/src/__tests__/patients/allergies/Allergies.test.tsx b/src/__tests__/patients/allergies/Allergies.test.tsx new file mode 100644 index 0000000000..c2d0ec7392 --- /dev/null +++ b/src/__tests__/patients/allergies/Allergies.test.tsx @@ -0,0 +1,132 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import Allergies from 'patients/allergies/Allergies' +import Permissions from 'model/Permissions' +import configureMockStore from 'redux-mock-store' +import { createMemoryHistory } from 'history' +import thunk from 'redux-thunk' +import { Router } from 'react-router' +import { Provider } from 'react-redux' +import Patient from 'model/Patient' +import User from 'model/User' +import { Button, Modal, List, ListItem, Alert } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import { mocked } from 'ts-jest/utils' +import PatientRepository from 'clients/db/PatientRepository' +import Allergy from 'model/Allergy' +import NewAllergyModal from 'patients/allergies/NewAllergyModal' +import * as patientSlice from '../../../patients/patient-slice' + +const mockStore = configureMockStore([thunk]) +const history = createMemoryHistory() +const expectedPatient = { + id: '123', + rev: '123', + allergies: [ + { id: '1', name: 'allergy1' }, + { id: '2', name: 'allergy2' }, + ], +} as Patient + +let user: any +let store: any + +const setup = (patient = expectedPatient, permissions = [Permissions.AddAllergy]) => { + user = { permissions } as User + store = mockStore({ patient, user }) + const wrapper = mount( + + + + + , + ) + + return wrapper +} + +describe('Allergies', () => { + beforeEach(() => { + jest.resetAllMocks() + jest.spyOn(PatientRepository, 'saveOrUpdate') + }) + + describe('add new allergy button', () => { + it('should render a button to add new allergies', () => { + const wrapper = setup() + + const addAllergyButton = wrapper.find(Button) + expect(addAllergyButton).toHaveLength(1) + expect(addAllergyButton.text().trim()).toEqual('patient.allergies.new') + }) + + it('should not render a button to add new allergies if the user does not have permissions', () => { + const wrapper = setup(expectedPatient, []) + + const addAllergyButton = wrapper.find(Button) + expect(addAllergyButton).toHaveLength(0) + }) + + it('should open the New Allergy Modal when clicked', () => { + const wrapper = setup() + + act(() => { + const addAllergyButton = wrapper.find(Button) + const onClick = addAllergyButton.prop('onClick') as any + onClick({} as React.MouseEvent) + }) + + wrapper.update() + + expect(wrapper.find(Modal).prop('show')).toBeTruthy() + }) + + it('should update the patient with the new allergy when the save button is clicked', async () => { + const expectedAllergy = { name: 'name' } as Allergy + const expectedUpdatedPatient = { + ...expectedPatient, + allergies: [...(expectedPatient.allergies as any), expectedAllergy], + } as Patient + + const mockedPatientRepository = mocked(PatientRepository, true) + mockedPatientRepository.saveOrUpdate.mockResolvedValue(expectedUpdatedPatient) + + const wrapper = setup() + + await act(async () => { + const modal = wrapper.find(NewAllergyModal) + await modal.prop('onSave')(expectedAllergy) + }) + + expect(mockedPatientRepository.saveOrUpdate).toHaveBeenCalledWith(expectedUpdatedPatient) + expect(store.getActions()).toContainEqual(patientSlice.updatePatientStart()) + expect(store.getActions()).toContainEqual( + patientSlice.updatePatientSuccess(expectedUpdatedPatient), + ) + }) + }) + + describe('allergy list', () => { + it('should list the patients allergies', () => { + const allergies = expectedPatient.allergies as Allergy[] + const wrapper = setup() + + const list = wrapper.find(List) + const listItems = wrapper.find(ListItem) + + expect(list).toHaveLength(1) + expect(listItems).toHaveLength(allergies.length) + }) + + it('should render a warning message if the patient does not have any allergies', () => { + const wrapper = setup({ ...expectedPatient, allergies: [] }) + + const alert = wrapper.find(Alert) + + expect(alert).toHaveLength(1) + expect(alert.prop('title')).toEqual('patient.allergies.warning.noAllergies') + expect(alert.prop('message')).toEqual('patient.allergies.addAllergyAbove') + }) + }) +}) diff --git a/src/__tests__/patients/allergies/NewAllergyModal.test.tsx b/src/__tests__/patients/allergies/NewAllergyModal.test.tsx new file mode 100644 index 0000000000..e4601bc4f5 --- /dev/null +++ b/src/__tests__/patients/allergies/NewAllergyModal.test.tsx @@ -0,0 +1,90 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import NewAllergyModal from 'patients/allergies/NewAllergyModal' +import { shallow, mount } from 'enzyme' +import { Modal, Alert } from '@hospitalrun/components' +import { act } from '@testing-library/react' +import Allergy from 'model/Allergy' + +describe('New Allergy Modal', () => { + it('should render a modal with the correct labels', () => { + const wrapper = shallow( + , + ) + + const modal = wrapper.find(Modal) + expect(modal).toHaveLength(1) + expect(modal.prop('title')).toEqual('patient.allergies.new') + expect(modal.prop('closeButton')?.children).toEqual('actions.cancel') + expect(modal.prop('closeButton')?.color).toEqual('danger') + expect(modal.prop('successButton')?.children).toEqual('patient.allergies.new') + expect(modal.prop('successButton')?.color).toEqual('success') + expect(modal.prop('successButton')?.icon).toEqual('add') + }) + + describe('cancel', () => { + it('should call the onCloseButtonClick function when the close button is clicked', () => { + const onCloseButtonClickSpy = jest.fn() + const wrapper = shallow( + , + ) + + act(() => { + const modal = wrapper.find(Modal) + const { onClick } = modal.prop('closeButton') as any + onClick() + }) + + expect(onCloseButtonClickSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('save', () => { + it('should call the onSave function with the correct data when the save button is clicked', () => { + const expectedName = 'expected name' + const onSaveSpy = jest.fn() + const wrapper = mount( + , + ) + + act(() => { + const input = wrapper.findWhere((c) => c.prop('name') === 'name') + const onChange = input.prop('onChange') + onChange({ target: { value: expectedName } }) + }) + + wrapper.update() + + act(() => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + onSave({} as React.MouseEvent) + }) + + expect(onSaveSpy).toHaveBeenCalledTimes(1) + expect(onSaveSpy).toHaveBeenCalledWith({ name: expectedName } as Allergy) + }) + + it('should display an error message if the name field is not filled out', () => { + const wrapper = mount( + , + ) + + act(() => { + const modal = wrapper.find(Modal) + const onSave = (modal.prop('successButton') as any).onClick + onSave({} as React.MouseEvent) + }) + wrapper.update() + + expect(wrapper.find(Alert)).toHaveLength(1) + expect(wrapper.find(Alert).prop('title')).toEqual('states.error') + expect(wrapper.find(Alert).prop('message')).toContain('patient.allergies.error.nameRequired') + }) + }) +}) diff --git a/src/__tests__/patients/view/ViewPatient.test.tsx b/src/__tests__/patients/view/ViewPatient.test.tsx index 15bc7f1810..c0cd44ee45 100644 --- a/src/__tests__/patients/view/ViewPatient.test.tsx +++ b/src/__tests__/patients/view/ViewPatient.test.tsx @@ -12,6 +12,7 @@ import GeneralInformation from 'patients/GeneralInformation' import { createMemoryHistory } from 'history' import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab' import * as ButtonBarProvider from 'page-header/ButtonBarProvider' +import Allergies from 'patients/allergies/Allergies' import Patient from '../../../model/Patient' import PatientRepository from '../../../clients/db/PatientRepository' import * as titleUtil from '../../../page-header/useTitle' @@ -113,10 +114,11 @@ describe('ViewPatient', () => { const tabs = tabsHeader.find(Tab) expect(tabsHeader).toHaveLength(1) - expect(tabs).toHaveLength(3) + expect(tabs).toHaveLength(4) 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') + expect(tabs.at(3).prop('label')).toEqual('patient.allergies.label') }) it('should mark the general information tab as active and render the general information component when route is /patients/:id', async () => { @@ -173,4 +175,28 @@ describe('ViewPatient', () => { expect(relatedPersonTab).toHaveLength(1) expect(relatedPersonTab.prop('patient')).toEqual(patient) }) + + it('should mark the rallergies tab as active when it is clicked and render the allergies component when route is /patients/:id/allergies', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + await act(async () => { + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + tabs.at(3).prop('onClick')() + }) + + wrapper.update() + + const tabsHeader = wrapper.find(TabsHeader) + const tabs = tabsHeader.find(Tab) + const allergiesTab = wrapper.find(Allergies) + + expect(history.location.pathname).toEqual(`/patients/${patient.id}/allergies`) + expect(tabs.at(3).prop('active')).toBeTruthy() + expect(allergiesTab).toHaveLength(1) + expect(allergiesTab.prop('patient')).toEqual(patient) + }) }) diff --git a/src/locales/en-US/translation.json b/src/locales/en-US/translation.json index 59dd7f1e29..8d08fbe681 100644 --- a/src/locales/en-US/translation.json +++ b/src/locales/en-US/translation.json @@ -52,6 +52,17 @@ }, "errors": { "patientGivenNameRequired": "Patient Given Name is required." + }, + "allergies": { + "label": "Allergies", + "new": "Add Allergy", + "error": { + "nameRequired": "Name is required." + }, + "warning": { + "noAllergies": "No Allergies" + }, + "addAllergyAbove": "Add an allergy using the button above." } }, "sex": { diff --git a/src/model/Allergy.ts b/src/model/Allergy.ts new file mode 100644 index 0000000000..e4bb1e4c30 --- /dev/null +++ b/src/model/Allergy.ts @@ -0,0 +1,4 @@ +export default interface Allergy { + id: string + name: string +} diff --git a/src/model/Patient.ts b/src/model/Patient.ts index 9a8757cbe5..52db49648f 100644 --- a/src/model/Patient.ts +++ b/src/model/Patient.ts @@ -2,6 +2,7 @@ import AbstractDBModel from './AbstractDBModel' import Name from './Name' import ContactInformation from './ContactInformation' import RelatedPerson from './RelatedPerson' +import Allergy from './Allergy' export default interface Patient extends AbstractDBModel, Name, ContactInformation { sex: string @@ -12,4 +13,5 @@ export default interface Patient extends AbstractDBModel, Name, ContactInformati type?: string friendlyId: string relatedPersons?: RelatedPerson[] + allergies?: Allergy[] } diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts index fdb910cc63..b7403fb69e 100644 --- a/src/model/Permissions.ts +++ b/src/model/Permissions.ts @@ -4,6 +4,7 @@ enum Permissions { ReadAppointments = 'read:appointments', WriteAppointments = 'write:appointments', DeleteAppointment = 'delete:appointment', + AddAllergy = 'write:allergy', } export default Permissions diff --git a/src/patients/allergies/Allergies.tsx b/src/patients/allergies/Allergies.tsx new file mode 100644 index 0000000000..2f800fabbe --- /dev/null +++ b/src/patients/allergies/Allergies.tsx @@ -0,0 +1,86 @@ +import React, { useState } from 'react' +import useAddBreadcrumbs from 'breadcrumbs/useAddBreadcrumbs' +import Patient from 'model/Patient' +import { Button, List, ListItem, Alert } from '@hospitalrun/components' +import { useSelector, useDispatch } from 'react-redux' +import { RootState } from 'store' +import Permissions from 'model/Permissions' +import { useTranslation } from 'react-i18next' +import Allergy from 'model/Allergy' +import { useHistory } from 'react-router' +import { updatePatient } from 'patients/patient-slice' +import { getTimestampId } from 'patients/util/timestamp-id-generator' +import NewAllergyModal from './NewAllergyModal' + +interface AllergiesProps { + patient: Patient +} + +const Allergies = (props: AllergiesProps) => { + const { t } = useTranslation() + const history = useHistory() + const dispatch = useDispatch() + const { patient } = props + const { permissions } = useSelector((state: RootState) => state.user) + const [showNewAllergyModal, setShowNewAllergyModal] = useState(false) + + const breadcrumbs = [ + { + i18nKey: 'patient.allergies.label', + location: `/patients/${patient.id}/allergies`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + const onAddAllergy = (allergy: Allergy) => { + allergy.id = getTimestampId() + const allergies = [] + if (patient.allergies) { + allergies.push(...patient.allergies) + } + + allergies.push(allergy) + const patientToUpdate = { ...patient, allergies } + dispatch(updatePatient(patientToUpdate, history)) + } + + return ( + <> +
+
+ {permissions.includes(Permissions.AddAllergy) && ( + + )} +
+
+
+ {(!patient.allergies || patient.allergies.length === 0) && ( + + )} + + {patient.allergies?.map((a: Allergy) => ( + {a.name} + ))} + + setShowNewAllergyModal(false)} + onSave={(allergy) => onAddAllergy(allergy)} + /> + + ) +} + +export default Allergies diff --git a/src/patients/allergies/NewAllergyModal.tsx b/src/patients/allergies/NewAllergyModal.tsx new file mode 100644 index 0000000000..531f9e4207 --- /dev/null +++ b/src/patients/allergies/NewAllergyModal.tsx @@ -0,0 +1,85 @@ +import React, { useState, useEffect } from 'react' +import { Modal, Alert } from '@hospitalrun/components' +import { useTranslation } from 'react-i18next' +import Allergy from 'model/Allergy' +import TextInputWithLabelFormGroup from 'components/input/TextInputWithLabelFormGroup' + +interface NewAllergyModalProps { + show: boolean + onCloseButtonClick: () => void + onSave: (allergy: Allergy) => void +} + +const NewAllergyModal = (props: NewAllergyModalProps) => { + const { show, onCloseButtonClick, onSave } = props + const [allergy, setAllergy] = useState({ name: '' }) + const [errorMessage, setErrorMessage] = useState('') + const { t } = useTranslation() + + useEffect(() => { + setErrorMessage('') + setAllergy({ name: '' }) + }, [show]) + + const onNameChange = (event: React.ChangeEvent) => { + const name = event.target.value + setAllergy((prevAllergy) => ({ ...prevAllergy, name })) + } + + const onSaveButtonClick = () => { + let newErrorMessage = '' + if (!allergy.name) { + newErrorMessage += `${t('patient.allergies.error.nameRequired')} ` + } + + if (newErrorMessage) { + setErrorMessage(newErrorMessage.trim()) + return + } + + onSave(allergy as Allergy) + } + + const onClose = () => { + onCloseButtonClick() + } + + const body = ( + <> + {errorMessage && } + + + + + ) + + return ( + + ) +} + +export default NewAllergyModal diff --git a/src/patients/util/timestamp-id-generator.ts b/src/patients/util/timestamp-id-generator.ts new file mode 100644 index 0000000000..ae9198054a --- /dev/null +++ b/src/patients/util/timestamp-id-generator.ts @@ -0,0 +1,5 @@ +import { getTime } from 'date-fns' + +export function getTimestampId() { + return getTime(new Date()).toString() +} diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 80358d8562..3658ba2207 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -5,6 +5,7 @@ import { Panel, Spinner, TabsHeader, Tab, Button } from '@hospitalrun/components import { useTranslation } from 'react-i18next' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' +import Allergies from 'patients/allergies/Allergies' import useTitle from '../../page-header/useTitle' import { fetchPatient } from '../patient-slice' import { RootState } from '../../store' @@ -87,6 +88,11 @@ const ViewPatient = () => { label={t('scheduling.appointments.label')} onClick={() => history.push(`/patients/${patient.id}/appointments`)} /> + history.push(`/patients/${patient.id}/allergies`)} + /> @@ -98,6 +104,9 @@ const ViewPatient = () => { + + +
) diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index ea24efd27a..80089b0b02 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -12,6 +12,7 @@ const initialState: UserState = { Permissions.ReadAppointments, Permissions.WriteAppointments, Permissions.DeleteAppointment, + Permissions.AddAllergy, ], } From 656a03726000d143564744e91e2ccd05799d0bbe Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Mon, 24 Feb 2020 09:01:05 +0100 Subject: [PATCH 72/87] ci(deps): updates checkout action --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a32e065be..74288f2854 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: # node-version: 13.x steps: - run: git config --global core.autocrlf false # this is needed to prevent git changing EOL after cloning on Windows OS - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v1 with: @@ -48,7 +48,7 @@ jobs: # node-version: 13.x steps: - run: git config --global core.autocrlf false # this is needed to prevent git changing EOL after cloning on Windows OS - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v1 with: From 7b6b2cc5f5135f4b68256985c3056a75c8cdb7d6 Mon Sep 17 00:00:00 2001 From: aisultankassenov Date: Mon, 24 Feb 2020 14:39:30 +0600 Subject: [PATCH 73/87] fix(newPatient): correct input types of email and phone fields --- src/patients/GeneralInformation.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index c01525333a..60beb8021f 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -205,6 +205,7 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'phoneNumber') }} + type="number" />
@@ -217,6 +218,7 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'email') }} + type="email" />
From 21285004c478e07c590eed5ac2670b4eeec45e20 Mon Sep 17 00:00:00 2001 From: aisultankassenov Date: Mon, 24 Feb 2020 14:42:17 +0600 Subject: [PATCH 74/87] feat(textInput): add new input type tel --- src/components/input/TextInputWithLabelFormGroup.tsx | 2 +- src/patients/GeneralInformation.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/input/TextInputWithLabelFormGroup.tsx b/src/components/input/TextInputWithLabelFormGroup.tsx index 9946243d82..5f1d8ef2fa 100644 --- a/src/components/input/TextInputWithLabelFormGroup.tsx +++ b/src/components/input/TextInputWithLabelFormGroup.tsx @@ -6,7 +6,7 @@ interface Props { label: string name: string isEditable?: boolean - type: 'text' | 'email' | 'number' + type: 'text' | 'email' | 'number' | 'tel' placeholder?: string onChange?: (event: React.ChangeEvent) => void } diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index 60beb8021f..48fdd5faeb 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -205,7 +205,7 @@ const GeneralInformation = (props: Props) => { onChange={(event: React.ChangeEvent) => { onInputElementChange(event, 'phoneNumber') }} - type="number" + type="tel" />
From 6d5808a69635132d9ca10dcbb2c91b92b7c4c566 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2020 18:10:05 +0000 Subject: [PATCH 75/87] chore(deps-dev): bump @typescript-eslint/parser from 2.20.0 to 2.21.0 Bumps [@typescript-eslint/parser](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/parser) from 2.20.0 to 2.21.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/parser/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.21.0/packages/parser) 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 bfd5b89927..09dadad370 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "@types/react-router-dom": "~5.1.0", "@types/redux-mock-store": "~1.0.1", "@typescript-eslint/eslint-plugin": "~2.20.0", - "@typescript-eslint/parser": "~2.20.0", + "@typescript-eslint/parser": "~2.21.0", "commitizen": "~4.0.3", "commitlint-config-cz": "~0.13.0", "coveralls": "~3.0.9", From 6cb46e3719f19ec8ecfa0e56dca88334896f9568 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 24 Feb 2020 18:57:03 +0000 Subject: [PATCH 76/87] chore(deps-dev): bump @typescript-eslint/eslint-plugin Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.20.0 to 2.21.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v2.21.0/packages/eslint-plugin) 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 09dadad370..89d8d78d85 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@types/react-router": "~5.1.2", "@types/react-router-dom": "~5.1.0", "@types/redux-mock-store": "~1.0.1", - "@typescript-eslint/eslint-plugin": "~2.20.0", + "@typescript-eslint/eslint-plugin": "~2.21.0", "@typescript-eslint/parser": "~2.21.0", "commitizen": "~4.0.3", "commitlint-config-cz": "~0.13.0", From 1f837c6fe239e6385c9044e697e290694d9152eb Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Mon, 24 Feb 2020 22:41:51 -0600 Subject: [PATCH 77/87] Add bug label by default to bug report and remove code snippets --- .github/ISSUE_TEMPLATE/bug.md | 5 +++-- .github/ISSUE_TEMPLATE/regression.md | 2 +- .github/ISSUE_TEMPLATE/security.md | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 03825d0fd9..134be510b5 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,9 +1,10 @@ --- -name: 🐛 Bug report +name: "\U0001F41B Bug report" about: Create a report to help us improve title: '' -labels: '' +labels: bug assignees: '' + --- Before you submit an issue we recommend you drop into our [Spectrum channel](https://spectrum.chat/hospitalrun) and ask any questions you have or mention any problems you've had getting started with HospitalRun. diff --git a/.github/ISSUE_TEMPLATE/regression.md b/.github/ISSUE_TEMPLATE/regression.md index 81b24ec91a..798e409905 100644 --- a/.github/ISSUE_TEMPLATE/regression.md +++ b/.github/ISSUE_TEMPLATE/regression.md @@ -1,5 +1,5 @@ --- -name: 💥 Regression Report +name: "\U0001F4A5 Regression Report" about: Report unexpected behavior that worked in previous versions title: '' labels: '' diff --git a/.github/ISSUE_TEMPLATE/security.md b/.github/ISSUE_TEMPLATE/security.md index 5052e5681d..2abca344df 100644 --- a/.github/ISSUE_TEMPLATE/security.md +++ b/.github/ISSUE_TEMPLATE/security.md @@ -1,5 +1,5 @@ --- -name: 👮 Security Issue +name: "\U0001F46E Security Issue" about: Responsible Disclosure title: '' labels: '' From 1fa3859ef7fdbd4ca95fae9a27d37b9dce411fec Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2020 10:42:09 +0000 Subject: [PATCH 78/87] chore(deps): bump @hospitalrun/components from 0.33.3 to 0.34.0 Bumps [@hospitalrun/components](https://github.com/HospitalRun/components) from 0.33.3 to 0.34.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/v0.33.3...v0.34.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 89d8d78d85..3c9a163c55 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "private": false, "license": "MIT", "dependencies": { - "@hospitalrun/components": "^0.33.1", + "@hospitalrun/components": "^0.34.0", "@reduxjs/toolkit": "~1.2.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", From d320f0816254148b98a5e32c81aa75c5810e9575 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 25 Feb 2020 11:57:49 +0000 Subject: [PATCH 79/87] chore(deps): bump date-fns from 2.9.0 to 2.10.0 Bumps [date-fns](https://github.com/date-fns/date-fns) from 2.9.0 to 2.10.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.9.0...v2.10.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 3c9a163c55..470177424f 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@reduxjs/toolkit": "~1.2.1", "@types/pouchdb-find": "~6.3.4", "bootstrap": "~4.4.1", - "date-fns": "~2.9.0", + "date-fns": "~2.10.0", "i18next": "~19.3.1", "i18next-browser-languagedetector": "~4.0.1", "i18next-xhr-backend": "~3.2.2", From 3cee57808d00fc92057ab4cd5df0be4838ff166b Mon Sep 17 00:00:00 2001 From: oliv37 Date: Wed, 26 Feb 2020 20:24:12 +0100 Subject: [PATCH 80/87] fix(breadcrumb): add the breadcrumb for the edit appointment view fix #1854 --- src/__tests__/HospitalRun.test.tsx | 28 +++++++---- .../util/scheduling-appointment.util.test.ts | 46 +++++++++++++++++++ .../appointments/edit/EditAppointment.tsx | 14 ++++++ .../util/scheduling-appointment.util.ts | 9 ++++ .../appointments/view/ViewAppointment.tsx | 10 +--- 5 files changed, 90 insertions(+), 17 deletions(-) create mode 100644 src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts create mode 100644 src/scheduling/appointments/util/scheduling-appointment.util.ts diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index f352146852..bf7572ae68 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -327,15 +327,15 @@ describe('HospitalRun', () => { mockedAppointmentRepository.find.mockResolvedValue(appointment) mockedPatientRepository.find.mockResolvedValue(patient) + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WriteAppointments, Permissions.ReadAppointments] }, + appointment: { appointment, patient: {} as Patient }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -343,6 +343,18 @@ describe('HospitalRun', () => { ) expect(wrapper.find(EditAppointment)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { text: '123', location: '/appointments/123' }, + { + i18nKey: 'scheduling.appointments.editAppointment', + location: '/appointments/edit/123', + }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), + ) }) it('should render the Dashboard when the user does not have read appointment privileges', () => { diff --git a/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts new file mode 100644 index 0000000000..f802b15c66 --- /dev/null +++ b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts @@ -0,0 +1,46 @@ +import Appointment from 'model/Appointment' +import { getAppointmentLabel } from '../../../../scheduling/appointments/util/scheduling-appointment.util' + +describe('scheduling appointment util', () => { + describe('getAppointmentLabel', () => { + it('should return the locale string representation of the start time and end time', () => { + const appointment = { + id: '123', + startDateTime: '2020-03-07T18:15:00.000Z', + endDateTime: '2020-03-07T20:15:00.000Z', + } as Appointment + + expect(getAppointmentLabel(appointment)).toEqual( + `${new Date(appointment.startDateTime).toLocaleString()} - ${new Date( + appointment.endDateTime, + ).toLocaleString()}`, + ) + }) + + it('should return the appointment id when start time is not defined', () => { + const appointment = { + id: '123', + startDateTime: '2020-03-07T18:15:00.000Z', + } as Appointment + + expect(getAppointmentLabel(appointment)).toEqual('123') + }) + + it('should return the appointment id when end time is not defined', () => { + const appointment = { + id: '123', + endDateTime: '2020-03-07T20:15:00.000Z', + } as Appointment + + expect(getAppointmentLabel(appointment)).toEqual('123') + }) + + it('should return the appointment id when start time and end time are not defined', () => { + const appointment = { + id: '123', + } as Appointment + + expect(getAppointmentLabel(appointment)).toEqual('123') + }) + }) +}) diff --git a/src/scheduling/appointments/edit/EditAppointment.tsx b/src/scheduling/appointments/edit/EditAppointment.tsx index ba88a17720..d2db65f976 100644 --- a/src/scheduling/appointments/edit/EditAppointment.tsx +++ b/src/scheduling/appointments/edit/EditAppointment.tsx @@ -10,6 +10,8 @@ import useTitle from '../../../page-header/useTitle' import Appointment from '../../../model/Appointment' import { updateAppointment, fetchAppointment } from '../appointment-slice' import { RootState } from '../../../store' +import { getAppointmentLabel } from '../util/scheduling-appointment.util' +import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' const EditAppointment = () => { const { t } = useTranslation() @@ -22,6 +24,18 @@ const EditAppointment = () => { const { appointment: reduxAppointment, patient, isLoading } = useSelector( (state: RootState) => state.appointment, ) + const breadcrumbs = [ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { + text: getAppointmentLabel(reduxAppointment), + location: `/appointments/${reduxAppointment.id}`, + }, + { + i18nKey: 'scheduling.appointments.editAppointment', + location: `/appointments/edit/${reduxAppointment.id}`, + }, + ] + useAddBreadcrumbs(breadcrumbs, true) useEffect(() => { setAppointment(reduxAppointment) diff --git a/src/scheduling/appointments/util/scheduling-appointment.util.ts b/src/scheduling/appointments/util/scheduling-appointment.util.ts new file mode 100644 index 0000000000..02f290174f --- /dev/null +++ b/src/scheduling/appointments/util/scheduling-appointment.util.ts @@ -0,0 +1,9 @@ +import Appointment from '../../../model/Appointment' + +export function getAppointmentLabel(appointment: Appointment) { + const { id, startDateTime, endDateTime } = appointment + + return startDateTime && endDateTime + ? `${new Date(startDateTime).toLocaleString()} - ${new Date(endDateTime).toLocaleString()}` + : id +} diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index b8f6297756..37a28fea2e 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -7,19 +7,11 @@ import { Spinner, Button, Modal } from '@hospitalrun/components' import { useTranslation } from 'react-i18next' import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider' import Permissions from 'model/Permissions' -import Appointment from 'model/Appointment' import { fetchAppointment, deleteAppointment } from '../appointment-slice' import AppointmentDetailForm from '../AppointmentDetailForm' +import { getAppointmentLabel } from '../util/scheduling-appointment.util' import useAddBreadcrumbs from '../../../breadcrumbs/useAddBreadcrumbs' -function getAppointmentLabel(appointment: Appointment) { - const { id, startDateTime, endDateTime } = appointment - - return startDateTime && endDateTime - ? `${new Date(startDateTime).toLocaleString()} - ${new Date(endDateTime).toLocaleString()}` - : id -} - const ViewAppointment = () => { const { t } = useTranslation() useTitle(t('scheduling.appointments.viewAppointment')) From 2a8e778a95721102d9b2f65b1ee6cd787be60370 Mon Sep 17 00:00:00 2001 From: Revln9 Date: Wed, 26 Feb 2020 20:27:19 +0100 Subject: [PATCH 81/87] Add CTA slack adding the call to action to join the slack group --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3237cfa5f6..8db2c97a50 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ React frontend for [HospitalRun](http://hospitalrun.io/): free software for deve - To contribute, follow the guidelines in the readme or alternatively ask for details on Slack channel [#contributors](https://hospitalrun-slack.herokuapp.com). - To use version 1.0.0-beta (not production ready) in a hospital facility, ask for support on Slack channel [#troubleshooting](https://hospitalrun-slack.herokuapp.com) or Spectrum channel [#support](https://spectrum.chat/hospitalrun). +### [Join our slack development group](https://hospitalrun-slack.herokuapp.com) # Contributing From 9f2654a65ee105d4c97f4ace4a89c69a70316cc0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2020 20:25:03 +0000 Subject: [PATCH 82/87] chore(deps-dev): bump eslint-plugin-react-hooks from 2.4.0 to 2.5.0 Bumps [eslint-plugin-react-hooks](https://github.com/facebook/react/tree/HEAD/packages/eslint-plugin-react-hooks) from 2.4.0 to 2.5.0. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/HEAD/packages/eslint-plugin-react-hooks) 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 470177424f..31081dac17 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,7 @@ "eslint-plugin-jsx-a11y": "~6.2.3", "eslint-plugin-prettier": "~3.1.1", "eslint-plugin-react": "~7.18.0", - "eslint-plugin-react-hooks": "~2.4.0", + "eslint-plugin-react-hooks": "~2.5.0", "history": "~4.10.1", "husky": "~4.2.1", "jest": "~24.9.0", From 9e25579de8ed5a52b1c74f45d4ab03d6c700d14a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 26 Feb 2020 20:41:42 +0000 Subject: [PATCH 83/87] chore(deps): bump react-dom from 16.12.0 to 16.13.0 Bumps [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) from 16.12.0 to 16.13.0. - [Release notes](https://github.com/facebook/react/releases) - [Changelog](https://github.com/facebook/react/blob/master/CHANGELOG.md) - [Commits](https://github.com/facebook/react/commits/v16.13.0/packages/react-dom) 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 470177424f..a7a9650b8a 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "react": "~16.12.0", "react-bootstrap": "~1.0.0-beta.16", "react-bootstrap-typeahead": "~3.4.7", - "react-dom": "~16.12.0", + "react-dom": "~16.13.0", "react-i18next": "~11.3.0", "react-redux": "~7.2.0", "react-router": "~5.1.2", From 38efb2ef97f669043b132d6704e54f5f3b1c1848 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Thu, 27 Feb 2020 17:41:54 +0100 Subject: [PATCH 84/87] Update README.md --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8db2c97a50..1fef51aa1f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,12 @@ React frontend for [HospitalRun](http://hospitalrun.io/): free software for deve - To contribute, follow the guidelines in the readme or alternatively ask for details on Slack channel [#contributors](https://hospitalrun-slack.herokuapp.com). - To use version 1.0.0-beta (not production ready) in a hospital facility, ask for support on Slack channel [#troubleshooting](https://hospitalrun-slack.herokuapp.com) or Spectrum channel [#support](https://spectrum.chat/hospitalrun). -### [Join our slack development group](https://hospitalrun-slack.herokuapp.com) + +
+ +[![Slack](https://img.shields.io/badge/Slack-Join%20our%20devs%20group-blueviolet?style=for-the-badge&logo=slack)](https://hospitalrun-slack.herokuapp.com) + +
# Contributing From ff85291db64343094fdb415163ac82118223bd12 Mon Sep 17 00:00:00 2001 From: Matteo Vivona Date: Thu, 27 Feb 2020 17:42:40 +0100 Subject: [PATCH 85/87] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 75dffd003e..9b3dafb4e9 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "pouchdb-adapter-memory": "~7.2.1", "pouchdb-find": "~7.2.1", "pouchdb-quick-search": "~1.3.0", - "react": "~16.12.0", + "react": "~16.13.0", "react-bootstrap": "~1.0.0-beta.16", "react-bootstrap-typeahead": "~3.4.7", "react-dom": "~16.13.0", From 33b4dc31f710af25266dbb786983cb68936ef2f3 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Thu, 27 Feb 2020 22:19:13 +0100 Subject: [PATCH 86/87] fix(breadcrumb): set appointment date precision to minutes in breadcrumb fix #1853 --- .../util/scheduling-appointment.util.test.ts | 17 ++++++++++++----- .../util/scheduling-appointment.util.ts | 14 +++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts index f802b15c66..292e71cd9d 100644 --- a/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts +++ b/src/__tests__/scheduling/appointments/util/scheduling-appointment.util.test.ts @@ -4,17 +4,24 @@ import { getAppointmentLabel } from '../../../../scheduling/appointments/util/sc describe('scheduling appointment util', () => { describe('getAppointmentLabel', () => { it('should return the locale string representation of the start time and end time', () => { + const options = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + } + const appointment = { id: '123', startDateTime: '2020-03-07T18:15:00.000Z', endDateTime: '2020-03-07T20:15:00.000Z', } as Appointment - expect(getAppointmentLabel(appointment)).toEqual( - `${new Date(appointment.startDateTime).toLocaleString()} - ${new Date( - appointment.endDateTime, - ).toLocaleString()}`, - ) + const startDateLabel = new Date(appointment.startDateTime).toLocaleString([], options) + const endDateLabel = new Date(appointment.endDateTime).toLocaleString([], options) + + expect(getAppointmentLabel(appointment)).toEqual(`${startDateLabel} - ${endDateLabel}`) }) it('should return the appointment id when start time is not defined', () => { diff --git a/src/scheduling/appointments/util/scheduling-appointment.util.ts b/src/scheduling/appointments/util/scheduling-appointment.util.ts index 02f290174f..eb1d28bdd8 100644 --- a/src/scheduling/appointments/util/scheduling-appointment.util.ts +++ b/src/scheduling/appointments/util/scheduling-appointment.util.ts @@ -1,9 +1,21 @@ import Appointment from '../../../model/Appointment' +const options = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', +} + +function toLocaleString(date: Date) { + return date.toLocaleString([], options) +} + export function getAppointmentLabel(appointment: Appointment) { const { id, startDateTime, endDateTime } = appointment return startDateTime && endDateTime - ? `${new Date(startDateTime).toLocaleString()} - ${new Date(endDateTime).toLocaleString()}` + ? `${toLocaleString(new Date(startDateTime))} - ${toLocaleString(new Date(endDateTime))}` : id } From 9b2e721a0d9e7b33cd91fe7da4e110dfc2130ee6 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Fri, 28 Feb 2020 20:05:55 +0100 Subject: [PATCH 87/87] fix(button toolbar): align buttons to the right in the toolbar fix #1852 --- src/__tests__/page-header/ButtonToolBar.test.tsx | 11 ++++++++++- src/index.css | 4 ++++ src/page-header/ButtonToolBar.tsx | 7 ++++++- 3 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/__tests__/page-header/ButtonToolBar.test.tsx b/src/__tests__/page-header/ButtonToolBar.test.tsx index 5c79e2f337..ff369b086d 100644 --- a/src/__tests__/page-header/ButtonToolBar.test.tsx +++ b/src/__tests__/page-header/ButtonToolBar.test.tsx @@ -19,9 +19,18 @@ describe('Button Tool Bar', () => { jest.spyOn(ButtonBarProvider, 'useButtons') mocked(ButtonBarProvider).useButtons.mockReturnValue(buttons) - const wrapper = mount() + const wrapper = mount().find('.button-toolbar') expect(wrapper.childAt(0).getElement()).toEqual(buttons[0]) expect(wrapper.childAt(1).getElement()).toEqual(buttons[1]) }) + + it('should return null when there is no button in the provider', () => { + jest.spyOn(ButtonBarProvider, 'useButtons') + mocked(ButtonBarProvider).useButtons.mockReturnValue([]) + + const wrapper = mount() + + expect(wrapper.html()).toBeNull() + }) }) diff --git a/src/index.css b/src/index.css index 417dbe2316..0c52028d7a 100644 --- a/src/index.css +++ b/src/index.css @@ -93,3 +93,7 @@ code { padding: 0; background-color: white; } + +.button-toolbar > button { + margin-left: .5rem; +} \ No newline at end of file diff --git a/src/page-header/ButtonToolBar.tsx b/src/page-header/ButtonToolBar.tsx index 05d8be02f0..72eec7b627 100644 --- a/src/page-header/ButtonToolBar.tsx +++ b/src/page-header/ButtonToolBar.tsx @@ -3,7 +3,12 @@ import { useButtons } from './ButtonBarProvider' const ButtonToolBar = () => { const buttons = useButtons() - return <>{buttons.map((button) => button)} + + if (buttons.length === 0) { + return null + } + + return
{buttons.map((button) => button)}
} export default ButtonToolBar