From 65b40aeba2c9f75d4d5944e046cdaa7da126f1d1 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 8 Feb 2020 16:28:41 +0100 Subject: [PATCH 01/15] 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 a68ed7e282ec630049671f1f4a292cae6ed91379 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sun, 9 Feb 2020 19:39:13 +0100 Subject: [PATCH 02/15] 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 03/15] 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 a4c1cfbcd6ab52579b469e1df54d44c0769c25bd Mon Sep 17 00:00:00 2001 From: oliv37 Date: Tue, 11 Feb 2020 20:39:56 +0100 Subject: [PATCH 04/15] 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 a6eaf2a9547875a4a0947f4e9a7e21e41add11c8 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Wed, 12 Feb 2020 20:22:46 +0100 Subject: [PATCH 05/15] 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 5beda756d134439911cd4a412147a66e1b222176 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Sat, 15 Feb 2020 14:58:00 +0100 Subject: [PATCH 06/15] 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 07/15] 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 08/15] 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 bbb3ca06ec2d3ed4622b79cea81558a87519a8a8 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Mon, 17 Feb 2020 19:36:10 +0100 Subject: [PATCH 09/15] feat(breadcrumb): sort the breadcrumbs in addBreadcrumbs action fix #1770 --- .../breadcrumbs/Breadcrumbs.test.tsx | 2 +- .../breadcrumbs/breadcrumbs-slice.test.ts | 24 ++++++++++++++++++- src/breadcrumbs/Breadcrumbs.tsx | 21 +++++++--------- src/breadcrumbs/breadcrumbs-slice.ts | 4 +++- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx index e36f64ceea..586bbf748e 100644 --- a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx +++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx @@ -32,9 +32,9 @@ describe('Breadcrumbs', () => { it('should render breadcrumbs items', () => { const breadcrumbs = [ - { text: 'Edit Patient', location: '/patient/1/edit' }, { i18nKey: 'patient.label', location: '/patient' }, { text: 'Bob', location: '/patient/1' }, + { text: 'Edit Patient', location: '/patient/1/edit' }, ] const wrapper = setup(breadcrumbs) diff --git a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts index f3b6f7da35..10f73f19ff 100644 --- a/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts +++ b/src/__tests__/breadcrumbs/breadcrumbs-slice.test.ts @@ -24,7 +24,7 @@ describe('breadcrumbs slice', () => { expect(breadcrumbsStore.breadcrumbs).toEqual(breadcrumbsToAdd) }) - it('should handle the ADD_BREADCRUMBS action with existing breadcreumbs', () => { + it('should handle the ADD_BREADCRUMBS action with existing breadcrumbs', () => { const breadcrumbsToAdd = [{ text: 'Bob', location: '/user/1' }] const state = { @@ -39,6 +39,28 @@ describe('breadcrumbs slice', () => { expect(breadcrumbsStore.breadcrumbs).toEqual([...state.breadcrumbs, ...breadcrumbsToAdd]) }) + it('should handle the ADD_BREADCRUMBS action and sort the breadcrumbs by their location', () => { + const breadcrumbsToAdd = [{ text: 'Bob', location: '/user/1/' }] + + const state = { + breadcrumbs: [ + { text: 'user', location: '/user' }, + { text: 'edit user', location: '/user/1/edit' }, + ], + } + + const breadcrumbsStore = breadcrumbs(state, { + type: addBreadcrumbs.type, + payload: breadcrumbsToAdd, + }) + + expect(breadcrumbsStore.breadcrumbs).toEqual([ + { text: 'user', location: '/user' }, + { text: 'Bob', location: '/user/1/' }, + { text: 'edit user', location: '/user/1/edit' }, + ]) + }) + it('should handle the REMOVE_BREADCRUMBS action', () => { const breadcrumbsToRemove = [{ text: 'Bob', location: '/user/1' }] diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx index ba666ece17..14cf3ddd33 100644 --- a/src/breadcrumbs/Breadcrumbs.tsx +++ b/src/breadcrumbs/Breadcrumbs.tsx @@ -12,19 +12,16 @@ const Breadcrumbs = () => { return ( - {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 + {breadcrumbs.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 2cc55424a3..86a689e0d8 100644 --- a/src/breadcrumbs/breadcrumbs-slice.ts +++ b/src/breadcrumbs/breadcrumbs-slice.ts @@ -14,7 +14,9 @@ const breadcrumbsSlice = createSlice({ initialState, reducers: { addBreadcrumbs(state, { payload }: PayloadAction) { - state.breadcrumbs = [...state.breadcrumbs, ...payload] + state.breadcrumbs = [...state.breadcrumbs, ...payload].sort( + (b1, b2) => b1.location.length - b2.location.length, + ) }, removeBreadcrumbs(state, { payload }: PayloadAction) { const locations = payload.map((b) => b.location) From 492cb37c593f94ad17cce4a26fa527990b1c9639 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Mon, 17 Feb 2020 21:37:59 +0100 Subject: [PATCH 10/15] feat(breadcrumb): use reduxPatient instead of patient for breadcrumbs --- src/__tests__/HospitalRun.test.tsx | 26 ++++++++++++++++++-------- src/patients/edit/EditPatient.tsx | 4 ++-- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 5886ad7c3b..bbfec06db1 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -7,10 +7,12 @@ 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' +import { addBreadcrumbs } from 'breadcrumbs/breadcrumbs-slice' import NewPatient from '../patients/new/NewPatient' import EditPatient from '../patients/edit/EditPatient' import ViewPatient from '../patients/view/ViewPatient' @@ -76,15 +78,15 @@ describe('HospitalRun', () => { mockedPatientRepository.find.mockResolvedValue(patient) + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WritePatients, Permissions.ReadPatients] }, + patient: { patient }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -92,6 +94,14 @@ describe('HospitalRun', () => { ) expect(wrapper.find(EditPatient)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'patients.label', location: '/patients' }, + { text: 'test test test', location: `/patients/${patient.id}` }, + { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, + ]), + ) }) it('should render the Dashboard when the user does not have read patient privileges', () => { diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index f6f2e455ac..f75a7f5c4c 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -37,8 +37,8 @@ const EditPatient = () => { const breadcrumbs = [ { i18nKey: 'patients.label', location: '/patients' }, - { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, - { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, + { text: getPatientFullName(reduxPatient), location: `/patients/${reduxPatient.id}` }, + { i18nKey: 'patients.editPatient', location: `/patients/${reduxPatient.id}/edit` }, ] useAddBreadcrumbs(breadcrumbs) From b5e4c387ed43d1373c43bc5a2a66893c090bad27 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Mon, 17 Feb 2020 21:55:31 +0100 Subject: [PATCH 11/15] feat(breadcrumb): test the dispatch of addBreadcrumbs action --- src/__tests__/HospitalRun.test.tsx | 85 +++++++++++++------- src/scheduling/appointments/Appointments.tsx | 2 +- 2 files changed, 56 insertions(+), 31 deletions(-) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index bbfec06db1..bdf1cd4bc4 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -27,14 +27,14 @@ describe('HospitalRun', () => { describe('routing', () => { describe('/patients/new', () => { it('should render the new patient screen when /patients/new is accessed', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WritePatients] }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -42,6 +42,13 @@ describe('HospitalRun', () => { ) expect(wrapper.find(NewPatient)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'patients.label', location: '/patients' }, + { i18nKey: 'patients.newPatient', location: '/patients/new' }, + ]), + ) }) it('should render the Dashboard if the user does not have write patient privileges', () => { @@ -156,15 +163,15 @@ describe('HospitalRun', () => { mockedPatientRepository.find.mockResolvedValue(patient) + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ReadPatients] }, + patient: { patient }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -172,6 +179,13 @@ describe('HospitalRun', () => { ) expect(wrapper.find(ViewPatient)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'patients.label', location: '/patients' }, + { text: 'test test test', location: `/patients/${patient.id}` }, + ]), + ) }) it('should render the Dashboard when the user does not have read patient privileges', () => { @@ -195,15 +209,15 @@ describe('HospitalRun', () => { describe('/appointments', () => { it('should render the appointments screen when /appointments is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ReadAppointments] }, + appointments: { appointments: [] }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -215,6 +229,10 @@ describe('HospitalRun', () => { }) expect(wrapper.find(Appointments)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }]), + ) }) it('should render the Dashboard when the user does not have read appointment privileges', () => { @@ -239,14 +257,14 @@ describe('HospitalRun', () => { describe('/appointments/new', () => { it('should render the new appointment screen when /appointments/new is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.WriteAppointments] }, + breadcrumbs: { breadcrumbs: [] }, + }) + const wrapper = mount( - + @@ -254,6 +272,13 @@ describe('HospitalRun', () => { ) expect(wrapper.find(NewAppointment)).toHaveLength(1) + + expect(store.getActions()).toContainEqual( + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, + ]), + ) }) it('should render the Dashboard when the user does not have read appointment privileges', () => { diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index b52194b922..28fdb48f1f 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -17,7 +17,7 @@ interface Event { allDay: boolean } -const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/patients' }] +const breadcrumbs = [{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }] const Appointments = () => { const { t } = useTranslation() From beffc1f059c748e880b9f76766c955d8e5094f08 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Thu, 20 Feb 2020 20:31:55 +0100 Subject: [PATCH 12/15] feat(breadcrumb): add the dashboard breadcrumb item add the dashboard item for all the breacrumbs, don't display the breadcrumb for the Dashboard page --- src/__tests__/HospitalRun.test.tsx | 9 ++++++++- .../breadcrumbs/Breadcrumbs.test.tsx | 13 +++++++++++-- .../breadcrumbs/useAddBreadcrumbs.test.tsx | 19 +++++++++++++++++++ src/breadcrumbs/Breadcrumbs.tsx | 4 ++++ src/breadcrumbs/useAddBreadcrumbs.ts | 9 ++++++--- src/dashboard/Dashboard.tsx | 4 ---- src/patients/edit/EditPatient.tsx | 2 +- src/patients/list/Patients.tsx | 2 +- src/patients/new/NewPatient.tsx | 2 +- src/patients/view/ViewPatient.tsx | 2 +- src/scheduling/appointments/Appointments.tsx | 2 +- .../appointments/new/NewAppointment.tsx | 2 +- .../appointments/view/ViewAppointment.tsx | 2 +- 13 files changed, 55 insertions(+), 17 deletions(-) diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index bdf1cd4bc4..d3cc0e40f7 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -47,6 +47,7 @@ describe('HospitalRun', () => { addBreadcrumbs([ { i18nKey: 'patients.label', location: '/patients' }, { i18nKey: 'patients.newPatient', location: '/patients/new' }, + { i18nKey: 'dashboard.label', location: '/' }, ]), ) }) @@ -107,6 +108,7 @@ describe('HospitalRun', () => { { i18nKey: 'patients.label', location: '/patients' }, { text: 'test test test', location: `/patients/${patient.id}` }, { i18nKey: 'patients.editPatient', location: `/patients/${patient.id}/edit` }, + { i18nKey: 'dashboard.label', location: '/' }, ]), ) }) @@ -184,6 +186,7 @@ describe('HospitalRun', () => { addBreadcrumbs([ { i18nKey: 'patients.label', location: '/patients' }, { text: 'test test test', location: `/patients/${patient.id}` }, + { i18nKey: 'dashboard.label', location: '/' }, ]), ) }) @@ -231,7 +234,10 @@ describe('HospitalRun', () => { expect(wrapper.find(Appointments)).toHaveLength(1) expect(store.getActions()).toContainEqual( - addBreadcrumbs([{ i18nKey: 'scheduling.appointments.label', location: '/appointments' }]), + addBreadcrumbs([ + { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, + { i18nKey: 'dashboard.label', location: '/' }, + ]), ) }) @@ -277,6 +283,7 @@ describe('HospitalRun', () => { addBreadcrumbs([ { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, { i18nKey: 'scheduling.appointments.new', location: '/appointments/new' }, + { i18nKey: 'dashboard.label', location: '/' }, ]), ) }) diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx index 586bbf748e..e34a022a17 100644 --- a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx +++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx @@ -5,7 +5,10 @@ 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' +import { + Breadcrumb as HRBreadcrumb, + BreadcrumbItem as HRBreadcrumbItem, +} from '@hospitalrun/components' import Breadcrumbs from 'breadcrumbs/Breadcrumbs' import Breadcrumb from 'model/Breadcrumb' @@ -30,6 +33,12 @@ describe('Breadcrumbs', () => { return wrapper } + it('should not render the breadcrumb when there is no items in the store', () => { + const wrapper = setup([]) + + expect(wrapper.find(HRBreadcrumb)).toHaveLength(0) + }) + it('should render breadcrumbs items', () => { const breadcrumbs = [ { i18nKey: 'patient.label', location: '/patient' }, @@ -38,7 +47,7 @@ describe('Breadcrumbs', () => { ] const wrapper = setup(breadcrumbs) - const items = wrapper.find(BreadcrumbItem) + const items = wrapper.find(HRBreadcrumbItem) expect(items).toHaveLength(3) expect(items.at(0).text()).toEqual('patient.label') diff --git a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx index 7a118f01a1..452f76ebdc 100644 --- a/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx +++ b/src/__tests__/breadcrumbs/useAddBreadcrumbs.test.tsx @@ -26,6 +26,25 @@ describe('useAddBreadcrumbs', () => { expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledWith(breadcrumbs) }) + it('should call addBreadcrumbs with an additional dashboard breadcrumb', () => { + const wrapper = ({ children }: any) => {children} + + jest.spyOn(breadcrumbsSlice, 'addBreadcrumbs') + const breadcrumbs = [ + { + text: 'Patients', + location: '/patients', + }, + ] + + renderHook(() => useAddBreadcrumbs(breadcrumbs, true), { wrapper } as any) + expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledTimes(1) + expect(breadcrumbsSlice.addBreadcrumbs).toHaveBeenCalledWith([ + ...breadcrumbs, + { i18nKey: 'dashboard.label', location: '/' }, + ]) + }) + it('should call removeBreadcrumbs with the correct data after unmount', () => { const wrapper = ({ children }: any) => {children} diff --git a/src/breadcrumbs/Breadcrumbs.tsx b/src/breadcrumbs/Breadcrumbs.tsx index 14cf3ddd33..263f8e9476 100644 --- a/src/breadcrumbs/Breadcrumbs.tsx +++ b/src/breadcrumbs/Breadcrumbs.tsx @@ -10,6 +10,10 @@ const Breadcrumbs = () => { const { t } = useTranslation() const { breadcrumbs } = useSelector((state: RootState) => state.breadcrumbs) + if (breadcrumbs.length === 0) { + return null + } + return ( {breadcrumbs.map(({ i18nKey, text, location }, index) => { diff --git a/src/breadcrumbs/useAddBreadcrumbs.ts b/src/breadcrumbs/useAddBreadcrumbs.ts index 135e3fe5f2..76d68e80ce 100644 --- a/src/breadcrumbs/useAddBreadcrumbs.ts +++ b/src/breadcrumbs/useAddBreadcrumbs.ts @@ -3,12 +3,15 @@ import { useDispatch } from 'react-redux' import Breadcrumb from 'model/Breadcrumb' import { addBreadcrumbs, removeBreadcrumbs } from './breadcrumbs-slice' -export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[]): void { +export default function useAddBreadcrumbs(breadcrumbs: Breadcrumb[], withDashboard = false): void { const dispatch = useDispatch() - const breadcrumbsStringified = JSON.stringify(breadcrumbs) + + const breadcrumbsStringified = withDashboard + ? JSON.stringify([...breadcrumbs, { i18nKey: 'dashboard.label', location: '/' }]) + : JSON.stringify(breadcrumbs) useEffect(() => { - const breadcrumbsParsed = JSON.parse(breadcrumbsStringified) + const breadcrumbsParsed: Breadcrumb[] = JSON.parse(breadcrumbsStringified) dispatch(addBreadcrumbs(breadcrumbsParsed)) return () => { diff --git a/src/dashboard/Dashboard.tsx b/src/dashboard/Dashboard.tsx index 1324a028df..bf295875dc 100644 --- a/src/dashboard/Dashboard.tsx +++ b/src/dashboard/Dashboard.tsx @@ -1,14 +1,10 @@ import React from 'react' import { useTranslation } from 'react-i18next' import useTitle from '../page-header/useTitle' -import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' - -const breadcrumbs = [{ i18nKey: 'dashboard.label', location: '/' }] const Dashboard: React.FC = () => { const { t } = useTranslation() useTitle(t('dashboard.label')) - useAddBreadcrumbs(breadcrumbs) return

