From 992e58df8ebdaac2e2df2b0e50423608513a5176 Mon Sep 17 00:00:00 2001 From: Martim Date: Sun, 12 Apr 2020 16:49:56 -0300 Subject: [PATCH] fix(patient): Input validation and error feedback (#1977) --- .../patients/edit/EditPatient.test.tsx | 66 +++++++++++++++++++ .../patients/new/NewPatient.test.tsx | 41 +++++++++++- .../input/DatePickerWithLabelFormGroup.tsx | 6 +- .../enUs/translations/patient/index.ts | 6 +- .../ptBr/translations/patient/index.ts | 8 +-- src/patients/GeneralInformation.tsx | 29 ++++---- src/patients/edit/EditPatient.tsx | 53 ++++++++++++--- src/patients/new/NewPatient.tsx | 54 ++++++++++++--- 8 files changed, 224 insertions(+), 39 deletions(-) diff --git a/src/__tests__/patients/edit/EditPatient.test.tsx b/src/__tests__/patients/edit/EditPatient.test.tsx index 7a632f89a3..717a724428 100644 --- a/src/__tests__/patients/edit/EditPatient.test.tsx +++ b/src/__tests__/patients/edit/EditPatient.test.tsx @@ -9,6 +9,7 @@ import { act } from 'react-dom/test-utils' import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import { Button } from '@hospitalrun/components' +import { addDays, endOfToday } from 'date-fns' import EditPatient from '../../../patients/edit/EditPatient' import GeneralInformation from '../../../patients/GeneralInformation' import Patient from '../../../model/Patient' @@ -119,6 +120,71 @@ describe('Edit Patient', () => { expect(store.getActions()).toContainEqual(patientSlice.updatePatientSuccess(patient)) }) + it('should pass no given name error when form doesnt contain a given name on save button click', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + const givenName = wrapper.findWhere((w: any) => w.prop('name') === 'givenName') + expect(givenName.prop('value')).toBe('') + + const generalInformationForm = wrapper.find(GeneralInformation) + expect(generalInformationForm.prop('errorMessage')).toBe('') + + const saveButton = wrapper.find(Button).at(0) + const onClick = saveButton.prop('onClick') as any + expect(saveButton.text().trim()).toEqual('actions.save') + + act(() => { + onClick() + }) + + wrapper.update() + expect(wrapper.find(GeneralInformation).prop('errorMessage')).toMatch( + 'patient.errors.updatePatientError', + ) + expect(wrapper.find(GeneralInformation).prop('feedbackFields').givenName).toMatch( + 'patient.errors.patientGivenNameFeedback', + ) + expect(wrapper.update.isInvalid === true) + }) + + it('should pass invalid date of birth error when input date is grater than today on save button click', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + const generalInformationForm = wrapper.find(GeneralInformation) + + act(() => { + generalInformationForm.prop('onFieldChange')( + 'dateOfBirth', + addDays(endOfToday(), 10).toISOString(), + ) + }) + + wrapper.update() + + const saveButton = wrapper.find(Button).at(0) + const onClick = saveButton.prop('onClick') as any + expect(saveButton.text().trim()).toEqual('actions.save') + + await act(async () => { + await onClick() + }) + + wrapper.update() + expect(wrapper.find(GeneralInformation).prop('errorMessage')).toMatch( + 'patient.errors.updatePatientError', + ) + expect(wrapper.find(GeneralInformation).prop('feedbackFields').dateOfBirth).toMatch( + 'patient.errors.patientDateOfBirthFeedback', + ) + expect(wrapper.update.isInvalid === true) + }) + it('should navigate to /patients/:id when cancel is clicked', async () => { let wrapper: any await act(async () => { diff --git a/src/__tests__/patients/new/NewPatient.test.tsx b/src/__tests__/patients/new/NewPatient.test.tsx index 5010329724..cd9a85f262 100644 --- a/src/__tests__/patients/new/NewPatient.test.tsx +++ b/src/__tests__/patients/new/NewPatient.test.tsx @@ -10,6 +10,7 @@ import configureMockStore, { MockStore } from 'redux-mock-store' import thunk from 'redux-thunk' import * as components from '@hospitalrun/components' +import { addDays, endOfToday } from 'date-fns' import NewPatient from '../../../patients/new/NewPatient' import GeneralInformation from '../../../patients/GeneralInformation' import Patient from '../../../model/Patient' @@ -95,7 +96,45 @@ describe('New Patient', () => { wrapper.update() expect(wrapper.find(GeneralInformation).prop('errorMessage')).toMatch( - 'patient.errors.patientGivenNameRequiredOnCreate', + 'patient.errors.createPatientError', + ) + expect(wrapper.find(GeneralInformation).prop('feedbackFields').givenName).toMatch( + 'patient.errors.patientGivenNameFeedback', + ) + expect(wrapper.update.isInvalid === true) + }) + + it('should pass invalid date of birth error when input date is grater than today on save button click', async () => { + let wrapper: any + await act(async () => { + wrapper = await setup() + }) + + const generalInformationForm = wrapper.find(GeneralInformation) + + act(() => { + generalInformationForm.prop('onFieldChange')( + 'dateOfBirth', + addDays(endOfToday(), 10).toISOString(), + ) + }) + + wrapper.update() + + const saveButton = wrapper.find(components.Button).at(0) + const onClick = saveButton.prop('onClick') as any + expect(saveButton.text().trim()).toEqual('actions.save') + + await act(async () => { + await onClick() + }) + + wrapper.update() + expect(wrapper.find(GeneralInformation).prop('errorMessage')).toMatch( + 'patient.errors.createPatientError', + ) + expect(wrapper.find(GeneralInformation).prop('feedbackFields').dateOfBirth).toMatch( + 'patient.errors.patientDateOfBirthFeedback', ) expect(wrapper.update.isInvalid === true) }) diff --git a/src/components/input/DatePickerWithLabelFormGroup.tsx b/src/components/input/DatePickerWithLabelFormGroup.tsx index b6fc61f4b6..e1f137b0e8 100644 --- a/src/components/input/DatePickerWithLabelFormGroup.tsx +++ b/src/components/input/DatePickerWithLabelFormGroup.tsx @@ -8,10 +8,12 @@ interface Props { isEditable?: boolean onChange?: (date: Date) => void isRequired?: boolean + feedback?: string + isInvalid?: boolean } const DatePickerWithLabelFormGroup = (props: Props) => { - const { onChange, label, name, isEditable, value, isRequired } = props + const { onChange, label, name, isEditable, value, isRequired, feedback, isInvalid } = props const id = `${name}DatePicker` return (
@@ -24,6 +26,8 @@ const DatePickerWithLabelFormGroup = (props: Props) => { timeIntervals={30} withPortal={false} disabled={!isEditable} + feedback={feedback} + isInvalid={isInvalid} onChange={(inputDate) => { if (onChange) { onChange(inputDate) diff --git a/src/locales/enUs/translations/patient/index.ts b/src/locales/enUs/translations/patient/index.ts index 95c36e0973..5332c7073b 100644 --- a/src/locales/enUs/translations/patient/index.ts +++ b/src/locales/enUs/translations/patient/index.ts @@ -1,6 +1,5 @@ export default { patient: { - code: 'Code', firstName: 'First Name', lastName: 'Last Name', suffix: 'Suffix', @@ -85,9 +84,10 @@ export default { private: 'Private', }, errors: { - patientGivenNameRequiredOnCreate: 'Could not create new patient.', - patientGivenNameRequiredOnUpdate: 'Could not update patient.', + createPatientError: 'Could not create new patient.', + updatePatientError: 'Could not update patient.', patientGivenNameFeedback: 'Given Name is required.', + patientDateOfBirthFeedback: 'Date of Birth can not be greater than today', }, }, } diff --git a/src/locales/ptBr/translations/patient/index.ts b/src/locales/ptBr/translations/patient/index.ts index 71b1bbcb40..571a1ff816 100644 --- a/src/locales/ptBr/translations/patient/index.ts +++ b/src/locales/ptBr/translations/patient/index.ts @@ -64,10 +64,10 @@ export default { private: 'Particular', }, errors: { - patientGivenNameRequiredOnCreate: 'Nome do Paciente é necessário.', - // todo Portuguese translation - patientGivenNameRequiredOnUpdate: '', - patientGivenNameFeedback: '', + createPatientError: 'Não foi possível criar um paciente.', + updatePatientError: 'Não foi possível atualizar o paciente', + patientGivenNameFeedback: 'Nome do Paciente é necessário.', + patientDateOfBirthFeedback: 'Data de Nascimento não pode ser maior que hoje.', }, }, } diff --git a/src/patients/GeneralInformation.tsx b/src/patients/GeneralInformation.tsx index bd580ea680..93efcb6ce4 100644 --- a/src/patients/GeneralInformation.tsx +++ b/src/patients/GeneralInformation.tsx @@ -9,25 +9,28 @@ import TextInputWithLabelFormGroup from '../components/input/TextInputWithLabelF import SelectWithLabelFormGroup from '../components/input/SelectWithLableFormGroup' import DatePickerWithLabelFormGroup from '../components/input/DatePickerWithLabelFormGroup' +interface Feedback { + givenName: string + dateOfBirth: string +} + +interface InvalidFields { + givenName: boolean + dateOfBirth: boolean +} + interface Props { patient: Patient isEditable?: boolean errorMessage?: string onFieldChange?: (key: string, value: string | boolean) => void - isInvalid?: boolean - patientGivenNameFeedback?: string + invalidFields?: InvalidFields + feedbackFields?: Feedback } const GeneralInformation = (props: Props) => { const { t } = useTranslation() - const { - patient, - isEditable, - onFieldChange, - errorMessage, - isInvalid, - patientGivenNameFeedback, - } = props + const { patient, isEditable, onFieldChange, errorMessage, invalidFields, feedbackFields } = props const onSelectChange = (event: React.ChangeEvent, fieldName: string) => onFieldChange && onFieldChange(fieldName, event.target.value) @@ -81,8 +84,8 @@ const GeneralInformation = (props: Props) => { onInputElementChange(event, 'givenName') }} isRequired - isInvalid={isInvalid} - feedback={patientGivenNameFeedback} + isInvalid={invalidFields?.givenName} + feedback={feedbackFields?.givenName} />
@@ -163,6 +166,8 @@ const GeneralInformation = (props: Props) => { ? new Date(patient.dateOfBirth) : undefined } + isInvalid={invalidFields?.dateOfBirth} + feedback={feedbackFields?.dateOfBirth} onChange={(date: Date) => { onDateOfBirthChange(date) }} diff --git a/src/patients/edit/EditPatient.tsx b/src/patients/edit/EditPatient.tsx index ea45f85338..bb9bdce13d 100644 --- a/src/patients/edit/EditPatient.tsx +++ b/src/patients/edit/EditPatient.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useDispatch, useSelector } from 'react-redux' import { Spinner, Button, Toast } from '@hospitalrun/components' +import { parseISO } from 'date-fns' import GeneralInformation from '../GeneralInformation' import useTitle from '../../page-header/useTitle' import Patient from '../../model/Patient' @@ -27,8 +28,14 @@ const EditPatient = () => { const [patient, setPatient] = useState({} as Patient) const [errorMessage, setErrorMessage] = useState('') - const [isInvalid, setIsInvalid] = useState(false) - const [patientGivenNameFeedback, setPatientGivenNameFeedback] = useState('') + const [invalidFields, setInvalidFields] = useState({ + givenName: false, + dateOfBirth: false, + }) + const [feedbackFields, setFeedbackFields] = useState({ + givenName: '', + dateOfBirth: '', + }) const { patient: reduxPatient, isLoading } = useSelector((state: RootState) => state.patient) useTitle( @@ -68,12 +75,40 @@ const EditPatient = () => { ) } - const onSave = () => { + const validateInput = () => { + let inputIsValid = true + if (!patient.givenName) { - setErrorMessage(t('patient.errors.patientGivenNameRequiredOnUpdate')) - setIsInvalid(true) - setPatientGivenNameFeedback(t('patient.errors.patientGivenNameFeedback')) - } else { + inputIsValid = false + setErrorMessage(t('patient.errors.updatePatientError')) + setInvalidFields((prevState) => ({ + ...prevState, + givenName: true, + })) + setFeedbackFields((prevState) => ({ + ...prevState, + givenName: t('patient.errors.patientGivenNameFeedback'), + })) + } + if (patient.dateOfBirth) { + if (parseISO(patient.dateOfBirth) > new Date(Date.now())) { + inputIsValid = false + setErrorMessage(t('patient.errors.updatePatientError')) + setInvalidFields((prevState) => ({ + ...prevState, + dateOfBirth: true, + })) + setFeedbackFields((prevState) => ({ + ...prevState, + dateOfBirth: t('patient.errors.patientDateOfBirthFeedback'), + })) + } + } + return inputIsValid + } + + const onSave = () => { + if (validateInput()) { dispatch( updatePatient( { @@ -104,8 +139,8 @@ const EditPatient = () => { patient={patient} onFieldChange={onFieldChange} errorMessage={errorMessage} - isInvalid={isInvalid} - patientGivenNameFeedback={patientGivenNameFeedback} + invalidFields={invalidFields} + feedbackFields={feedbackFields} />
diff --git a/src/patients/new/NewPatient.tsx b/src/patients/new/NewPatient.tsx index 5e60756fbe..e6a0867f48 100644 --- a/src/patients/new/NewPatient.tsx +++ b/src/patients/new/NewPatient.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next' import { useDispatch } from 'react-redux' import { Button, Toast } from '@hospitalrun/components' +import { parseISO } from 'date-fns' import GeneralInformation from '../GeneralInformation' import useTitle from '../../page-header/useTitle' import Patient from '../../model/Patient' @@ -23,8 +24,14 @@ const NewPatient = () => { const [patient, setPatient] = useState({} as Patient) const [errorMessage, setErrorMessage] = useState('') - const [isInvalid, setIsInvalid] = useState(false) - const [patientGivenNameFeedback, setPatientGivenNameFeedback] = useState('') + const [invalidFields, setInvalidFields] = useState({ + givenName: false, + dateOfBirth: false, + }) + const [feedbackFields, setFeedbackFields] = useState({ + givenName: '', + dateOfBirth: '', + }) useTitle(t('patients.newPatient')) useAddBreadcrumbs(breadcrumbs, true) @@ -42,12 +49,40 @@ const NewPatient = () => { ) } - const onSave = () => { + const validateInput = () => { + let inputIsValid = true + if (!patient.givenName) { - setErrorMessage(t('patient.errors.patientGivenNameRequiredOnCreate')) - setIsInvalid(true) - setPatientGivenNameFeedback(t('patient.errors.patientGivenNameFeedback')) - } else { + inputIsValid = false + setErrorMessage(t('patient.errors.createPatientError')) + setInvalidFields((prevState) => ({ + ...prevState, + givenName: true, + })) + setFeedbackFields((prevState) => ({ + ...prevState, + givenName: t('patient.errors.patientGivenNameFeedback'), + })) + } + if (patient.dateOfBirth) { + if (parseISO(patient.dateOfBirth) > new Date(Date.now())) { + inputIsValid = false + setErrorMessage(t('patient.errors.createPatientError')) + setInvalidFields((prevState) => ({ + ...prevState, + dateOfBirth: true, + })) + setFeedbackFields((prevState) => ({ + ...prevState, + dateOfBirth: t('patient.errors.patientDateOfBirthFeedback'), + })) + } + } + return inputIsValid + } + + const onSave = () => { + if (validateInput()) { dispatch( createPatient( { @@ -65,6 +100,7 @@ const NewPatient = () => { ...patient, [key]: value, }) + setErrorMessage('') } return ( @@ -74,8 +110,8 @@ const NewPatient = () => { patient={patient} onFieldChange={onFieldChange} errorMessage={errorMessage} - isInvalid={isInvalid} - patientGivenNameFeedback={patientGivenNameFeedback} + invalidFields={invalidFields} + feedbackFields={feedbackFields} />