Skip to content
This repository has been archived by the owner on Jan 9, 2023. It is now read-only.

feat(toolbar): basic button toolbar #1817

Merged
merged 3 commits into from
Feb 20, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 63 additions & 57 deletions src/HospitalRun.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { Toaster } from '@hospitalrun/components'
import Appointments from 'scheduling/appointments/Appointments'
import NewAppointment from 'scheduling/appointments/new/NewAppointment'
import ViewAppointment from 'scheduling/appointments/view/ViewAppointment'
import { ButtonBarProvider } from 'page-header/ButtonBarProvider'
import ButtonToolBar from 'page-header/ButtonToolBar'
import Sidebar from './components/Sidebar'
import Permissions from './model/Permissions'
import Dashboard from './dashboard/Dashboard'
Expand All @@ -19,68 +21,72 @@ import PrivateRoute from './components/PrivateRoute'
const HospitalRun = () => {
const { title } = useSelector((state: RootState) => state.title)
const { permissions } = useSelector((state: RootState) => state.user)

return (
<div>
<Navbar />
<div className="container-fluid">
<Sidebar />
<div className="row">
<main role="main" className="col-md-9 ml-sm-auto col-lg-10 px-4">
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 className="h2">{title}</h1>
</div>
<div>
<Switch>
<Route exact path="/" component={Dashboard} />
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadPatients)}
exact
path="/patients"
component={Patients}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.WritePatients)}
exact
path="/patients/new"
component={NewPatient}
/>
<PrivateRoute
isAuthenticated={
permissions.includes(Permissions.WritePatients) &&
permissions.includes(Permissions.ReadPatients)
}
exact
path="/patients/edit/:id"
component={EditPatient}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadPatients)}
path="/patients/:id"
component={ViewPatient}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadAppointments)}
exact
path="/appointments"
component={Appointments}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.WriteAppointments)}
exact
path="/appointments/new"
component={NewAppointment}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadAppointments)}
exact
path="/appointments/:id"
component={ViewAppointment}
/>
</Switch>
</div>
<Toaster autoClose={5000} hideProgressBar draggable />
</main>
</div>
<ButtonBarProvider>
<div className="row">
<main role="main" className="col-md-9 ml-sm-auto col-lg-10 px-4">
<div className="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 className="h2">{title}</h1>
<ButtonToolBar />
</div>
<div>
<Switch>
<Route exact path="/" component={Dashboard} />
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadPatients)}
exact
path="/patients"
component={Patients}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.WritePatients)}
exact
path="/patients/new"
component={NewPatient}
/>
<PrivateRoute
isAuthenticated={
permissions.includes(Permissions.WritePatients) &&
permissions.includes(Permissions.ReadPatients)
}
exact
path="/patients/edit/:id"
component={EditPatient}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadPatients)}
path="/patients/:id"
component={ViewPatient}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadAppointments)}
exact
path="/appointments"
component={Appointments}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.WriteAppointments)}
exact
path="/appointments/new"
component={NewAppointment}
/>
<PrivateRoute
isAuthenticated={permissions.includes(Permissions.ReadAppointments)}
exact
path="/appointments/:id"
component={ViewAppointment}
/>
</Switch>
</div>
<Toaster autoClose={5000} hideProgressBar draggable />
</main>
</div>
</ButtonBarProvider>
</div>
</div>
)
Expand Down
27 changes: 27 additions & 0 deletions src/__tests__/page-header/ButtonBarProvider.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '../../__mocks__/matchMediaMock'
import React from 'react'
import { renderHook } from '@testing-library/react-hooks'
import {
ButtonBarProvider,
useButtons,
useButtonToolbarSetter,
} from 'page-header/ButtonBarProvider'
import { Button } from '@hospitalrun/components'

describe('Button Bar Provider', () => {
it('should update and fetch data from the button bar provider', () => {
const expectedButtons = [<Button>test 1</Button>]
const wrapper = ({ children }: any) => <ButtonBarProvider>{children}</ButtonBarProvider>

const { result } = renderHook(
() => {
const update = useButtonToolbarSetter()
update(expectedButtons)
return useButtons()
},
{ wrapper },
)

expect(result.current).toEqual(expectedButtons)
})
})
27 changes: 27 additions & 0 deletions src/__tests__/page-header/ButtonToolBar.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '../../__mocks__/matchMediaMock'
import React from 'react'
import { Button } from '@hospitalrun/components'
import { mocked } from 'ts-jest/utils'
import { mount } from 'enzyme'
import * as ButtonBarProvider from '../../page-header/ButtonBarProvider'
import ButtonToolBar from '../../page-header/ButtonToolBar'

describe('Button Tool Bar', () => {
beforeEach(() => {
jest.resetAllMocks()
})

it('should render the buttons in the provider', () => {
const buttons: React.ReactNode[] = [
<Button key="test1">Test 1</Button>,
<Button key="test2">Test 2</Button>,
]
jest.spyOn(ButtonBarProvider, 'useButtons')
mocked(ButtonBarProvider).useButtons.mockReturnValue(buttons)

const wrapper = mount(<ButtonToolBar />)

expect(wrapper.childAt(0).getElement()).toEqual(buttons[0])
expect(wrapper.childAt(1).getElement()).toEqual(buttons[1])
})
})
16 changes: 16 additions & 0 deletions src/__tests__/patients/list/Patients.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import thunk from 'redux-thunk'
import configureStore from 'redux-mock-store'
import { mocked } from 'ts-jest/utils'
import { act } from 'react-dom/test-utils'
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
import Patients from '../../../patients/list/Patients'
import PatientRepository from '../../../clients/db/PatientRepository'
import * as patientSlice from '../../../patients/patients-slice'
Expand Down Expand Up @@ -42,6 +43,10 @@ describe('Patients', () => {
})

