From 9908aa27ae07d366e1481e755779adc511ec2f31 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Wed, 7 Aug 2024 16:37:20 -0400 Subject: [PATCH] MPDX-7418 - Fix Email Addresses - Confirm Button (#981) * added graphQL mutation * fixed add email functionality * Fix inline email address editing * Fix part of the caching issue. The way the component is using state of the emails is causing the most issues why they aren't updating. The new email gets added but replaces the last email. * fixed modal close button styling * added delete functionality and fixed modal issues * Do a bit of cleanup of the code * Fix up the tests now that delete is working * Implement single confirm button * Allow for selection of primary when all email addresses are primary * Disable confirm button if there is not exactly 1 primary email for the person * Fixing small issues with the code. Nothing crazy --------- Co-authored-by: Collin Pastika Co-authored-by: Daniel Bisgrove --- .../FixEmailAddresses/EmailValidationForm.tsx | 8 +- .../FixEmailAddressPerson.test.tsx | 100 +++++++-- .../FixEmailAddressPerson.tsx | 30 ++- .../FixEmailAddresses.test.tsx | 190 +++++++++++++++--- .../FixEmailAddresses/FixEmailAddresses.tsx | 62 +++++- 5 files changed, 338 insertions(+), 52 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index 0ef12d797..b9823f016 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -5,7 +5,6 @@ import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import * as yup from 'yup'; import { AddIcon } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents'; -import { useAccountListId } from 'src/hooks/useAccountListId'; import i18n from 'src/lib/i18n'; import { useEmailAddressesMutation } from './AddEmailAddress.generated'; import { RowWrapper } from './FixEmailAddressPerson/FixEmailAddressPerson'; @@ -31,6 +30,7 @@ interface EmailValidationFormEmail { interface EmailValidationFormProps { personId: string; + accountListId: string; } const validationSchema = yup.object({ @@ -45,9 +45,11 @@ const validationSchema = yup.object({ isValid: yup.bool().default(false), }); -const EmailValidationForm = ({ personId }: EmailValidationFormProps) => { +const EmailValidationForm = ({ + personId, + accountListId, +}: EmailValidationFormProps) => { const { t } = useTranslation(); - const accountListId = useAccountListId(); const [emailAddressesMutation] = useEmailAddressesMutation(); const { enqueueSnackbar } = useSnackbar(); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx index 74316430c..7b6634135 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -6,11 +6,7 @@ import { DateTime } from 'luxon'; import { SnackbarProvider } from 'notistack'; import TestWrapper from '__tests__/util/TestWrapper'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { - render, - screen, - waitFor, -} from '__tests__/util/testingLibraryReactMock'; +import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; import theme from 'src/theme'; import { EmailAddressesMutation } from '../AddEmailAddress.generated'; import { EmailAddressData, PersonEmailAddresses } from '../FixEmailAddresses'; @@ -49,6 +45,7 @@ const person: PersonInvalidEmailFragment = { const setContactFocus = jest.fn(); const mutationSpy = jest.fn(); +const handleSingleConfirm = jest.fn(); const mockEnqueue = jest.fn(); jest.mock('notistack', () => ({ @@ -62,14 +59,23 @@ jest.mock('notistack', () => ({ }, })); -const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { +const defaultDataState = { + contactTestId: { + emailAddresses: person.emailAddresses.nodes as EmailAddressData[], + }, +} as { [key: string]: PersonEmailAddresses }; + +type TestComponentProps = { + mocks?: ApolloErgonoMockMap; + dataState?: { [key: string]: PersonEmailAddresses }; +}; + +const TestComponent = ({ + mocks, + dataState = defaultDataState, +}: TestComponentProps) => { const handleChangeMock = jest.fn(); const handleChangePrimaryMock = jest.fn(); - const dataState = { - contactTestId: { - emailAddresses: person.emailAddresses.nodes as EmailAddressData[], - }, - } as { [key: string]: PersonEmailAddresses }; return ( @@ -85,9 +91,10 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { @@ -235,7 +242,6 @@ describe('FixEmailAddressPerson', () => { await waitFor(() => getByTestId('delete-contactTestId-1')); userEvent.click(getByTestId('delete-contactTestId-1')); - screen.logTestingPlaygroundURL(); await waitFor(() => { expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(); }); @@ -273,4 +279,72 @@ describe('FixEmailAddressPerson', () => { ); }); }); + + describe('confirm button', () => { + it('should disable confirm button if there is more than one primary email', async () => { + const dataState = { + contactTestId: { + emailAddresses: [ + { + ...person.emailAddresses.nodes[0], + primary: true, + }, + { + ...person.emailAddresses.nodes[1], + primary: true, + }, + ] as EmailAddressData[], + }, + }; + + const { getByRole, queryByRole } = render( + , + ); + + await waitFor(() => { + expect(queryByRole('loading')).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Confirm' })).toBeDisabled(); + }); + }); + + it('should disable confirm button if there are no primary emails', async () => { + const dataState = { + contactTestId: { + emailAddresses: [ + { + ...person.emailAddresses.nodes[0], + primary: false, + }, + { + ...person.emailAddresses.nodes[1], + primary: false, + }, + ] as EmailAddressData[], + }, + }; + const { getByRole, queryByRole } = render( + , + ); + + await waitFor(() => { + expect(queryByRole('loading')).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Confirm' })).toBeDisabled(); + }); + }); + + it('should not disable confirm button if there is exactly one primary email', async () => { + const { getByRole, queryByRole } = render(); + + expect(handleSingleConfirm).toHaveBeenCalledTimes(0); + + await waitFor(() => { + expect(queryByRole('loading')).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Confirm' })).not.toBeDisabled(); + }); + + userEvent.click(getByRole('button', { name: 'Confirm' })); + + expect(handleSingleConfirm).toHaveBeenCalledTimes(1); + }); + }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index e791e5efd..e644aac85 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -25,7 +25,7 @@ import { dateFormatShort } from 'src/lib/intlFormat'; import theme from 'src/theme'; import { ConfirmButtonIcon } from '../../ConfirmButtonIcon'; import EmailValidationForm from '../EmailValidationForm'; -import { PersonEmailAddresses } from '../FixEmailAddresses'; +import { EmailAddressData, PersonEmailAddresses } from '../FixEmailAddresses'; import { PersonInvalidEmailFragment } from '../FixEmailAddresses.generated'; const PersonCard = styled(Box)(({ theme }) => ({ @@ -105,6 +105,10 @@ export interface FixEmailAddressPersonProps { event: React.ChangeEvent, ) => void; handleChangePrimary: (personId: string, emailIndex: number) => void; + handleSingleConfirm: ( + person: PersonInvalidEmailFragment, + emails: EmailAddressData[], + ) => void; setContactFocus: SetContactFocus; } @@ -131,6 +135,7 @@ export const FixEmailAddressPerson: React.FC = ({ accountListId, handleChange, handleChangePrimary, + handleSingleConfirm, setContactFocus, }) => { const { t } = useTranslation(); @@ -214,6 +219,10 @@ export const FixEmailAddressPerson: React.FC = ({ setEmailToDelete(null); }; + const hasOnePrimaryEmail = (): boolean => { + return emails.filter((email) => email.isPrimary)?.length === 1; + }; + return ( <> @@ -287,7 +296,10 @@ export const FixEmailAddressPerson: React.FC = ({ {email.isPrimary ? ( - + handleChangePrimary(id, index)} + > ) : ( @@ -358,7 +370,10 @@ export const FixEmailAddressPerson: React.FC = ({ justifyContent="flex-start" px={2} > - + @@ -373,7 +388,14 @@ export const FixEmailAddressPerson: React.FC = ({ style={{ paddingLeft: theme.spacing(1) }} > - diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index c1d08e0e3..3f90ddde5 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -1,17 +1,22 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; -import { render, screen, waitFor } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ApolloErgonoMockMap } from 'graphql-ergonomock'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; +import TestWrapper from '__tests__/util/TestWrapper'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { GetInvalidEmailAddressesQuery } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; +import { + GetInvalidEmailAddressesQuery, + UpdateEmailAddressesMutation, +} from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; import theme from '../../../theme'; import { EmailAddressesMutation } from './AddEmailAddress.generated'; import { FixEmailAddresses } from './FixEmailAddresses'; import { contactId, + contactOneEmailAddressNodes, mockInvalidEmailAddressesResponse, newEmail, } from './FixEmailAddressesMocks'; @@ -51,18 +56,21 @@ const Components = ({ mocks = defaultGraphQLMock }: ComponentsProps) => ( - - mocks={mocks} - onCall={mutationSpy} - > - - + + + mocks={mocks} + onCall={mutationSpy} + > + + + @@ -89,15 +97,76 @@ describe('FixEmailAddresses-Home', () => { expect(queryByTestId('no-data')).not.toBeInTheDocument(); }); - it('change primary of first email', async () => { - const { getByTestId, queryByTestId } = render(); + describe('handleChangePrimary()', () => { + it('changes primary of first email', async () => { + const { getByTestId, queryByTestId } = render(); + + const star1 = await waitFor(() => + getByTestId('starOutlineIcon-testid-1'), + ); + userEvent.click(star1); + + expect(queryByTestId('starIcon-testid-0')).not.toBeInTheDocument(); + expect(getByTestId('starIcon-testid-1')).toBeInTheDocument(); + expect(getByTestId('starOutlineIcon-testid-0')).toBeInTheDocument(); + }); - const star1 = await waitFor(() => getByTestId('starOutlineIcon-testid-1')); - userEvent.click(star1); + it('should choose primary and deselect primary from others', async () => { + const { getByTestId, queryByTestId } = render( + , + ); + + let newPrimary; + await waitFor(() => { + expect(getByTestId('starIcon-testid-0')).toBeInTheDocument(); + expect(getByTestId('starIcon-testid-1')).toBeInTheDocument(); + newPrimary = getByTestId('starIcon-testid-2'); + expect(newPrimary).toBeInTheDocument(); + }); + userEvent.click(newPrimary); - expect(queryByTestId('starIcon-testid-0')).not.toBeInTheDocument(); - expect(getByTestId('starIcon-testid-1')).toBeInTheDocument(); - expect(getByTestId('starOutlineIcon-testid-0')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByTestId('starIcon-testid-0')).not.toBeInTheDocument(); + expect(queryByTestId('starIcon-testid-1')).not.toBeInTheDocument(); + expect(getByTestId('starIcon-testid-2')).toBeInTheDocument(); + expect(getByTestId('starOutlineIcon-testid-0')).toBeInTheDocument(); + expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(); + expect( + queryByTestId('starOutlineIcon-testid-2'), + ).not.toBeInTheDocument(); + }); + }); }); it('should add an new email address, firing a GraphQL mutation and resetting the form', async () => { @@ -133,17 +202,18 @@ describe('FixEmailAddresses-Home', () => { expect(textFieldNew).toHaveValue(''); }); - //TODO: Fix during MPDX-7936 - it.skip('delete third email from first person', async () => { - const { getByTestId, queryByTestId } = render(); + it('delete third email from first person', async () => { + const { getByTestId, getByText, getByRole, queryByTestId } = render( + , + ); const delete02 = await waitFor(() => getByTestId('delete-testid-2')); userEvent.click(delete02); - const deleteButton = await waitFor(() => - getByTestId('modal-delete-button'), + await waitFor(() => + getByText(`Are you sure you wish to delete this email address:`), ); - userEvent.click(deleteButton); + userEvent.click(getByRole('button', { name: 'Yes' })); await waitFor(() => { expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); @@ -168,7 +238,6 @@ describe('FixEmailAddresses-Home', () => { ); userEvent.click(getByRole('button', { name: 'Yes' })); - screen.logTestingPlaygroundURL(); await waitFor(() => { expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); @@ -246,8 +315,71 @@ describe('FixEmailAddresses-Home', () => { await waitFor(() => expect(getByTestId('starOutlineIcon-testid-2')).toBeInTheDocument(), ); + }); + }); + + describe('handleSingleConfirm', () => { + it('should successfully submit changes to multiple emails', async () => { + const personName = 'Test Contact'; + const { getAllByRole, getByTestId, getByText, queryByText } = render( + , + ); + + await waitFor(() => { + expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(); + }); + + expect(getByText(personName)).toBeInTheDocument(); + + const confirmButton = getAllByRole('button', { name: 'Confirm' })[0]; + userEvent.click(confirmButton); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + `Successfully updated email addresses for ${personName}`, + { variant: 'success' }, + ); + expect(queryByText(personName)).not.toBeInTheDocument(); + }); + }, 999999); + + it('should handle an error', async () => { + const { getAllByRole, getByTestId } = render( + { + throw new Error('Server Error'); + }, + }} + />, + ); - screen.logTestingPlaygroundURL(); + await waitFor(() => + expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(), + ); + + const confirmButton = getAllByRole('button', { name: 'Confirm' })[0]; + userEvent.click(confirmButton); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + 'Error updating email addresses for Test Contact', + { variant: 'error', autoHideDuration: 7000 }, + ); + }); }); }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index dcf8849b8..697f948d2 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -9,9 +9,15 @@ import { Typography, } from '@mui/material'; import { styled } from '@mui/material/styles'; +import { useSnackbar } from 'notistack'; import { Trans, useTranslation } from 'react-i18next'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; -import { useGetInvalidEmailAddressesQuery } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; +import { + PersonInvalidEmailFragment, + useGetInvalidEmailAddressesQuery, + useUpdateEmailAddressesMutation, +} from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; +import { PersonEmailAddressInput } from 'src/graphql/types.generated'; import theme from 'src/theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import NoData from '../NoData'; @@ -103,10 +109,13 @@ export const FixEmailAddresses: React.FC = ({ }) => { const [defaultSource, setDefaultSource] = useState('MPDX'); const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); const { data, loading } = useGetInvalidEmailAddressesQuery({ variables: { accountListId }, }); + const [updateEmailAddressesMutation] = useUpdateEmailAddressesMutation(); + const [dataState, setDataState] = useState<{ [key: string]: PersonEmailAddresses; }>({}); @@ -169,6 +178,52 @@ export const FixEmailAddresses: React.FC = ({ setDefaultSource(event.target.value); }; + const handleSingleConfirm = async ( + person: PersonInvalidEmailFragment, + emails: EmailAddressData[], + ) => { + const personName = `${person.firstName} ${person.lastName}`; + const emailAddresses = [] as PersonEmailAddressInput[]; + emails.map((emailAddress) => { + emailAddresses.push({ + email: emailAddress.email, + id: emailAddress.id, + primary: emailAddress.primary, + validValues: true, + }); + }); + + await updateEmailAddressesMutation({ + variables: { + input: { + accountListId, + attributes: { + id: person.id, + emailAddresses, + }, + }, + }, + update: (cache) => { + cache.evict({ id: `Person:${person.id}` }); + cache.gc(); + }, + onCompleted: () => { + enqueueSnackbar( + t(`Successfully updated email addresses for ${personName}`), + { + variant: 'success', + }, + ); + }, + onError: () => { + enqueueSnackbar(t(`Error updating email addresses for ${personName}`), { + variant: 'error', + autoHideDuration: 7000, + }); + }, + }); + }; + return ( {!loading && data && dataState ? ( @@ -177,7 +232,7 @@ export const FixEmailAddresses: React.FC = ({ {t('Fix Email Addresses')} - {data.people.nodes.length && ( + {!!data.people.nodes.length && ( <> @@ -216,7 +271,7 @@ export const FixEmailAddresses: React.FC = ({ )} - {data.people.nodes.length > 0 ? ( + {!!data.people.nodes.length ? ( <> {data?.people.nodes.map((person) => ( @@ -227,6 +282,7 @@ export const FixEmailAddresses: React.FC = ({ accountListId={accountListId} handleChange={handleChange} handleChangePrimary={handleChangePrimary} + handleSingleConfirm={handleSingleConfirm} setContactFocus={setContactFocus} /> ))}