From 83fc431dc2b69d258bdb8e8e9f5ee5ea50af235f Mon Sep 17 00:00:00 2001 From: WinstonPoh Date: Mon, 21 Sep 2020 13:26:17 +1200 Subject: [PATCH] chore: removes dispatch. adds useAppointment, useDeleteAppointment hooks. refactor scheduling WIP --- .eslintrc.js | 1 + couchdb/local.ini | 1 + docker-compose.yml | 2 +- package.json | 2 +- .../view/ViewAppointment.test.tsx | 51 ++++------ .../scheduling/hooks/useAppointment.test.tsx | 28 ++++++ .../appointments/view/ViewAppointment.tsx | 93 ++++++++++--------- src/scheduling/hooks/useAppointment.tsx | 12 +++ src/scheduling/hooks/useDeleteAppointment.tsx | 21 +++++ 9 files changed, 131 insertions(+), 80 deletions(-) create mode 100644 src/__tests__/scheduling/hooks/useAppointment.test.tsx create mode 100644 src/scheduling/hooks/useAppointment.tsx create mode 100644 src/scheduling/hooks/useDeleteAppointment.tsx diff --git a/.eslintrc.js b/.eslintrc.js index 2559631af4..6972d480ec 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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', diff --git a/couchdb/local.ini b/couchdb/local.ini index 9775ad68cb..d29c6b8459 100644 --- a/couchdb/local.ini +++ b/couchdb/local.ini @@ -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} diff --git a/docker-compose.yml b/docker-compose.yml index a78cff598a..850bd4fc0b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ # Dockerc compose only for developing purpose -version: "3.8" +version: "3.3" services: couchdb: diff --git a/package.json b/package.json index 2bd2bddbbc..f636574f21 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx index 3490f28e18..b1e7d58f39 100644 --- a/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx +++ b/src/__tests__/scheduling/appointments/view/ViewAppointment.test.tsx @@ -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' @@ -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) @@ -88,9 +90,6 @@ 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] @@ -98,10 +97,6 @@ describe('View Appointment', () => { }) 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] @@ -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) @@ -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', @@ -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(() => { @@ -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(() => { @@ -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, @@ -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 () => { diff --git a/src/__tests__/scheduling/hooks/useAppointment.test.tsx b/src/__tests__/scheduling/hooks/useAppointment.test.tsx new file mode 100644 index 0000000000..93f555c7a4 --- /dev/null +++ b/src/__tests__/scheduling/hooks/useAppointment.test.tsx @@ -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) + }) +}) diff --git a/src/scheduling/appointments/view/ViewAppointment.tsx b/src/scheduling/appointments/view/ViewAppointment.tsx index b40c336035..b734434cd9 100644 --- a/src/scheduling/appointments/view/ViewAppointment.tsx +++ b/src/scheduling/appointments/view/ViewAppointment.tsx @@ -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(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) @@ -35,19 +38,21 @@ 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(