From 5b48747efcb6c5374e7c1c32abaf5fe07cbbcb3f Mon Sep 17 00:00:00 2001 From: Michael Peels Date: Wed, 18 Sep 2024 11:31:52 -0400 Subject: [PATCH] Update type for Address form. Fix MultiValueEntry component not setting coded values on edit --- .../add/extended/AddPatientExtendedForm.tsx | 7 +- .../inputs/address/AddressMultiEntry.tsx | 32 +-- .../inputs/address/AddressView.spec.tsx | 37 +-- .../extended/inputs/address/AddressView.tsx | 20 +- .../data/address/AddressEntryFields.spec.tsx | 149 ++++++++++ .../data/address/AddressEntryFields.tsx | 264 ++++++++++++++++++ .../data/{ => address}/asAddress.spec.ts | 0 .../patient/data/{ => address}/asAddress.ts | 4 +- .../src/apps/patient/data/index.ts | 2 +- .../entry/multi-value/MultiValueEntry.tsx | 9 +- 10 files changed, 455 insertions(+), 69 deletions(-) create mode 100644 apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.spec.tsx create mode 100644 apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.tsx rename apps/modernization-ui/src/apps/patient/data/{ => address}/asAddress.spec.ts (100%) rename apps/modernization-ui/src/apps/patient/data/{ => address}/asAddress.ts (83%) diff --git a/apps/modernization-ui/src/apps/patient/add/extended/AddPatientExtendedForm.tsx b/apps/modernization-ui/src/apps/patient/add/extended/AddPatientExtendedForm.tsx index 5469ea96b2..c843fa508f 100644 --- a/apps/modernization-ui/src/apps/patient/add/extended/AddPatientExtendedForm.tsx +++ b/apps/modernization-ui/src/apps/patient/add/extended/AddPatientExtendedForm.tsx @@ -1,22 +1,21 @@ import { FormProvider, useForm } from 'react-hook-form'; -import styles from './add-patient-extended-form.module.scss'; import { PhoneAndEmailMultiEntry } from './inputs/phone/PhoneAndEmailMultiEntry'; import { PhoneEmailFields } from 'apps/patient/profile/phoneEmail/PhoneEmailEntry'; import { useState } from 'react'; import { AddPatientExtendedNav } from './nav/AddPatientExtendedNav'; import { AddressMultiEntry } from './inputs/address/AddressMultiEntry'; -import { AddressFields } from 'apps/patient/profile/addresses/AddressEntry'; import { RaceMultiEntry } from './inputs/race/RaceMultiEntry'; import { RaceEntry } from 'apps/patient/profile/race/RaceEntry'; import { NameEntry } from 'apps/patient/profile/names/NameEntry'; import { NameMultiEntry } from './inputs/Name/NameMultiEntry'; import { Administrative } from './inputs/administrative/Administrative'; -import { AdministrativeEntry } from 'apps/patient/data/entry'; +import { AddressEntry, AdministrativeEntry } from 'apps/patient/data/entry'; import { internalizeDate } from 'date'; +import styles from './add-patient-extended-form.module.scss'; type ExtendedPatientCreationForm = { administrative: AdministrativeEntry; - address: AddressFields[]; + address: AddressEntry[]; phone: PhoneEmailFields[]; race: RaceEntry[]; name: NameEntry[]; diff --git a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressMultiEntry.tsx b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressMultiEntry.tsx index 46b8b50ada..0cca399acf 100644 --- a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressMultiEntry.tsx +++ b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressMultiEntry.tsx @@ -1,48 +1,44 @@ -import { AddressFields } from 'apps/patient/profile/addresses/AddressEntry'; -import { AddressEntryFields } from 'apps/patient/profile/addresses/AddressEntryFields'; +import { AddressEntry } from 'apps/patient/data/entry'; import { internalizeDate } from 'date'; import { MultiValueEntry } from 'design-system/entry/multi-value/MultiValueEntry'; import { Column } from 'design-system/table'; import { AddressView } from './AddressView'; -import { usePatientAddressCodedValues } from 'apps/patient/profile/addresses/usePatientAddressCodedValues'; -import { useLocationCodedValues } from 'location'; +import { AddressEntryFields } from 'apps/patient/data/address/AddressEntryFields'; -const defaultValue: AddressFields = { +const defaultValue: AddressEntry = { asOf: internalizeDate(new Date()), - type: '', - use: '', + type: { name: '', value: '' }, + use: { name: '', value: '' }, address1: '', address2: '', city: '', - state: '', + state: { name: '', value: '' }, zipcode: '', - county: '', - country: '', + county: { name: '', value: '' }, + country: { name: '', value: '' }, censusTract: '', comment: '' }; type Props = { - onChange: (data: AddressFields[]) => void; + onChange: (data: AddressEntry[]) => void; isDirty: (isDirty: boolean) => void; }; export const AddressMultiEntry = ({ onChange, isDirty }: Props) => { - const coded = usePatientAddressCodedValues(); - const location = useLocationCodedValues(); const renderForm = () => ; - const renderView = (entry: AddressFields) => ; + const renderView = (entry: AddressEntry) => ; - const columns: Column[] = [ + const columns: Column[] = [ { id: 'addressAsOf', name: 'As of', render: (v) => v.asOf }, - { id: 'addressType', name: 'Type', render: (v) => coded.types.find((t) => t.value === v.type)?.name }, + { id: 'addressType', name: 'Type', render: (v) => v.type.name }, { id: 'address', name: 'Address', render: (v) => v.address1 }, { id: 'city', name: 'City', render: (v) => v.city }, - { id: 'state', name: 'State', render: (v) => location.states.all.find((s) => s.value === v.state)?.name }, + { id: 'state', name: 'State', render: (v) => v.state?.name }, { id: 'zip', name: 'Zip', render: (v) => v.zipcode } ]; return ( - + id="section-Address" title="Address" defaultValues={defaultValue} diff --git a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.spec.tsx b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.spec.tsx index 8084f0f536..4d3eed50de 100644 --- a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.spec.tsx +++ b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.spec.tsx @@ -1,41 +1,18 @@ import { render } from '@testing-library/react'; +import { AddressEntry } from 'apps/patient/data/entry'; import { AddressView } from './AddressView'; -import { AddressFields } from 'apps/patient/profile/addresses/AddressEntry'; -const mockPatientAddressCodedValues = { - types: [{ name: 'House', value: 'H' }], - uses: [{ name: 'Home', value: 'HM' }] -}; - -jest.mock('apps/patient/profile/addresses/usePatientAddressCodedValues', () => ({ - usePatientAddressCodedValues: () => mockPatientAddressCodedValues -})); - -const mockLocationCodedValues = { - states: { - all: [{ name: 'StateName', value: '1' }] - }, - counties: { - byState: (state: string) => [{ name: 'CountyName', value: '2' }] - }, - countries: [{ name: 'CountryName', value: '3' }] -}; - -jest.mock('location/useLocationCodedValues', () => ({ - useLocationCodedValues: () => mockLocationCodedValues -})); - -const entry: AddressFields = { +const entry: AddressEntry = { asOf: '12/25/2020', - type: 'H', - use: 'HM', + type: { name: 'House', value: 'H' }, + use: { name: 'Home', value: 'HM' }, address1: '123 main st', address2: '2nd floor', city: 'city', - state: '1', + state: { name: 'StateName', value: '1' }, zipcode: '12345', - county: '2', - country: '3', + county: { name: 'CountyName', value: '2' }, + country: { name: 'CountryName', value: '3' }, censusTract: '22222', comment: 'comment' }; diff --git a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.tsx b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.tsx index 5b95a66230..f739f89eda 100644 --- a/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.tsx +++ b/apps/modernization-ui/src/apps/patient/add/extended/inputs/address/AddressView.tsx @@ -1,29 +1,23 @@ -import { AddressFields } from 'apps/patient/profile/addresses/AddressEntry'; -import { usePatientAddressCodedValues } from 'apps/patient/profile/addresses/usePatientAddressCodedValues'; +import { AddressEntry } from 'apps/patient/data/entry'; import { DataDisplay } from 'design-system/data-display/DataDisplay'; -import { useLocationCodedValues } from 'location'; type Props = { - entry: AddressFields; + entry: AddressEntry; }; export const AddressView = ({ entry }: Props) => { - const coded = usePatientAddressCodedValues(); - const location = useLocationCodedValues(); - const counties = location.counties.byState(entry.state ?? ''); - return ( <> - e.value === entry.type)?.name} required /> - e.value === entry.use)?.name} required /> + + - s.value === entry.state)?.name} /> + - c.value === entry.county)?.name} /> + - c.value === entry.country)?.name} /> + ); diff --git a/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.spec.tsx b/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.spec.tsx new file mode 100644 index 0000000000..c400edbfbb --- /dev/null +++ b/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.spec.tsx @@ -0,0 +1,149 @@ +import { fireEvent, render, waitFor, screen } from '@testing-library/react'; +import { renderHook } from '@testing-library/react-hooks'; +import { act } from 'react-dom/test-utils'; +import { FormProvider, useForm } from 'react-hook-form'; +import { AddressEntry } from '../entry'; +import { AddressEntryFields } from './AddressEntryFields'; +import userEvent from '@testing-library/user-event'; + +const mockPatientAddressCodedValues = { + types: [{ name: 'House', value: 'H' }], + uses: [{ name: 'Home', value: 'HM' }] +}; + +jest.mock('apps/patient/profile/addresses/usePatientAddressCodedValues', () => ({ + usePatientAddressCodedValues: () => mockPatientAddressCodedValues +})); + +const mockLocationCodedValues = { + states: { + all: [{ name: 'StateName', value: '1' }] + }, + counties: { + byState: (state: string) => [{ name: 'CountyName', value: '2' }] + }, + countries: [{ name: 'CountryName', value: '3' }] +}; + +jest.mock('location/useLocationCodedValues', () => ({ + useLocationCodedValues: () => mockLocationCodedValues +})); + +const form = renderHook(() => + useForm({ + mode: 'onBlur', + defaultValues: { + asOf: undefined, + type: undefined, + use: undefined, + address1: '', + address2: '', + city: '', + state: undefined, + zipcode: '', + county: undefined, + country: undefined, + censusTract: '', + comment: '' + } + }) +).result.current; + +describe('PhoneEmailEntryFields', () => { + it('should render the proper labels', () => { + const { getByLabelText } = render( + + + + ); + + expect(getByLabelText('Address as of')).toBeInTheDocument(); + expect(getByLabelText('Type')).toBeInTheDocument(); + expect(getByLabelText('Use')).toBeInTheDocument(); + expect(getByLabelText('Street address 1')).toBeInTheDocument(); + expect(getByLabelText('Street address 2')).toBeInTheDocument(); + expect(getByLabelText('City')).toBeInTheDocument(); + expect(getByLabelText('State')).toBeInTheDocument(); + expect(getByLabelText('Zip')).toBeInTheDocument(); + expect(getByLabelText('County')).toBeInTheDocument(); + expect(getByLabelText('Census tract')).toBeInTheDocument(); + expect(getByLabelText('Country')).toBeInTheDocument(); + expect(getByLabelText('Address comments')).toBeInTheDocument(); + }); + + it('should require type', async () => { + const { getByLabelText, getByText } = render( + + + + ); + + const typeInput = getByLabelText('Type'); + act(() => { + fireEvent.blur(typeInput); + }); + await waitFor(() => { + expect(getByText('Type is required.')).toBeInTheDocument(); + }); + }); + + it('should require use', async () => { + const { getByLabelText, getByText } = render( + + + + ); + + const useInput = getByLabelText('Use'); + act(() => { + fireEvent.blur(useInput); + }); + await waitFor(() => { + expect(getByText('Use is required.')).toBeInTheDocument(); + }); + }); + + it('should require as of', async () => { + const { getByLabelText, getByText } = render( + + + + ); + + const asOf = getByLabelText('Address as of'); + act(() => { + fireEvent.blur(asOf); + }); + await waitFor(() => { + expect(getByText('As of date is required.')).toBeInTheDocument(); + }); + }); + + it('should be valid with as of, type, and use', async () => { + const { getByLabelText } = render( + + + + ); + + const asOf = getByLabelText('Address as of'); + const type = getByLabelText('Type'); + const use = getByLabelText('Use'); + await screen.findByText('Use'); + + act(() => { + userEvent.paste(asOf, '01/20/2020'); + fireEvent.blur(asOf); + userEvent.selectOptions(use, 'HM'); + fireEvent.blur(use); + userEvent.selectOptions(type, 'H'); + fireEvent.blur(type); + }); + + await waitFor(() => { + expect(form.getFieldState('asOf').invalid).toBeFalsy(); + expect(form.getFieldState('type').invalid).toBeFalsy(); + expect(form.getFieldState('use').invalid).toBeFalsy(); + }); + }); +}); diff --git a/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.tsx b/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.tsx new file mode 100644 index 0000000000..18d1830223 --- /dev/null +++ b/apps/modernization-ui/src/apps/patient/data/address/AddressEntryFields.tsx @@ -0,0 +1,264 @@ +import { AddressSuggestion, AddressSuggestionInput } from 'address/suggestion'; +import { usePatientAddressCodedValues } from 'apps/patient/profile/addresses/usePatientAddressCodedValues'; +import { DatePickerInput } from 'components/FormInputs/DatePickerInput'; +import { Input } from 'components/FormInputs/Input'; +import { SingleSelect } from 'design-system/select'; +import { useLocationCodedValues } from 'location'; +import { Controller, useFormContext, useWatch } from 'react-hook-form'; +import { maxLengthRule } from 'validation/entry'; +import { AddressEntry } from '../entry'; + +export const AddressEntryFields = () => { + const { control, reset } = useFormContext(); + const coded = usePatientAddressCodedValues(); + const location = useLocationCodedValues(); + const selectedState = useWatch({ control, name: 'state' }); + const enteredCity = useWatch({ control, name: 'city' }); + const enteredZip = useWatch({ control, name: 'zipcode' }); + const counties = location.counties.byState(selectedState?.value ?? ''); + + const handleSuggestionSelection = (selected: AddressSuggestion) => { + reset( + { + address1: selected.address1, + city: selected.city, + state: selected.state ?? undefined, + zipcode: selected.zip + }, + { keepDefaultValues: true } + ); + }; + + return ( +
+ ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + ( + + )} + /> +
+ ); +}; diff --git a/apps/modernization-ui/src/apps/patient/data/asAddress.spec.ts b/apps/modernization-ui/src/apps/patient/data/address/asAddress.spec.ts similarity index 100% rename from apps/modernization-ui/src/apps/patient/data/asAddress.spec.ts rename to apps/modernization-ui/src/apps/patient/data/address/asAddress.spec.ts diff --git a/apps/modernization-ui/src/apps/patient/data/asAddress.ts b/apps/modernization-ui/src/apps/patient/data/address/asAddress.ts similarity index 83% rename from apps/modernization-ui/src/apps/patient/data/asAddress.ts rename to apps/modernization-ui/src/apps/patient/data/address/asAddress.ts index 9cd3736291..a39b40c5df 100644 --- a/apps/modernization-ui/src/apps/patient/data/asAddress.ts +++ b/apps/modernization-ui/src/apps/patient/data/address/asAddress.ts @@ -1,6 +1,6 @@ import { asValue } from 'options'; -import { Address } from './api'; -import { AddressEntry } from './entry'; +import { Address } from '../api'; +import { AddressEntry } from '../entry'; const asAddress = (entry: AddressEntry): Address => { const { use, type, state, county, country, ...remaining } = entry; diff --git a/apps/modernization-ui/src/apps/patient/data/index.ts b/apps/modernization-ui/src/apps/patient/data/index.ts index 773f2643c2..4131f9c2be 100644 --- a/apps/modernization-ui/src/apps/patient/data/index.ts +++ b/apps/modernization-ui/src/apps/patient/data/index.ts @@ -1,6 +1,6 @@ export { asAdministrative } from './asAdministrative'; export { asName } from './asName'; -export { asAddress } from './asAddress'; +export { asAddress } from './address/asAddress'; export { asPhoneEmail } from './asPhoneEmail'; export { asIdentification } from './asIdentification'; export { asRace } from './asRace'; diff --git a/apps/modernization-ui/src/design-system/entry/multi-value/MultiValueEntry.tsx b/apps/modernization-ui/src/design-system/entry/multi-value/MultiValueEntry.tsx index 7cc2f2784f..af81aefd39 100644 --- a/apps/modernization-ui/src/design-system/entry/multi-value/MultiValueEntry.tsx +++ b/apps/modernization-ui/src/design-system/entry/multi-value/MultiValueEntry.tsx @@ -67,9 +67,16 @@ export const MultiValueEntry = ({ const handleEdit = (index: number) => { edit(index); - form.reset(state.data[index], { keepDefaultValues: true }); }; + useEffect(() => { + // Perform form reset after status update to allow time for rendering of form + // fixes issue with coded values not being selected within the form + if ('editing' === state.status) { + form.reset(state.data[state.index], { keepDefaultValues: true }); + } + }, [state.status]); + const iconColumn: Column = { id: '', name: '',