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

Commit

Permalink
chore: removes dispatch. adds useAppointment, useDeleteAppointment ho…
Browse files Browse the repository at this point in the history
…oks. refactor scheduling WIP
  • Loading branch information
WinstonPoh committed Sep 21, 2020
1 parent 812fe6a commit 83fc431
Show file tree
Hide file tree
Showing 9 changed files with 131 additions and 80 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ module.exports = {
'eslint:recommended',
'plugin:@typescript-eslint/eslint-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:react-hooks/recommended',
'prettier',
'prettier/@typescript-eslint',
'plugin:prettier/recommended',
Expand Down
1 change: 1 addition & 0 deletions couchdb/local.ini
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ credentials = true

[chttpd]
bind_address = 0.0.0.0
authentication_handlers = {chttpd_auth, cookie_authentication_handler}, {couch_httpd_auth, proxy_authentication_handler}, {chttpd_auth, default_authentication_handler}
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Dockerc compose only for developing purpose

version: "3.8"
version: "3.3"

services:
couchdb:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"eslint-plugin-jsx-a11y": "~6.3.0",
"eslint-plugin-prettier": "~3.1.2",
"eslint-plugin-react": "~7.20.0",
"eslint-plugin-react-hooks": "~4.1.0",
"eslint-plugin-react-hooks": "~4.1.2",
"history": "4.10.1",
"husky": "~4.3.0",
"jest": "24.9.0",
Expand Down
51 changes: 17 additions & 34 deletions src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import { mocked } from 'ts-jest/utils'

import * as ButtonBarProvider from '../../../../page-header/button-toolbar/ButtonBarProvider'
import * as titleUtil from '../../../../page-header/title/useTitle'
import * as appointmentSlice from '../../../../scheduling/appointments/appointment-slice'
import AppointmentDetailForm from '../../../../scheduling/appointments/AppointmentDetailForm'
import ViewAppointment from '../../../../scheduling/appointments/view/ViewAppointment'
import AppointmentRepository from '../../../../shared/db/AppointmentRepository'
Expand Down Expand Up @@ -39,8 +38,11 @@ const patient = {
describe('View Appointment', () => {
let history: any
let store: MockStore
let setButtonToolBarSpy: any

const setup = async (status = 'completed', permissions = [Permissions.ReadAppointments]) => {
setButtonToolBarSpy = jest.fn()
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy)
jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(appointment)
jest.spyOn(AppointmentRepository, 'delete').mockResolvedValue(appointment)
jest.spyOn(PatientRepository, 'find').mockResolvedValue(patient)
Expand Down Expand Up @@ -88,20 +90,13 @@ describe('View Appointment', () => {
})

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

await setup('loading', [Permissions.WriteAppointments, Permissions.ReadAppointments])

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

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

await setup('loading', [Permissions.DeleteAppointment, Permissions.ReadAppointments])

const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
Expand All @@ -111,32 +106,23 @@ describe('View Appointment', () => {
})

it('button toolbar empty if has only ReadAppointments permission', async () => {
const setButtonToolBarSpy = jest.fn()
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter').mockReturnValue(setButtonToolBarSpy)

await setup('loading')

const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect(actualButtons.length).toEqual(0)
})

it('should dispatch getAppointment if id is present', async () => {
it('should call getAppointment by id if id is present', async () => {
await setup()

expect(AppointmentRepository.find).toHaveBeenCalledWith(appointment.id)
expect(store.getActions()).toContainEqual(appointmentSlice.fetchAppointmentStart())
expect(store.getActions()).toContainEqual(
appointmentSlice.fetchAppointmentSuccess({ appointment, patient }),
)
})

it('should render a loading spinner', async () => {
const { wrapper } = await setup('loading')
// it('should render a loading spinner', async () => {
// const { wrapper } = await setup('loading')
// expect(wrapper.find(components.Spinner)).toHaveLength(1)
// })

expect(wrapper.find(components.Spinner)).toHaveLength(1)
})

it('should render a AppointmentDetailForm with the correct data', async () => {
it('should render an AppointmentDetailForm with the correct data', async () => {
const { wrapper } = await setup()

const appointmentDetailForm = wrapper.find(AppointmentDetailForm)
Expand All @@ -159,20 +145,20 @@ describe('View Appointment', () => {
})

describe('delete appointment', () => {
let setButtonToolBarSpy = jest.fn()
// let setButtonToolBarSpy = jest.fn()
let deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
beforeEach(() => {
jest.resetAllMocks()
jest.restoreAllMocks()
jest.spyOn(ButtonBarProvider, 'useButtonToolbarSetter')
deleteAppointmentSpy = jest.spyOn(AppointmentRepository, 'delete')
setButtonToolBarSpy = jest.fn()
mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
// setButtonToolBarSpy = jest.fn()
// mocked(ButtonBarProvider).useButtonToolbarSetter.mockReturnValue(setButtonToolBarSpy)
})

it('should render a delete appointment button in the button toolbar', async () => {
await setup('completed', [Permissions.ReadAppointments, Permissions.DeleteAppointment])

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
expect(setButtonToolBarSpy).toHaveBeenCalled()
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]
expect((actualButtons[0] as any).props.children).toEqual(
'scheduling.appointments.deleteAppointment',
Expand All @@ -185,7 +171,7 @@ describe('View Appointment', () => {
Permissions.DeleteAppointment,
])

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
expect(setButtonToolBarSpy).toHaveBeenCalled()
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]

act(() => {
Expand All @@ -204,7 +190,7 @@ describe('View Appointment', () => {
Permissions.DeleteAppointment,
])

expect(setButtonToolBarSpy).toHaveBeenCalledTimes(1)
expect(setButtonToolBarSpy).toHaveBeenCalled()
const actualButtons: React.ReactNode[] = setButtonToolBarSpy.mock.calls[0][0]

act(() => {
Expand All @@ -223,7 +209,7 @@ describe('View Appointment', () => {
expect(deleteConfirmationModal.prop('show')).toEqual(false)
})

it('should dispatch DELETE_APPOINTMENT action when modal confirmation button is clicked', async () => {
it('should delete from appointment repository when modal confirmation button is clicked', async () => {
const { wrapper } = await setup('completed', [
Permissions.ReadAppointments,
Permissions.DeleteAppointment,
Expand All @@ -239,9 +225,6 @@ describe('View Appointment', () => {

expect(deleteAppointmentSpy).toHaveBeenCalledTimes(1)
expect(deleteAppointmentSpy).toHaveBeenCalledWith(appointment)

expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentStart())
expect(store.getActions()).toContainEqual(appointmentSlice.deleteAppointmentSuccess())
})

it('should navigate to /appointments and display a message when delete is successful', async () => {
Expand Down
28 changes: 28 additions & 0 deletions src/__tests__/scheduling/hooks/useAppointment.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { renderHook, act } from '@testing-library/react-hooks'

import useAppointment from '../../../scheduling/hooks/useAppointment'
import AppointmentRepository from '../../../shared/db/AppointmentRepository'
import Appointment from '../../../shared/model/Appointment'
import waitUntilQueryIsSuccessful from '../../test-utils/wait-for-query.util'

describe('useAppointment', () => {
it('should get an appointment by id', async () => {
const expectedAppointmentId = 'some id'
const expectedAppointment = {
id: expectedAppointmentId,
} as Appointment
jest.spyOn(AppointmentRepository, 'find').mockResolvedValue(expectedAppointment)

let actualData: any
await act(async () => {
const renderHookResult = renderHook(() => useAppointment(expectedAppointmentId))
const { result } = renderHookResult
await waitUntilQueryIsSuccessful(renderHookResult)
actualData = result.current.data
})

expect(AppointmentRepository.find).toHaveBeenCalledTimes(1)
expect(AppointmentRepository.find).toBeCalledWith(expectedAppointmentId)
expect(actualData).toEqual(expectedAppointment)
})
})
93 changes: 49 additions & 44 deletions src/scheduling/appointments/view/ViewAppointment.tsx
Original file line number Diff line number Diff line change
@@ -1,32 +1,35 @@
import { Spinner, Button, Modal, Toast } from '@hospitalrun/components'
import React, { useEffect, useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import { useParams, useHistory } from 'react-router-dom'
import React, { useCallback, useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { useHistory, useParams } from 'react-router-dom'

import useAddBreadcrumbs from '../../../page-header/breadcrumbs/useAddBreadcrumbs'
import { useButtonToolbarSetter } from '../../../page-header/button-toolbar/ButtonBarProvider'
import useTitle from '../../../page-header/title/useTitle'
import usePatient from '../../../patients/hooks/usePatient'
import useTranslator from '../../../shared/hooks/useTranslator'
import Permissions from '../../../shared/model/Permissions'
import { RootState } from '../../../shared/store'
import { fetchAppointment, deleteAppointment } from '../appointment-slice'
import useAppointment from '../../hooks/useAppointment'
import useDeleteAppointment from '../../hooks/useDeleteAppointment'
import AppointmentDetailForm from '../AppointmentDetailForm'
import { getAppointmentLabel } from '../util/scheduling-appointment.util'

const ViewAppointment = () => {
const { t } = useTranslator()
useTitle(t('scheduling.appointments.viewAppointment'))
const dispatch = useDispatch()
const { id } = useParams()
useTitle(t('scheduling.appointments.viewAppointment'))
const history = useHistory()
const { appointment, patient, status } = useSelector((state: RootState) => state.appointment)
const { permissions } = useSelector((state: RootState) => state.user)
const [deleteMutate] = useDeleteAppointment()
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState<boolean>(false)
const setButtonToolBar = useButtonToolbarSetter()
const { permissions } = useSelector((state: RootState) => state.user)

const { data } = useAppointment(id)
const { data: patient } = usePatient(data ? data.patient : id)
const breadcrumbs = [
{ i18nKey: 'scheduling.appointments.label', location: '/appointments' },
{ text: getAppointmentLabel(appointment), location: `/patients/${appointment.id}` },
{ text: data ? getAppointmentLabel(data) : '', location: `/patients/${id}` },
]
useAddBreadcrumbs(breadcrumbs, true)

Expand All @@ -35,27 +38,29 @@ const ViewAppointment = () => {
setShowDeleteConfirmation(true)
}

const onDeleteSuccess = () => {
history.push('/appointments')
Toast('success', t('states.success'), t('scheduling.appointment.successfullyDeleted'))
}

const onDeleteConfirmationButtonClick = () => {
dispatch(deleteAppointment(appointment, onDeleteSuccess))
if (!data) {
return
}

deleteMutate({ appointmentId: data.id }).then(() => {
history.push('/appointments')
Toast('success', t('states.success'), t('scheduling.appointment.successfullyDeleted'))
})
setShowDeleteConfirmation(false)
}

useEffect(() => {
const buttons = []
if (permissions.includes(Permissions.WriteAppointments)) {
const getButtons = useCallback(() => {
const buttons: React.ReactNode[] = []
if (data && permissions.includes(Permissions.WriteAppointments)) {
buttons.push(
<Button
key="editAppointmentButton"
color="success"
icon="edit"
outlined
onClick={() => {
history.push(`/appointments/edit/${appointment.id}`)
history.push(`/appointments/edit/${data.id}`)
}}
>
{t('actions.edit')}
Expand All @@ -76,39 +81,39 @@ const ViewAppointment = () => {
)
}

setButtonToolBar(buttons)
}, [appointment.id, history, permissions, setButtonToolBar, t])
return buttons
}, [data, permissions, t, history])

useEffect(() => {
if (id) {
dispatch(fetchAppointment(id))
}
setButtonToolBar(getButtons())

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

if (status === 'loading') {
return <Spinner type="BarLoader" loading />
}
}, [setButtonToolBar, getButtons])

return (
<div>
<AppointmentDetailForm appointment={appointment} isEditable={false} patient={patient} />
<Modal
body={t('scheduling.appointment.deleteConfirmationMessage')}
buttonsAlignment="right"
show={showDeleteConfirmation}
closeButton={{
children: t('actions.delete'),
color: 'danger',
onClick: onDeleteConfirmationButtonClick,
}}
title={t('actions.confirmDelete')}
toggle={() => setShowDeleteConfirmation(false)}
/>
</div>
<>
{patient && data ? (
<div>
<AppointmentDetailForm appointment={data} isEditable={false} patient={patient} />
<Modal
body={t('scheduling.appointment.deleteConfirmationMessage')}
buttonsAlignment="right"
show={showDeleteConfirmation}
closeButton={{
children: t('actions.delete'),
color: 'danger',
onClick: onDeleteConfirmationButtonClick,
}}
title={t('actions.confirmDelete')}
toggle={() => setShowDeleteConfirmation(false)}
/>
</div>
) : (
<Spinner type="BarLoader" loading />
)}
</>
)
}

Expand Down
12 changes: 12 additions & 0 deletions src/scheduling/hooks/useAppointment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { QueryKey, useQuery } from 'react-query'

import AppointmentRepository from '../../shared/db/AppointmentRepository'
import Appointment from '../../shared/model/Appointment'

function getAppointmentById(_: QueryKey<string>, appointmentId: string): Promise<Appointment> {
return AppointmentRepository.find(appointmentId)
}

export default function useAppointment(appointmentId: string) {
return useQuery(['appointment', appointmentId], getAppointmentById)
}
21 changes: 21 additions & 0 deletions src/scheduling/hooks/useDeleteAppointment.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { queryCache, useMutation } from 'react-query'

import AppointmentRepository from '../../shared/db/AppointmentRepository'
import Appointment from '../../shared/model/Appointment'

interface deleteAppointmentRequest {
appointmentId: string
}

async function deleteAppointment(request: deleteAppointmentRequest): Promise<Appointment> {
const appointment = await AppointmentRepository.find(request.appointmentId)
return AppointmentRepository.delete(appointment)
}

export default function useDeleteAppointment() {
return useMutation(deleteAppointment, {
onSuccess: async () => {
queryCache.invalidateQueries('appointment')
},
})
}

0 comments on commit 83fc431

Please # to comment.