From 2e9e985c877db2f095ba11fb6900fc177283d5cc Mon Sep 17 00:00:00 2001 From: Jack Meyer Date: Sun, 26 Apr 2020 17:46:59 -0500 Subject: [PATCH] feat(incidents): add incident related routing --- src/HospitalRun.tsx | 2 + src/__tests__/HospitalRun.test.tsx | 47 ++++++++ src/__tests__/components/Sidebar.test.tsx | 89 +++++++++++++++ src/__tests__/incidents/Incidents.test.tsx | 102 ++++++++++++++++++ .../incidents/list/ViewIncidents.test.tsx | 61 +++++++++++ .../incidents/report/ReportIncident.test.tsx | 69 ++++++++++++ .../incidents/view/ViewIncident.test.tsx | 66 ++++++++++++ src/components/Sidebar.tsx | 49 +++++++++ src/incidents/Incidents.tsx | 46 ++++++++ src/incidents/list/ViewIncidents.tsx | 12 +++ src/incidents/report/ReportIncident.tsx | 20 ++++ src/incidents/view/ViewIncident.tsx | 22 ++++ .../enUs/translations/incidents/index.ts | 10 ++ src/locales/enUs/translations/index.ts | 2 + src/model/Permissions.ts | 3 + src/user/user-slice.ts | 3 + 16 files changed, 603 insertions(+) create mode 100644 src/__tests__/incidents/Incidents.test.tsx create mode 100644 src/__tests__/incidents/list/ViewIncidents.test.tsx create mode 100644 src/__tests__/incidents/report/ReportIncident.test.tsx create mode 100644 src/__tests__/incidents/view/ViewIncident.test.tsx create mode 100644 src/incidents/Incidents.tsx create mode 100644 src/incidents/list/ViewIncidents.tsx create mode 100644 src/incidents/report/ReportIncident.tsx create mode 100644 src/incidents/view/ViewIncident.tsx create mode 100644 src/locales/enUs/translations/incidents/index.ts diff --git a/src/HospitalRun.tsx b/src/HospitalRun.tsx index 199e4be740..320195d4f2 100644 --- a/src/HospitalRun.tsx +++ b/src/HospitalRun.tsx @@ -17,6 +17,7 @@ import { RootState } from './store' import Navbar from './components/Navbar' import PrivateRoute from './components/PrivateRoute' import Patients from './patients/Patients' +import Incidents from './incidents/Incidents' const HospitalRun = () => { const { title } = useSelector((state: RootState) => state.title) @@ -74,6 +75,7 @@ const HospitalRun = () => { /> + diff --git a/src/__tests__/HospitalRun.test.tsx b/src/__tests__/HospitalRun.test.tsx index 737a42f61c..edffdd7dd5 100644 --- a/src/__tests__/HospitalRun.test.tsx +++ b/src/__tests__/HospitalRun.test.tsx @@ -20,6 +20,7 @@ import Patient from '../model/Patient' import Appointment from '../model/Appointment' import HospitalRun from '../HospitalRun' import Permissions from '../model/Permissions' +import Incidents from '../incidents/Incidents' const mockStore = configureMockStore([thunk]) @@ -256,6 +257,52 @@ describe('HospitalRun', () => { expect(wrapper.find(Dashboard)).toHaveLength(1) }) }) + + describe('/incidents', () => { + it('should render the Incidents component when /incidents is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ViewIncidents] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + , + ) + }) + wrapper.update() + + expect(wrapper.find(Incidents)).toHaveLength(1) + }) + + it('should render the dashboard if the user does not have permissions to view incidents', () => { + jest.spyOn(LabRepository, 'findAll').mockResolvedValue([]) + const store = mockStore({ + title: 'test', + user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(Incidents)).toHaveLength(0) + expect(wrapper.find(Dashboard)).toHaveLength(1) + }) + }) }) describe('layout', () => { diff --git a/src/__tests__/components/Sidebar.test.tsx b/src/__tests__/components/Sidebar.test.tsx index 11bf2804af..5aaf695060 100644 --- a/src/__tests__/components/Sidebar.test.tsx +++ b/src/__tests__/components/Sidebar.test.tsx @@ -326,4 +326,93 @@ describe('Sidebar', () => { expect(history.location.pathname).toEqual('/labs') }) }) + + describe('incident links', () => { + it('should render the main incidents link', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(5).text().trim()).toEqual('incidents.label') + }) + + it('should render the new incident report link', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).text().trim()).toEqual('incidents.reports.new') + }) + + it('should render the incidents list link', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).text().trim()).toEqual('incidents.reports.label') + }) + + it('main incidents link should be active when the current path is /incidents', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(5).prop('active')).toBeTruthy() + }) + + it('should navigate to /incidents when the main incident link is clicked', () => { + const wrapper = setup('/') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(5).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/incidents') + }) + + it('new incident report link should be active when the current path is /incidents/new', () => { + const wrapper = setup('/incidents/new') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(6).prop('active')).toBeTruthy() + }) + + it('should navigate to /incidents/new when the new labs link is clicked', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(6).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/incidents/new') + }) + + it('incidents list link should be active when the current path is /incidents', () => { + const wrapper = setup('/incidents') + + const listItems = wrapper.find(ListItem) + + expect(listItems.at(7).prop('active')).toBeTruthy() + }) + + it('should navigate to /labs when the labs list link is clicked', () => { + const wrapper = setup('/incidents/new') + + const listItems = wrapper.find(ListItem) + + act(() => { + const onClick = listItems.at(7).prop('onClick') as any + onClick() + }) + + expect(history.location.pathname).toEqual('/incidents') + }) + }) }) diff --git a/src/__tests__/incidents/Incidents.test.tsx b/src/__tests__/incidents/Incidents.test.tsx new file mode 100644 index 0000000000..2646be6a36 --- /dev/null +++ b/src/__tests__/incidents/Incidents.test.tsx @@ -0,0 +1,102 @@ +import '../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { MemoryRouter } from 'react-router' +import { Provider } from 'react-redux' +import thunk from 'redux-thunk' +import configureMockStore from 'redux-mock-store' +import { act } from '@testing-library/react' +import Permissions from 'model/Permissions' +import ViewIncident from '../../incidents/view/ViewIncident' +import Incidents from '../../incidents/Incidents' +import ReportIncident from '../../incidents/report/ReportIncident' + +const mockStore = configureMockStore([thunk]) + +describe('Incidents', () => { + describe('routing', () => { + describe('/incidents/new', () => { + it('should render the new lab request screen when /incidents/new is accessed', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ReportIncident] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(ReportIncident)).toHaveLength(1) + }) + + it('should not navigate to /incidents/new if the user does not have RequestLab permissions', () => { + const store = mockStore({ + title: 'test', + user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + const wrapper = mount( + + + + + , + ) + + expect(wrapper.find(ReportIncident)).toHaveLength(0) + }) + }) + + describe('/incidents/:id', () => { + it('should render the view lab screen when /incidents/:id is accessed', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [Permissions.ViewIncident] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + let wrapper: any + + await act(async () => { + wrapper = await mount( + + + + + , + ) + + expect(wrapper.find(ViewIncident)).toHaveLength(1) + }) + }) + + it('should not navigate to /incidents/:id if the user does not have ViewIncident permissions', async () => { + const store = mockStore({ + title: 'test', + user: { permissions: [] }, + breadcrumbs: { breadcrumbs: [] }, + components: { sidebarCollapsed: false }, + }) + + const wrapper = await mount( + + + + + , + ) + + expect(wrapper.find(ViewIncident)).toHaveLength(0) + }) + }) + }) +}) diff --git a/src/__tests__/incidents/list/ViewIncidents.test.tsx b/src/__tests__/incidents/list/ViewIncidents.test.tsx new file mode 100644 index 0000000000..6c408f6572 --- /dev/null +++ b/src/__tests__/incidents/list/ViewIncidents.test.tsx @@ -0,0 +1,61 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { act } from '@testing-library/react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import Permissions from '../../../model/Permissions' +import * as titleUtil from '../../../page-header/useTitle' +import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' +import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' +import ViewIncidents from '../../../incidents/list/ViewIncidents' + +const mockStore = createMockStore([thunk]) + +describe('View Incidents', () => { + let history: any + + let setButtonToolBarSpy: any + const setup = async (permissions: Permissions[]) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/incidents`) + const store = mockStore({ + title: '', + user: { + permissions, + }, + }) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , + ) + }) + wrapper.update() + return wrapper + } + + it('should set the title', async () => { + await setup([Permissions.ViewIncidents]) + + expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.label') + }) +}) diff --git a/src/__tests__/incidents/report/ReportIncident.test.tsx b/src/__tests__/incidents/report/ReportIncident.test.tsx new file mode 100644 index 0000000000..5cad7b542c --- /dev/null +++ b/src/__tests__/incidents/report/ReportIncident.test.tsx @@ -0,0 +1,69 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { act } from '@testing-library/react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import Permissions from '../../../model/Permissions' +import * as titleUtil from '../../../page-header/useTitle' +import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' +import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' +import ReportIncident from '../../../incidents/report/ReportIncident' + +const mockStore = createMockStore([thunk]) + +describe('Report Incident', () => { + let history: any + + let setButtonToolBarSpy: any + const setup = async (permissions: Permissions[]) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + setButtonToolBarSpy = jest.fn() + jest.spyOn(titleUtil, 'default') + jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy) + + history = createMemoryHistory() + history.push(`/incidents/new`) + const store = mockStore({ + title: '', + user: { + permissions, + }, + }) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , + ) + }) + wrapper.update() + return wrapper + } + + it('should set the title', async () => { + await setup([Permissions.ReportIncident]) + + expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.new') + }) + + it('should set the breadcrumbs properly', async () => { + await setup([Permissions.ReportIncident]) + + expect(breadcrumbUtil.default).toHaveBeenCalledWith([ + { i18nKey: 'incidents.reports.new', location: '/incidents/new' }, + ]) + }) +}) diff --git a/src/__tests__/incidents/view/ViewIncident.test.tsx b/src/__tests__/incidents/view/ViewIncident.test.tsx new file mode 100644 index 0000000000..78daea2f77 --- /dev/null +++ b/src/__tests__/incidents/view/ViewIncident.test.tsx @@ -0,0 +1,66 @@ +import '../../../__mocks__/matchMediaMock' +import React from 'react' +import { mount } from 'enzyme' +import { createMemoryHistory } from 'history' +import { act } from '@testing-library/react' +import { Provider } from 'react-redux' +import { Route, Router } from 'react-router' +import createMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' +import Permissions from '../../../model/Permissions' +import * as titleUtil from '../../../page-header/useTitle' +import * as ButtonBarProvider from '../../../page-header/ButtonBarProvider' +import * as breadcrumbUtil from '../../../breadcrumbs/useAddBreadcrumbs' +import ViewIncident from '../../../incidents/view/ViewIncident' + +const mockStore = createMockStore([thunk]) + +describe('View Incident', () => { + let history: any + + const setup = async (permissions: Permissions[]) => { + jest.resetAllMocks() + jest.spyOn(breadcrumbUtil, 'default') + jest.spyOn(titleUtil, 'default') + + history = createMemoryHistory() + history.push(`/incidents/1234`) + const store = mockStore({ + title: '', + user: { + permissions, + }, + }) + + let wrapper: any + await act(async () => { + wrapper = await mount( + + + + + + + + + , + ) + }) + wrapper.update() + return wrapper + } + + it('should set the title', async () => { + await setup([Permissions.ViewIncident]) + + expect(titleUtil.default).toHaveBeenCalledWith('incidents.reports.view') + }) + + it('should set the breadcrumbs properly', async () => { + await setup([Permissions.ViewIncident]) + + expect(breadcrumbUtil.default).toHaveBeenCalledWith([ + { i18nKey: 'incidents.reports.view', location: '/incidents/1234' }, + ]) + }) +}) diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index e70dfcd5e3..8468043fc2 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -39,6 +39,8 @@ const Sidebar = () => { ? 'appointment' : splittedPath[1].includes('labs') ? 'labs' + : splittedPath[1].includes('incidents') + ? 'incidents' : 'none', ) @@ -230,6 +232,52 @@ const Sidebar = () => { ) + const getIncidentLinks = () => ( + <> + { + navigateTo('/incidents') + setExpansion('incidents') + }} + className="nav-item" + style={listItemStyle} + > + + {!sidebarCollapsed && t('incidents.label')} + + {splittedPath[1].includes('incidents') && expandedItem === 'incidents' && ( + + navigateTo('/incidents/new')} + active={splittedPath[1].includes('incidents') && splittedPath.length > 2} + > + + {!sidebarCollapsed && t('incidents.reports.new')} + + navigateTo('/incidents')} + active={splittedPath[1].includes('incidents') && splittedPath.length < 3} + > + + {!sidebarCollapsed && t('incidents.reports.label')} + + + )} + + ) + return ( diff --git a/src/incidents/Incidents.tsx b/src/incidents/Incidents.tsx new file mode 100644 index 0000000000..fef38887e8 --- /dev/null +++ b/src/incidents/Incidents.tsx @@ -0,0 +1,46 @@ +import React from 'react' +import { Switch } from 'react-router' +import { useSelector } from 'react-redux' +import PrivateRoute from '../components/PrivateRoute' +import { RootState } from '../store' +import Permissions from '../model/Permissions' +import ViewIncidents from './list/ViewIncidents' +import ReportIncident from './report/ReportIncident' +import ViewIncident from './view/ViewIncident' +import useAddBreadcrumbs from '../breadcrumbs/useAddBreadcrumbs' + +const Incidents = () => { + const { permissions } = useSelector((state: RootState) => state.user) + const breadcrumbs = [ + { + i18nKey: 'incidents.label', + location: `/incidents`, + }, + ] + useAddBreadcrumbs(breadcrumbs, true) + + return ( + + + + + + ) +} + +export default Incidents diff --git a/src/incidents/list/ViewIncidents.tsx b/src/incidents/list/ViewIncidents.tsx new file mode 100644 index 0000000000..2acf9090a1 --- /dev/null +++ b/src/incidents/list/ViewIncidents.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import useTitle from '../../page-header/useTitle' + +const ViewIncidents = () => { + const { t } = useTranslation() + useTitle(t('incidents.reports.label')) + + return