Example

} diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index f75a7f5c4c..2e3db2da99 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -40,7 +40,7 @@ const EditPatient = () => { { text: getPatientFullName(reduxPatient), location: `/patients/${reduxPatient.id}` }, { i18nKey: 'patients.editPatient', location: `/patients/${reduxPatient.id}/edit` }, ] - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) useEffect(() => { setPatient(reduxPatient) diff --git a/src/patients/list/Patients.tsx b/src/patients/list/Patients.tsx index 9696661830..0b28ff4e9e 100644 --- a/src/patients/list/Patients.tsx +++ b/src/patients/list/Patients.tsx @@ -14,7 +14,7 @@ const Patients = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('patients.label')) - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) 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 971eafff32..5c80cdc0b6 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -25,7 +25,7 @@ const NewPatient = () => { const [errorMessage, setErrorMessage] = useState('') useTitle(t('patients.newPatient')) - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) const onCancel = () => { history.push('/patients') diff --git a/src/patients/view/ViewPatient.tsx b/src/patients/view/ViewPatient.tsx index 527b3ecb95..f3f7339fb4 100644 --- a/src/patients/view/ViewPatient.tsx +++ b/src/patients/view/ViewPatient.tsx @@ -36,7 +36,7 @@ const ViewPatient = () => { { i18nKey: 'patients.label', location: '/patients' }, { text: getPatientFullName(patient), location: `/patients/${patient.id}` }, ] - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) const { id } = useParams() useEffect(() => { diff --git a/src/scheduling/appointments/Appointments.tsx b/src/scheduling/appointments/Appointments.tsx index 28fdb48f1f..b0af31a3af 100644 --- a/src/scheduling/appointments/Appointments.tsx +++ b/src/scheduling/appointments/Appointments.tsx @@ -23,7 +23,7 @@ const Appointments = () => { const { t } = useTranslation() const history = useHistory() useTitle(t('scheduling.appointments.label')) - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) 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 c705352fb9..487948f5eb 100644 --- a/src/scheduling/appointments/new/NewAppointment.tsx +++ b/src/scheduling/appointments/new/NewAppointment.tsx @@ -22,7 +22,7 @@ const NewAppointment = () => { const history = useHistory() const dispatch = useDispatch() useTitle(t('scheduling.appointments.new')) - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) const startDateTime = roundToNearestMinutes(new Date(), { nearestTo: 15 }) const endDateTime = addMinutes(startDateTime, 60) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index e25dc8825c..5744041875 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -29,7 +29,7 @@ const ViewAppointment = () => { { i18nKey: 'scheduling.appointments.label', location: '/appointments' }, { text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` }, ] - useAddBreadcrumbs(breadcrumbs) + useAddBreadcrumbs(breadcrumbs, true) useEffect(() => { if (id) { From 03174ad959bc8ef6c6e125d09c80cd97ff05aef9 Mon Sep 17 00:00:00 2001 From: oliv37 Date: Thu, 20 Feb 2020 20:34:53 +0100 Subject: [PATCH 13/15] feat(breadcrumb): improve Breadcrumbs tests --- src/__tests__/breadcrumbs/Breadcrumbs.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx index e34a022a17..f14bc46b9f 100644 --- a/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx +++ b/src/__tests__/breadcrumbs/Breadcrumbs.test.tsx @@ -33,10 +33,11 @@ describe('Breadcrumbs', () => { return wrapper } - it('should not render the breadcrumb when there is no items in the store', () => { + it('should not render the breadcrumb when there are no items in the store', () => { const wrapper = setup([]) expect(wrapper.find(HRBreadcrumb)).toHaveLength(0) + expect(wrapper.find(HRBreadcrumbItem)).toHaveLength(0) }) it('should render breadcrumbs items', () => { From dcb46b89acb562e982a99531ca006e976f922d17 Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 23 Feb 2020 00:25:57 -0600 Subject: [PATCH 14/15] 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 15/15] 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'