describe('layout', () => {
afterEach(() => {
jest.restoreAllMocks()
})

it('should render a search input with button', () => {
const wrapper = setup()
const searchInput = wrapper.find(TextInput)
Expand All @@ -66,6 +71,17 @@ describe('Patients', () => {
`${patients[0].fullName} (${patients[0].friendlyId})`,
)
})

it('should add a "New Patient" button to the button tool bar', () => {
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
const setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)

setup()

const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('patients.newPatient')
})
})

describe('search functionality', () => {
Expand Down
31 changes: 12 additions & 19 deletions src/__tests__/patients/view/ViewPatient.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import thunk from 'redux-thunk'
import GeneralInformation from 'patients/GeneralInformation'
import { createMemoryHistory } from 'history'
import RelatedPersonTab from 'patients/related-persons/RelatedPersonTab'
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
import Patient from '../../../model/Patient'
import PatientRepository from '../../../clients/db/PatientRepository'
import * as titleUtil from '../../../page-header/useTitle'
Expand Down Expand Up @@ -71,25 +72,6 @@ describe('ViewPatient', () => {
jest.restoreAllMocks()
})

it('should navigate to /patients/edit/:id when edit is clicked', async () => {
let wrapper: any
await act(async () => {
wrapper = await setup()
})

wrapper.update()

const editButton = wrapper.find(Button).at(3)
const onClick = editButton.prop('onClick') as any
expect(editButton.text().trim()).toEqual('actions.edit')

act(() => {
onClick()
})

expect(history.location.pathname).toEqual('/patients/edit/123')
})

it('should dispatch fetchPatient when component loads', async () => {
await act(async () => {
await setup()
Expand All @@ -110,6 +92,17 @@ describe('ViewPatient', () => {
)
})

it('should add a "Edit Patient" button to the button tool bar', () => {
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
const setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)

setup()

const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('actions.edit')
})

it('should render a tabs header with the correct tabs', async () => {
let wrapper: any
await act(async () => {
Expand Down
14 changes: 14 additions & 0 deletions src/__tests__/scheduling/appointments/Appointments.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { act } from '@testing-library/react'
import PatientRepository from 'clients/db/PatientRepository'
import { mocked } from 'ts-jest/utils'
import Patient from 'model/Patient'
import * as ButtonBarProvider from 'page-header/ButtonBarProvider'
import * as titleUtil from '../../../page-header/useTitle'

describe('Appointments', () => {
Expand Down Expand Up @@ -51,6 +52,19 @@ describe('Appointments', () => {
expect(titleUtil.default).toHaveBeenCalledWith('scheduling.appointments.label')
})

it('should add a "New Appointment" button to the button tool bar', async () => {
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
const setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)

await act(async () => {
await setup()
})

const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual('scheduling.appointments.new')
})

it('should render a calendar with the proper events', async () => {
let wrapper: any
await act(async () => {
Expand Down
38 changes: 38 additions & 0 deletions src/page-header/ButtonBarProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import React, { useState } from 'react'

type Props = {
children?: React.ReactNode
}

type ButtonUpdater = (buttons: React.ReactNode[]) => void

const ButtonBarStateContext = React.createContext<React.ReactNode[]>([])
const ButtonBarUpdateContext = React.createContext<ButtonUpdater>(() => {
// empty initial state
})

function ButtonBarProvider(props: Props) {
const { children } = props
const [state, setState] = useState<React.ReactNode[]>([])
return (
<ButtonBarStateContext.Provider value={state}>
<ButtonBarUpdateContext.Provider value={setState}>{children}</ButtonBarUpdateContext.Provider>
</ButtonBarStateContext.Provider>
)
}
function useButtons() {
const context = React.useContext(ButtonBarStateContext)
if (context === undefined) {
throw new Error('useButtons must be used within a Button Bar Context')
}
return context
}
function useButtonToolbarSetter() {
const context = React.useContext(ButtonBarUpdateContext)
if (context === undefined) {
throw new Error('useButtonToolBarSetter must be used within a Button Bar Context')
}
return context
}

export { ButtonBarProvider, useButtons, useButtonToolbarSetter }
9 changes: 9 additions & 0 deletions src/page-header/ButtonToolBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react'
import { useButtons } from './ButtonBarProvider'

const ButtonToolBar = () => {
const buttons = useButtons()
return <>{buttons.map((button) => button)}</>
}

export default ButtonToolBar
20 changes: 19 additions & 1 deletion src/patients/list/Patients.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useSelector, useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { useTranslation } from 'react-i18next'
import { Spinner, TextInput, Button, List, ListItem, Container, Row } from '@hospitalrun/components'
import { useButtonToolbarSetter } from 'page-header/ButtonBarProvider'
import { RootState } from '../../store'
import { fetchPatients, searchPatients } from '../patients-slice'
import useTitle from '../../page-header/useTitle'
Expand All @@ -14,11 +15,28 @@ const Patients = () => {
const dispatch = useDispatch()
const { patients, isLoading } = useSelector((state: RootState) => state.patients)

const setButtonToolBar = useButtonToolbarSetter()
setButtonToolBar([
<Button
key="newPatientButton"
outlined
color="success"
icon="patient-add"
onClick={() => history.push('/patients/new')}
>
{t('patients.newPatient')}
</Button>,
])

const [searchText, setSearchText] = useState<string>('')

useEffect(() => {
dispatch(fetchPatients())
}, [dispatch])

return () => {
setButtonToolBar([])
}
}, [dispatch, setButtonToolBar])

if (isLoading) {
return <Spinner color="blue" loading size={[10, 25]} type="ScaleLoader" />
Expand Down
Loading