Reported Incidents

+} + +export default ViewIncidents diff --git a/src/incidents/report/ReportIncident.tsx b/src/incidents/report/ReportIncident.tsx new file mode 100644 index 0000000000..70f22c861a --- /dev/null +++ b/src/incidents/report/ReportIncident.tsx @@ -0,0 +1,20 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import useTitle from '../../page-header/useTitle' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' + +const ReportIncident = () => { + const { t } = useTranslation() + useTitle(t('incidents.reports.new')) + const breadcrumbs = [ + { + i18nKey: 'incidents.reports.new', + location: `/incidents/new`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + return

Report Incident

+} + +export default ReportIncident diff --git a/src/incidents/view/ViewIncident.tsx b/src/incidents/view/ViewIncident.tsx new file mode 100644 index 0000000000..17b4c87a68 --- /dev/null +++ b/src/incidents/view/ViewIncident.tsx @@ -0,0 +1,22 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useParams } from 'react-router' +import useAddBreadcrumbs from '../../breadcrumbs/useAddBreadcrumbs' +import useTitle from '../../page-header/useTitle' + +const ViewIncident = () => { + const { t } = useTranslation() + const { id } = useParams() + useTitle(t('incidents.reports.view')) + const breadcrumbs = [ + { + i18nKey: 'incidents.reports.view', + location: `/incidents/${id}`, + }, + ] + useAddBreadcrumbs(breadcrumbs) + + return

View Incident

+} + +export default ViewIncident diff --git a/src/locales/enUs/translations/incidents/index.ts b/src/locales/enUs/translations/incidents/index.ts new file mode 100644 index 0000000000..97cf559d13 --- /dev/null +++ b/src/locales/enUs/translations/incidents/index.ts @@ -0,0 +1,10 @@ +export default { + incidents: { + label: 'Incidents', + reports: { + label: 'Reported Incidents', + new: 'Report Incident', + view: 'View Incident', + }, + }, +} diff --git a/src/locales/enUs/translations/index.ts b/src/locales/enUs/translations/index.ts index d6bf2a1db2..674001f887 100644 --- a/src/locales/enUs/translations/index.ts +++ b/src/locales/enUs/translations/index.ts @@ -6,6 +6,7 @@ import scheduling from './scheduling' import states from './states' import sex from './sex' import labs from './labs' +import incidents from './incidents' export default { ...actions, @@ -16,4 +17,5 @@ export default { ...states, ...sex, ...labs, + ...incidents, } diff --git a/src/model/Permissions.ts b/src/model/Permissions.ts index 170fb173ba..91a407cd2d 100644 --- a/src/model/Permissions.ts +++ b/src/model/Permissions.ts @@ -11,6 +11,9 @@ enum Permissions { CompleteLab = 'complete:lab', ViewLab = 'read:lab', ViewLabs = 'read:labs', + ViewIncidents = 'read:incidents', + ViewIncident = 'read:incident', + ReportIncident = 'write:incident', } export default Permissions diff --git a/src/user/user-slice.ts b/src/user/user-slice.ts index 98c163429c..22af27f53e 100644 --- a/src/user/user-slice.ts +++ b/src/user/user-slice.ts @@ -19,6 +19,9 @@ const initialState: UserState = { Permissions.RequestLab, Permissions.CompleteLab, Permissions.CancelLab, + Permissions.ViewIncident, + Permissions.ViewIncidents, + Permissions.ReportIncident, ], }