From c0148a536764a41e4063f8e5a06690748953815e Mon Sep 17 00:00:00 2001 From: Collin Pastika Date: Mon, 8 Jul 2024 16:23:57 -0400 Subject: [PATCH 01/14] added graphQL mutation squash --- .../Tool/FixEmailAddresses/AddEmailAddress.graphql | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql diff --git a/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql new file mode 100644 index 000000000..e6fabd887 --- /dev/null +++ b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql @@ -0,0 +1,11 @@ +mutation EmailAddresses($input: PersonUpdateMutationInput!) { + updatePerson(input: $input) { + person { + emailAddresses { + nodes { + email + } + } + } + } +} From 73a395cb881f2409967dcc84ac80fc2a5b786fcc Mon Sep 17 00:00:00 2001 From: Collin Pastika Date: Mon, 17 Jun 2024 14:33:20 -0400 Subject: [PATCH 02/14] fixed add email functionality --- .../FixEmailAddresses/EmailValidationForm.tsx | 74 ++++++- .../FixEmailAddressPerson.test.tsx | 94 +++++++-- .../FixEmailAddressPerson.tsx | 8 +- ...sses.graphql => FixEmailAddresses.graphql} | 0 .../FixEmailAddresses.test.tsx | 188 +++++++++++++++--- .../FixEmailAddresses/FixEmailAddresses.tsx | 17 +- .../FixEmailAddressesMocks.ts | 158 +++++++++++---- 7 files changed, 422 insertions(+), 117 deletions(-) rename src/components/Tool/FixEmailAddresses/{GetInvalidEmailAddresses.graphql => FixEmailAddresses.graphql} (100%) diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index 9c10ce480..2487ee12a 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -4,7 +4,13 @@ import { Form, Formik } from 'formik'; 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 { useEmailAddressesMutation } from './AddEmailAddress.generated'; import { RowWrapper } from './FixEmailAddressPerson'; +import { + GetInvalidEmailAddressesDocument, + GetInvalidEmailAddressesQuery, +} from './FixEmailAddresses.generated'; const ContactInputField = styled(TextField, { shouldForwardProp: (prop) => prop !== 'destroyed', @@ -24,16 +30,12 @@ interface EmailValidationFormEmail { interface EmailValidationFormProps { index: number; personId: string; - handleAdd?: (personId: string, email: string) => void; } -//TODO: Implement during MPDX-7946 -const onSubmit = (values, actions) => { - actions.resetForm(); -}; - const EmailValidationForm = ({ personId }: EmailValidationFormProps) => { const { t } = useTranslation(); + const accountListId = useAccountListId(); + const [emailAddressesMutation] = useEmailAddressesMutation(); const initialEmail = { email: '', @@ -55,6 +57,66 @@ const EmailValidationForm = ({ personId }: EmailValidationFormProps) => { isValid: Yup.bool().default(false), }); + const onSubmit = (values, actions) => { + emailAddressesMutation({ + variables: { + input: { + attributes: { + id: personId, + emailAddresses: [ + { + email: values.email, + }, + ], + }, + accountListId: accountListId || '', + }, + }, + update: (cache, { data: addEmailAddressData }) => { + actions.resetForm(); + const query = { + query: GetInvalidEmailAddressesDocument, + variables: { + accountListId: accountListId, + }, + }; + const dataFromCache = + cache.readQuery(query); + if (dataFromCache) { + const peopleWithNewEmail = dataFromCache.people.nodes.map( + (person) => { + if ( + person.id === personId && + addEmailAddressData?.updatePerson?.person.emailAddresses.nodes + ) { + return { + ...person, + emailAddresses: { + nodes: + addEmailAddressData?.updatePerson?.person.emailAddresses + .nodes, + }, + }; + } else { + return person; + } + }, + ); + + cache.writeQuery({ + ...query, + data: { + people: { + ...dataFromCache.people, + nodes: peopleWithNewEmail, + }, + }, + }); + } + }, + }); + }; + return ( { +const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { const handleChangeMock = jest.fn(); const handleDeleteModalOpenMock = jest.fn(); - const handleAddMock = jest.fn(); const handleChangePrimaryMock = jest.fn(); return ( - + + mocks={mocks} + > + + ); @@ -68,7 +78,15 @@ const TestComponent: React.FC = () => { describe('FixEmailAddressPerson', () => { it('default', () => { const { getByText, getByTestId, getByDisplayValue } = render( - , + , ); expect(getByText(testData.name)).toBeInTheDocument(); @@ -81,7 +99,17 @@ describe('FixEmailAddressPerson', () => { }); it('input reset after adding an email address', async () => { - const { getByTestId, getByLabelText } = render(); + const { getByTestId, getByLabelText } = render( + , + ); const addInput = getByLabelText('New Email Address'); const addButton = getByTestId('addButton-testid'); @@ -99,7 +127,15 @@ describe('FixEmailAddressPerson', () => { describe('validation', () => { it('should show an error message if there is no email', async () => { const { getByLabelText, getByTestId, getByText } = render( - , + , ); const addInput = getByLabelText('New Email Address'); @@ -115,7 +151,15 @@ describe('FixEmailAddressPerson', () => { it('should show an error message if there is an invalid email', async () => { const { getByLabelText, getByTestId, getByText } = render( - , + , ); const addInput = getByLabelText('New Email Address'); @@ -130,7 +174,17 @@ describe('FixEmailAddressPerson', () => { }); it('should not disable the add button', async () => { - const { getByLabelText, getByTestId } = render(); + const { getByLabelText, getByTestId } = render( + , + ); const addInput = getByLabelText('New Email Address'); userEvent.type(addInput, 'new@new.com'); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx index 6fbc00973..b2795932c 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx @@ -104,7 +104,6 @@ export interface FixEmailAddressPersonProps { event: React.ChangeEvent, ) => void; handleDelete: (personId: string, emailAddress: number) => void; - handleAdd: (personId: string, email: string) => void; handleChangePrimary: (personId: string, emailIndex: number) => void; setContactFocus: SetContactFocus; } @@ -118,7 +117,6 @@ export const FixEmailAddressPerson: React.FC = ({ handleDelete, handleChangePrimary, setContactFocus, - handleAdd, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -277,11 +275,7 @@ export const FixEmailAddressPerson: React.FC = ({ { //TODO: index will need to be mapped to the correct personId } - + diff --git a/src/components/Tool/FixEmailAddresses/GetInvalidEmailAddresses.graphql b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql similarity index 100% rename from src/components/Tool/FixEmailAddresses/GetInvalidEmailAddresses.graphql rename to src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 645e76eab..03b0903af 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -1,18 +1,25 @@ import React from 'react'; +import { ApolloCache, InMemoryCache } from '@apollo/client'; import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ErgonoMockShape } from 'graphql-ergonomock'; +import { ApolloErgonoMockMap, ErgonoMockShape } from 'graphql-ergonomock'; 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 theme from '../../../theme'; +import { EmailAddressesMutation } from './AddEmailAddress.generated'; import { FixEmailAddresses } from './FixEmailAddresses'; import { contactId, + contactOneEmailAddressNodes, + contactTwoEmailAddressNodes, + mockCacheWriteData, + mockCacheWriteDataContactTwo, mockInvalidEmailAddressesResponse, + newEmail, } from './FixEmailAddressesMocks'; -import { GetInvalidEmailAddressesQuery } from './GetInvalidEmailAddresses.generated'; const accountListId = 'test121'; const router = { @@ -23,23 +30,25 @@ const router = { const setContactFocus = jest.fn(); const Components = ({ - mockNodes = mockInvalidEmailAddressesResponse, + mocks = { + GetInvalidEmailAddresses: { + people: { nodes: mockInvalidEmailAddressesResponse }, + }, + }, + cache, }: { - mockNodes?: ErgonoMockShape[]; + mocks?: ApolloErgonoMockMap; + cache?: ApolloCache; }) => ( - mocks={{ - GetInvalidEmailAddresses: { - people: { - nodes: mockNodes, - }, - }, - }} + mocks={mocks} + cache={cache} > { expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); }); - //TODO: Fix during MPDX-7946 - it.skip('add an email address to second person', async () => { - const { getByTestId, getByDisplayValue } = render(); - await waitFor(() => - expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(), - ); - expect(getByTestId('textfield-testid2-0')).toBeInTheDocument(); - - const textfieldNew1 = getByTestId( - 'addNewEmailInput-testid2', - ) as HTMLInputElement; - userEvent.type(textfieldNew1, 'email12345@gmail.com'); - const addButton1 = getByTestId('addButton-testid2'); - userEvent.click(addButton1); - - expect(textfieldNew1.value).toBe(''); - expect(getByTestId('textfield-testid2-2')).toBeInTheDocument(); - expect(getByDisplayValue('email12345@gmail.com')).toBeInTheDocument(); + describe('add email address', () => { + interface AddEmailAddressProps { + postSaveResponse: object; + emailAddressNodes: object[]; + elementToWaitFor: string; + textFieldIndex: number; + addButtonId: string; + cache: InMemoryCache; + } + + const addEmailAddress = async ({ + postSaveResponse, + emailAddressNodes, + elementToWaitFor, + textFieldIndex, + addButtonId, + cache, + }: AddEmailAddressProps) => { + let cardinality = 0; + jest.spyOn(cache, 'readQuery').mockReturnValue(postSaveResponse); + jest.spyOn(cache, 'writeQuery'); + + const updatePerson = { + person: { + emailAddresses: { + nodes: [ + ...emailAddressNodes, + { + email: newEmail.email, + }, + ], + }, + }, + } as ErgonoMockShape; + + const { getByTestId, getAllByLabelText } = render( + { + let queryResult; + if (cardinality === 0) { + queryResult = { + people: { + nodes: mockInvalidEmailAddressesResponse, + }, + }; + } else { + queryResult = postSaveResponse; + } + cardinality++; + return queryResult; + }, + EmailAddresses: { updatePerson }, + }} + cache={cache} + />, + ); + await waitFor(() => { + expect(getByTestId(elementToWaitFor)).toBeInTheDocument(); + }); + + const textFieldNew = + getAllByLabelText('New Email Address')[textFieldIndex]; + userEvent.type(textFieldNew, newEmail.email); + const addButton = getByTestId(addButtonId); + userEvent.click(addButton); + }; + + it('should add an email address to the first person', async () => { + const cache = new InMemoryCache(); + const postSaveResponse = { + people: { + nodes: [ + { + ...mockInvalidEmailAddressesResponse[0], + emailAddresses: { + nodes: [...contactOneEmailAddressNodes, newEmail], + }, + }, + { ...mockInvalidEmailAddressesResponse[1] }, + ], + }, + }; + await addEmailAddress({ + postSaveResponse, + emailAddressNodes: contactOneEmailAddressNodes, + elementToWaitFor: 'textfield-testid-0', + textFieldIndex: 0, + addButtonId: 'addButton-testid', + cache, + }); + + await waitFor(() => { + expect(cache.writeQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ data: mockCacheWriteData }), + ); + }); + }); + + it('should add an email address to the second person', async () => { + const cache = new InMemoryCache(); + const postSaveResponse = { + people: { + nodes: [ + { ...mockInvalidEmailAddressesResponse[0] }, + { + ...mockInvalidEmailAddressesResponse[1], + emailAddresses: { + nodes: [...contactTwoEmailAddressNodes, newEmail], + }, + }, + ], + }, + }; + await addEmailAddress({ + postSaveResponse, + emailAddressNodes: contactTwoEmailAddressNodes, + elementToWaitFor: 'textfield-testid2-0', + textFieldIndex: 1, + addButtonId: 'addButton-testid2', + cache, + }); + + await waitFor(() => { + expect(cache.writeQuery).toHaveBeenLastCalledWith( + expect.objectContaining({ data: mockCacheWriteDataContactTwo }), + ); + }); + }); }); it('should render no contacts with no data', async () => { - const { getByText, getByTestId } = render(); + const { getByText, getByTestId } = render( + , + ); await waitFor(() => expect(getByTestId('fixEmailAddresses-null-state')).toBeInTheDocument(), ); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index c89c99262..00fb2288f 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -11,6 +11,7 @@ import { import { styled } from '@mui/material/styles'; import { Trans, useTranslation } from 'react-i18next'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; +import { useGetInvalidEmailAddressesQuery } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; import { PersonEmailAddressInput } from 'src/graphql/types.generated'; import theme from '../../../theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; @@ -18,7 +19,6 @@ import NoData from '../NoData'; import { StyledInput } from '../StyledInput'; import DeleteModal from './DeleteModal'; import { FixEmailAddressPerson } from './FixEmailAddressPerson'; -import { useGetInvalidEmailAddressesQuery } from './GetInvalidEmailAddresses.generated'; const Container = styled(Box)(() => ({ padding: theme.spacing(3), @@ -206,25 +206,13 @@ export const FixEmailAddresses: React.FC = ({ handleDeleteModalClose(); }; - // Add a new email address to the state - const handleAdd = (personId: string, email: string): void => { - const temp = { ...dataState }; - temp[personId].emailAddresses.push({ - updatedAt: new Date().toISOString(), - email: email, - primary: false, - source: 'MPDX', - }); - setDataState(temp); - }; - // Change the primary address in the state const handleChangePrimary = (personId: string, emailIndex: number): void => { const temp = { ...dataState }; temp[personId].emailAddresses = temp[personId].emailAddresses.map( (email, index) => ({ ...email, - primary: index === emailIndex ? true : false, + primary: index === emailIndex, }), ); setDataState(temp); @@ -296,7 +284,6 @@ export const FixEmailAddresses: React.FC = ({ contactId={person.contactId} handleChange={handleChange} handleDelete={handleDeleteModalOpen} - handleAdd={handleAdd} handleChangePrimary={handleChangePrimary} setContactFocus={setContactFocus} /> diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts b/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts index 744daf715..040722f63 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts @@ -2,6 +2,61 @@ import { ErgonoMockShape } from 'graphql-ergonomock'; export const contactId = 'contactId'; +export const newEmail = { + __typename: 'EmailAddress', + id: 'id6', + updatedAt: new Date('2021-06-22T03:40:05-06:00').toISOString(), + email: 'email12345@gmail.com', + primary: false, + source: 'MPDX', +}; + +export const contactOneEmailAddressNodes = [ + { + __typename: 'EmailAddress', + id: 'id1', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + email: 'email1@gmail.com', + primary: true, + source: 'MPDX', + }, + { + __typename: 'EmailAddress', + id: 'id12', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + email: 'email2@gmail.com', + primary: false, + source: 'MPDX', + }, + { + __typename: 'EmailAddress', + id: 'id3', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + email: 'email3@gmail.com', + primary: false, + source: 'MPDX', + }, +]; + +export const contactTwoEmailAddressNodes = [ + { + __typename: 'EmailAddress', + id: 'id4', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + email: 'email4@gmail.com', + primary: true, + source: 'MPDX', + }, + { + __typename: 'EmailAddress', + id: 'id5', + updatedAt: new Date('2021-06-22T03:40:05-06:00').toISOString(), + email: 'email5@gmail.com', + primary: false, + source: 'MPDX', + }, +]; + export const mockInvalidEmailAddressesResponse: ErgonoMockShape[] = [ { id: 'testid', @@ -9,29 +64,7 @@ export const mockInvalidEmailAddressesResponse: ErgonoMockShape[] = [ lastName: 'Contact', contactId, emailAddresses: { - nodes: [ - { - id: 'id1', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - email: 'email1@gmail.com', - primary: true, - source: 'MPDX', - }, - { - id: 'id12', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - email: 'email2@gmail.com', - primary: false, - source: 'MPDX', - }, - { - id: 'id3', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - email: 'email3@gmail.com', - primary: false, - source: 'MPDX', - }, - ], + nodes: contactOneEmailAddressNodes, }, }, { @@ -40,22 +73,69 @@ export const mockInvalidEmailAddressesResponse: ErgonoMockShape[] = [ lastName: 'Lion', contactId: 'contactId2', emailAddresses: { - nodes: [ - { - id: 'id4', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: 'email4@gmail.com', - primary: true, - source: 'MPDX', - }, - { - id: 'id5', - updatedAt: new Date('2021-06-22T03:40:05-06:00').toISOString(), - number: 'email5@gmail.com', - primary: false, - source: 'MPDX', - }, - ], + nodes: contactTwoEmailAddressNodes, }, }, ]; + +export const mockCacheWriteData = { + people: { + nodes: [ + { + ...mockInvalidEmailAddressesResponse[0], + emailAddresses: { + nodes: [ + { + __typename: contactOneEmailAddressNodes[0].__typename, + email: contactOneEmailAddressNodes[0].email, + }, + { + __typename: contactOneEmailAddressNodes[1].__typename, + email: contactOneEmailAddressNodes[1].email, + }, + { + __typename: contactOneEmailAddressNodes[2].__typename, + email: contactOneEmailAddressNodes[2].email, + }, + { + __typename: newEmail.__typename, + email: newEmail.email, + }, + ], + }, + }, + { + ...mockInvalidEmailAddressesResponse[1], + }, + ], + }, +}; + +export const mockCacheWriteDataContactTwo = { + people: { + nodes: [ + { + ...mockInvalidEmailAddressesResponse[0], + }, + { + ...mockInvalidEmailAddressesResponse[1], + emailAddresses: { + nodes: [ + { + __typename: contactTwoEmailAddressNodes[0].__typename, + email: contactTwoEmailAddressNodes[0].email, + }, + { + __typename: contactTwoEmailAddressNodes[1].__typename, + email: contactTwoEmailAddressNodes[1].email, + }, + { + __typename: newEmail.__typename, + email: newEmail.email, + }, + ], + }, + }, + ], + }, +}; From 17b3df4b662f9979088c434c72da24ba64456294 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 19 Jul 2024 10:52:08 -0400 Subject: [PATCH 03/14] Fix inline email address editing --- .../FixEmailAddressPerson.test.tsx | 14 ++++++++++++-- .../FixEmailAddresses/FixEmailAddressPerson.tsx | 6 ++++-- .../FixEmailAddresses/FixEmailAddresses.test.tsx | 9 ++++----- .../Tool/FixEmailAddresses/FixEmailAddresses.tsx | 3 ++- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx index 4ba32b081..bf7068d4c 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx @@ -6,13 +6,14 @@ import { DateTime } from 'luxon'; import TestWrapper from '__tests__/util/TestWrapper'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; +import { PersonEmailAddressInput } from 'src/graphql/types.generated'; import theme from '../../../theme'; import { EmailAddressesMutation } from './AddEmailAddress.generated'; import { FixEmailAddressPerson, FixEmailAddressPersonProps, } from './FixEmailAddressPerson'; -import { EmailAddressData } from './FixEmailAddresses'; +import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; import { GetInvalidEmailAddressesQuery } from './FixEmailAddresses.generated'; import { mockInvalidEmailAddressesResponse } from './FixEmailAddressesMocks'; @@ -48,6 +49,14 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { const handleChangeMock = jest.fn(); const handleDeleteModalOpenMock = jest.fn(); const handleChangePrimaryMock = jest.fn(); + const toDelete = [] as PersonEmailAddressInput[]; + const dataState = { + id: { + emailAddresses: testData.emailAddresses as EmailAddressData[], + toDelete, + }, + } as { [key: string]: PersonEmailAddresses }; + return ( @@ -58,10 +67,11 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { mocks={mocks} > ({ [theme.breakpoints.up('md')]: { @@ -96,6 +96,7 @@ export interface FixEmailAddressPersonProps { name: string; emailAddresses?: EmailAddressData[]; personId: string; + dataState: { [key: string]: PersonEmailAddresses }; toDelete: PersonEmailAddressInput[]; contactId: string; handleChange: ( @@ -112,6 +113,7 @@ export const FixEmailAddressPerson: React.FC = ({ name, emailAddresses, personId, + dataState, contactId, handleChange, handleDelete, @@ -130,7 +132,7 @@ export const FixEmailAddressPerson: React.FC = ({ personId: personId, isPrimary: email.primary, })) || [], - [emailAddresses], + [emailAddresses, dataState], ); const handleContactNameClick = () => { diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 03b0903af..63f4ab966 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -273,17 +273,16 @@ describe('FixPhoneNumbers-Home', () => { ).toBeInTheDocument(); }); - //TODO: Fix during MPDX-7946 - it.skip('should modify first email of first contact', async () => { + it('should modify first email of first contact', async () => { const { getByTestId } = render(); await waitFor(() => { expect(getByTestId('textfield-testid-0')).toBeInTheDocument(); }); - const firstInput = getByTestId('textfield-testid-0') as HTMLInputElement; + const firstInput = getByTestId('textfield-testid-0'); - expect(firstInput.value).toBe('email1@gmail.com'); + expect(firstInput).toHaveValue('email1@gmail.com'); userEvent.type(firstInput, '123'); - expect(firstInput.value).toBe('email1@gmail.com123'); + expect(firstInput).toHaveValue('email1@gmail.com123'); }); describe('setContactFocus()', () => { diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 00fb2288f..34516aeb6 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -104,7 +104,7 @@ export interface EmailAddressData { destroy?: boolean; } -interface PersonEmailAddresses { +export interface PersonEmailAddresses { emailAddresses: EmailAddressData[]; toDelete: PersonEmailAddressInput[]; } @@ -279,6 +279,7 @@ export const FixEmailAddresses: React.FC = ({ name={`${person.firstName} ${person.lastName}`} key={person.id} personId={person.id} + dataState={dataState} emailAddresses={dataState[person.id]?.emailAddresses} toDelete={dataState[person.id]?.toDelete} contactId={person.contactId} From cfdbd7cfa356f970ed5337f39f86e95efa9ead29 Mon Sep 17 00:00:00 2001 From: Collin Pastika Date: Tue, 9 Jul 2024 15:19:53 -0400 Subject: [PATCH 04/14] 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. --- .../FixEmailAddresses/AddEmailAddress.graphql | 2 +- .../FixEmailAddressPerson.test.tsx | 83 +++++++++---------- .../FixEmailAddressPerson.tsx | 54 ++++++------ .../FixEmailAddresses.graphql | 14 ++++ .../FixEmailAddresses.test.tsx | 8 +- .../FixEmailAddresses/FixEmailAddresses.tsx | 21 +++-- .../FixEmailAddressesMocks.ts | 62 -------------- 7 files changed, 95 insertions(+), 149 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql index e6fabd887..c0eedcfce 100644 --- a/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql +++ b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql @@ -3,7 +3,7 @@ mutation EmailAddresses($input: PersonUpdateMutationInput!) { person { emailAddresses { nodes { - email + ...PersonEmailAddress } } } diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx index bf7068d4c..c79b0dcb5 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx @@ -9,39 +9,38 @@ import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; import { PersonEmailAddressInput } from 'src/graphql/types.generated'; import theme from '../../../theme'; import { EmailAddressesMutation } from './AddEmailAddress.generated'; -import { - FixEmailAddressPerson, - FixEmailAddressPersonProps, -} from './FixEmailAddressPerson'; +import { FixEmailAddressPerson } from './FixEmailAddressPerson'; import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; -import { GetInvalidEmailAddressesQuery } from './FixEmailAddresses.generated'; +import { + GetInvalidEmailAddressesQuery, + PersonInvalidEmailFragment, +} from './FixEmailAddresses.generated'; import { mockInvalidEmailAddressesResponse } from './FixEmailAddressesMocks'; -const testData = { - name: 'Test Contact', - personId: 'testid', +const person: PersonInvalidEmailFragment = { + id: 'contactTestId', + firstName: 'Test', + lastName: 'Contact', contactId: 'contactTestId', - emailAddresses: [ - { - source: 'DonorHub', - updatedAt: DateTime.fromISO('2021-06-21').toString(), - email: 'test1@test1.com', - primary: true, - isValid: false, - personId: 'testid', - isPrimary: true, - } as EmailAddressData, - { - source: 'MPDX', - updatedAt: DateTime.fromISO('2021-06-22').toString(), - email: 'test2@test1.com', - primary: false, - isValid: false, - personId: 'testid', - isPrimary: false, - } as EmailAddressData, - ], -} as FixEmailAddressPersonProps; + emailAddresses: { + nodes: [ + { + id: 'email1', + source: 'DonorHub', + updatedAt: DateTime.fromISO('2021-06-21').toString(), + email: 'test1@test1.com', + primary: true, + }, + { + id: 'email2', + source: 'MPDX', + updatedAt: DateTime.fromISO('2021-06-22').toString(), + email: 'test2@test1.com', + primary: false, + }, + ], + }, +}; const setContactFocus = jest.fn(); @@ -51,8 +50,8 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { const handleChangePrimaryMock = jest.fn(); const toDelete = [] as PersonEmailAddressInput[]; const dataState = { - id: { - emailAddresses: testData.emailAddresses as EmailAddressData[], + contactTestId: { + emailAddresses: person.emailAddresses.nodes as EmailAddressData[], toDelete, }, } as { [key: string]: PersonEmailAddresses }; @@ -67,13 +66,9 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { mocks={mocks} > { />, ); - expect(getByText(testData.name)).toBeInTheDocument(); + expect( + getByText(`${person.firstName} ${person.lastName}`), + ).toBeInTheDocument(); expect(getByText('DonorHub (6/21/2021)')).toBeInTheDocument(); - expect(getByTestId('textfield-testid-0')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-0')).toBeInTheDocument(); expect(getByDisplayValue('test1@test1.com')).toBeInTheDocument(); expect(getByText('MPDX (6/22/2021)')).toBeInTheDocument(); - expect(getByTestId('textfield-testid-1')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-1')).toBeInTheDocument(); expect(getByDisplayValue('test2@test1.com')).toBeInTheDocument(); }); @@ -122,7 +119,7 @@ describe('FixEmailAddressPerson', () => { ); const addInput = getByLabelText('New Email Address'); - const addButton = getByTestId('addButton-testid'); + const addButton = getByTestId('addButton-contactTestId'); userEvent.type(addInput, 'new@new.com'); await waitFor(() => { @@ -152,7 +149,7 @@ describe('FixEmailAddressPerson', () => { userEvent.click(addInput); userEvent.tab(); - const addButton = getByTestId('addButton-testid'); + const addButton = getByTestId('addButton-contactTestId'); await waitFor(() => { expect(addButton).toBeDisabled(); expect(getByText('Please enter a valid email address')).toBeVisible(); @@ -176,7 +173,7 @@ describe('FixEmailAddressPerson', () => { userEvent.type(addInput, 'ab'); userEvent.tab(); - const addButton = getByTestId('addButton-testid'); + const addButton = getByTestId('addButton-contactTestId'); await waitFor(() => { expect(addButton).toBeDisabled(); expect(getByText('Invalid Email Address Format')).toBeVisible(); @@ -200,7 +197,7 @@ describe('FixEmailAddressPerson', () => { userEvent.type(addInput, 'new@new.com'); userEvent.tab(); - const addButton = getByTestId('addButton-testid'); + const addButton = getByTestId('addButton-contactTestId'); await waitFor(() => { expect(addButton).not.toBeDisabled(); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx index 7cb3b27ce..6587b954f 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx @@ -23,7 +23,8 @@ import { dateFormatShort } from 'src/lib/intlFormat'; import theme from '../../../theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import EmailValidationForm from './EmailValidationForm'; -import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; +import { PersonEmailAddresses } from './FixEmailAddresses'; +import { PersonInvalidEmailFragment } from './FixEmailAddresses.generated'; const PersonCard = styled(Box)(({ theme }) => ({ [theme.breakpoints.up('md')]: { @@ -93,12 +94,9 @@ const useStyles = makeStyles()((theme: Theme) => ({ })); export interface FixEmailAddressPersonProps { - name: string; - emailAddresses?: EmailAddressData[]; - personId: string; - dataState: { [key: string]: PersonEmailAddresses }; + person: PersonInvalidEmailFragment; toDelete: PersonEmailAddressInput[]; - contactId: string; + dataState: { [key: string]: PersonEmailAddresses }; handleChange: ( personId: string, numberIndex: number, @@ -110,11 +108,8 @@ export interface FixEmailAddressPersonProps { } export const FixEmailAddressPerson: React.FC = ({ - name, - emailAddresses, - personId, + person, dataState, - contactId, handleChange, handleDelete, handleChangePrimary, @@ -124,16 +119,23 @@ export const FixEmailAddressPerson: React.FC = ({ const locale = useLocale(); const { classes } = useStyles(); - const emails = useMemo( - () => - emailAddresses?.map((email) => ({ + const { id, contactId } = person; + const name = `${person.firstName} ${person.lastName}`; + + const emails = useMemo(() => { + if (!dataState[id]?.emailAddresses.length) { + return []; + } + + return ( + dataState[id]?.emailAddresses.map((email) => ({ ...email, isValid: false, - personId: personId, + personId: id, isPrimary: email.primary, - })) || [], - [emailAddresses, dataState], - ); + })) || [] + ); + }, [person, dataState]); const handleContactNameClick = () => { setContactFocus(contactId); @@ -203,15 +205,13 @@ export const FixEmailAddressPerson: React.FC = ({ {email.isPrimary ? ( - + ) : ( - handleChangePrimary(personId, index) - } + data-testid={`starOutlineIcon-${id}-${index}`} + onClick={() => handleChangePrimary(id, index)} > @@ -227,19 +227,19 @@ export const FixEmailAddressPerson: React.FC = ({ , - ) => handleChange(personId, index, event)} + ) => handleChange(id, index, event)} value={email.email} disabled={email.source !== 'MPDX'} /> {email.source === 'MPDX' ? ( handleDelete(personId, index)} + data-testid={`delete-${id}-${index}`} + onClick={() => handleDelete(id, index)} > @@ -277,7 +277,7 @@ export const FixEmailAddressPerson: React.FC = ({ { //TODO: index will need to be mapped to the correct personId } - + diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql index f4534a40f..1929da204 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql @@ -30,3 +30,17 @@ fragment PersonEmailAddress on EmailAddress { updatedAt source } + +mutation UpdateEmailAddresses($input: PersonUpdateMutationInput!) { + updatePerson(input: $input) { + person { + emailAddresses { + nodes { + email + id + primary + } + } + } + } +} diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 63f4ab966..907485397 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -15,8 +15,6 @@ import { contactId, contactOneEmailAddressNodes, contactTwoEmailAddressNodes, - mockCacheWriteData, - mockCacheWriteDataContactTwo, mockInvalidEmailAddressesResponse, newEmail, } from './FixEmailAddressesMocks'; @@ -152,7 +150,7 @@ describe('FixPhoneNumbers-Home', () => { nodes: [ ...emailAddressNodes, { - email: newEmail.email, + ...newEmail, }, ], }, @@ -218,7 +216,7 @@ describe('FixPhoneNumbers-Home', () => { await waitFor(() => { expect(cache.writeQuery).toHaveBeenLastCalledWith( - expect.objectContaining({ data: mockCacheWriteData }), + expect.objectContaining({ data: postSaveResponse }), ); }); }); @@ -249,7 +247,7 @@ describe('FixPhoneNumbers-Home', () => { await waitFor(() => { expect(cache.writeQuery).toHaveBeenLastCalledWith( - expect.objectContaining({ data: mockCacheWriteDataContactTwo }), + expect.objectContaining({ data: postSaveResponse }), ); }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 34516aeb6..9c35b9fe7 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -157,7 +157,7 @@ export const FixEmailAddresses: React.FC = ({ ) : {}, ), - [loading], + [loading, data], ); const handleDeleteModalOpen = ( @@ -209,12 +209,14 @@ export const FixEmailAddresses: React.FC = ({ // Change the primary address in the state const handleChangePrimary = (personId: string, emailIndex: number): void => { const temp = { ...dataState }; - temp[personId].emailAddresses = temp[personId].emailAddresses.map( - (email, index) => ({ - ...email, - primary: index === emailIndex, - }), - ); + if (temp[personId]) { + temp[personId].emailAddresses = temp[personId].emailAddresses.map( + (email, index) => ({ + ...email, + primary: index === emailIndex, + }), + ); + } setDataState(temp); }; @@ -276,13 +278,10 @@ export const FixEmailAddresses: React.FC = ({ {data?.people.nodes.map((person) => ( Date: Thu, 11 Jul 2024 14:56:59 -0400 Subject: [PATCH 05/14] fixed modal close button styling --- .../Tool/FixEmailAddresses/DeleteModal.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal.tsx index c07cf0977..bb689c5e3 100644 --- a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx +++ b/src/components/Tool/FixEmailAddresses/DeleteModal.tsx @@ -74,13 +74,25 @@ const DeleteModal: React.FC = ({ flexDirection="column" alignItems="center" className={classes.headerBox} + position="relative" > - - {t('Confirm')} - - + + + {t('Confirm')} + } /> From a8b6328ed62d913de37cd0680af52784e85ed198 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 19 Jul 2024 16:35:44 -0400 Subject: [PATCH 06/14] added delete functionality and fixed modal issues --- .../{ => DeleteModal}/DeleteModal.test.tsx | 6 +- .../{ => DeleteModal}/DeleteModal.tsx | 16 +-- .../FixEmailAddresses/EmailValidationForm.tsx | 2 +- .../FixEmailAddressPerson.test.tsx | 16 +-- .../FixEmailAddressPerson.tsx | 18 +-- .../FixEmailAddresses/FixEmailAddresses.tsx | 117 ++++++++++-------- 6 files changed, 91 insertions(+), 84 deletions(-) rename src/components/Tool/FixEmailAddresses/{ => DeleteModal}/DeleteModal.test.tsx (92%) rename src/components/Tool/FixEmailAddresses/{ => DeleteModal}/DeleteModal.tsx (89%) rename src/components/Tool/FixEmailAddresses/{ => FixEmailAddressPerson}/FixEmailAddressPerson.test.tsx (92%) rename src/components/Tool/FixEmailAddresses/{ => FixEmailAddressPerson}/FixEmailAddressPerson.tsx (95%) diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.test.tsx similarity index 92% rename from src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx rename to src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.test.tsx index 9caf1caa8..8bd980207 100644 --- a/src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx +++ b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.test.tsx @@ -2,14 +2,14 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; import TestWrapper from '__tests__/util/TestWrapper'; import { render } from '__tests__/util/testingLibraryReactMock'; -import theme from '../../../theme'; +import theme from 'src/theme'; import DeleteModal from './DeleteModal'; const testState = { open: true, personId: '', - emailIndex: 0, - emailAddress: 'test@test.com', + id: '', + email: 'test@test.com', }; describe('FixEmailAddresses-DeleteModal', () => { diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx similarity index 89% rename from src/components/Tool/FixEmailAddresses/DeleteModal.tsx rename to src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx index bb689c5e3..cafb47221 100644 --- a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx +++ b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx @@ -18,8 +18,8 @@ import { CancelButton, DeleteButton, } from 'src/components/common/Modal/ActionButtons/ActionButtons'; -import theme from '../../../theme'; -import { ModalState } from './FixEmailAddresses'; +import theme from 'src/theme'; +import { ModalState } from '../FixEmailAddresses'; const useStyles = makeStyles()((theme: Theme) => ({ modal: { @@ -50,7 +50,7 @@ const useStyles = makeStyles()((theme: Theme) => ({ interface Props { modalState: ModalState; handleClose: () => void; - handleDelete: () => void; + handleDelete: ({ personId, id, email }: ModalState) => void; } const DeleteModal: React.FC = ({ @@ -61,11 +61,7 @@ const DeleteModal: React.FC = ({ const { classes } = useStyles(); const { t } = useTranslation(); return ( - + = ({ {t('Are you sure you wish to delete this email address:')} - {`"${modalState.emailAddress}"`} + {`"${modalState.email}"`} handleDelete(modalState)} sx={{ marginRight: 0 }} > {/*TODO: make "newAddress" field in address false so it says "edit" instead of "add" */} diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index 2487ee12a..8fed17857 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -6,7 +6,7 @@ import * as Yup from 'yup'; import { AddIcon } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents'; import { useAccountListId } from 'src/hooks/useAccountListId'; import { useEmailAddressesMutation } from './AddEmailAddress.generated'; -import { RowWrapper } from './FixEmailAddressPerson'; +import { RowWrapper } from './FixEmailAddressPerson/FixEmailAddressPerson'; import { GetInvalidEmailAddressesDocument, GetInvalidEmailAddressesQuery, diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx similarity index 92% rename from src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx rename to src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx index c79b0dcb5..a199cd7d3 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -6,16 +6,15 @@ import { DateTime } from 'luxon'; import TestWrapper from '__tests__/util/TestWrapper'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; -import { PersonEmailAddressInput } from 'src/graphql/types.generated'; -import theme from '../../../theme'; -import { EmailAddressesMutation } from './AddEmailAddress.generated'; -import { FixEmailAddressPerson } from './FixEmailAddressPerson'; -import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; +import theme from 'src/theme'; +import { EmailAddressesMutation } from '../AddEmailAddress.generated'; +import { EmailAddressData, PersonEmailAddresses } from '../FixEmailAddresses'; import { GetInvalidEmailAddressesQuery, PersonInvalidEmailFragment, -} from './FixEmailAddresses.generated'; -import { mockInvalidEmailAddressesResponse } from './FixEmailAddressesMocks'; +} from '../FixEmailAddresses.generated'; +import { mockInvalidEmailAddressesResponse } from '../FixEmailAddressesMocks'; +import { FixEmailAddressPerson } from './FixEmailAddressPerson'; const person: PersonInvalidEmailFragment = { id: 'contactTestId', @@ -48,11 +47,9 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { const handleChangeMock = jest.fn(); const handleDeleteModalOpenMock = jest.fn(); const handleChangePrimaryMock = jest.fn(); - const toDelete = [] as PersonEmailAddressInput[]; const dataState = { contactTestId: { emailAddresses: person.emailAddresses.nodes as EmailAddressData[], - toDelete, }, } as { [key: string]: PersonEmailAddresses }; @@ -67,7 +64,6 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { > ({ [theme.breakpoints.up('md')]: { @@ -95,14 +94,13 @@ const useStyles = makeStyles()((theme: Theme) => ({ export interface FixEmailAddressPersonProps { person: PersonInvalidEmailFragment; - toDelete: PersonEmailAddressInput[]; dataState: { [key: string]: PersonEmailAddresses }; handleChange: ( personId: string, numberIndex: number, event: React.ChangeEvent, ) => void; - handleDelete: (personId: string, emailAddress: number) => void; + handleDelete: (personId: string, id: string, email: string) => void; handleChangePrimary: (personId: string, emailIndex: number) => void; setContactFocus: SetContactFocus; } @@ -239,7 +237,9 @@ export const FixEmailAddressPerson: React.FC = ({ {email.source === 'MPDX' ? ( handleDelete(id, index)} + onClick={() => + handleDelete(id, email.id, email.email) + } > diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 9c35b9fe7..98dca54fc 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -9,16 +9,19 @@ 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 { PersonEmailAddressInput } from 'src/graphql/types.generated'; +import { + useGetInvalidEmailAddressesQuery, + useUpdateEmailAddressesMutation, +} from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; import theme from '../../../theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import NoData from '../NoData'; import { StyledInput } from '../StyledInput'; -import DeleteModal from './DeleteModal'; -import { FixEmailAddressPerson } from './FixEmailAddressPerson'; +import DeleteModal from './DeleteModal/DeleteModal'; +import { FixEmailAddressPerson } from './FixEmailAddressPerson/FixEmailAddressPerson'; const Container = styled(Box)(() => ({ padding: theme.spacing(3), @@ -82,21 +85,12 @@ const DefaultSourceWrapper = styled(Box)(({ theme }) => ({ })); export interface ModalState { - open: boolean; personId: string; - emailIndex: number; - emailAddress: string; + id: string; + email: string; } - -const defaultDeleteModalState = { - open: false, - personId: '', - emailIndex: 0, - emailAddress: '', -}; - export interface EmailAddressData { - id?: string; + id: string; primary: boolean; updatedAt: string; source: string; @@ -106,7 +100,6 @@ export interface EmailAddressData { export interface PersonEmailAddresses { emailAddresses: EmailAddressData[]; - toDelete: PersonEmailAddressInput[]; } interface FixEmailAddressesProps { @@ -119,14 +112,16 @@ export const FixEmailAddresses: React.FC = ({ setContactFocus, }) => { const [defaultSource, setDefaultSource] = useState('MPDX'); - const [deleteModalState, setDeleteModalState] = useState( - defaultDeleteModalState, + const [deleteModalState, setDeleteModalState] = useState( + null, ); const { t } = useTranslation(); + const { enqueueSnackbar } = useSnackbar(); const { data, loading } = useGetInvalidEmailAddressesQuery({ variables: { accountListId }, }); + const [updateEmailAddressesMutation] = useUpdateEmailAddressesMutation(); const [dataState, setDataState] = useState<{ [key: string]: PersonEmailAddresses; @@ -150,7 +145,6 @@ export const FixEmailAddresses: React.FC = ({ email: emailAddress.email, }), ), - toDelete: [], }, }), {}, @@ -162,18 +156,57 @@ export const FixEmailAddresses: React.FC = ({ const handleDeleteModalOpen = ( personId: string, - emailIndex: number, + id: string, + email: string, ): void => { setDeleteModalState({ - open: true, - personId: personId, - emailIndex: emailIndex, - emailAddress: dataState[personId].emailAddresses[emailIndex].email, + personId, + id, + email, }); }; const handleDeleteModalClose = (): void => { - setDeleteModalState(defaultDeleteModalState); + setDeleteModalState(null); + }; + + // Delete function called after confirming with the delete modal + const handleDelete = async ({ + personId, + id, + email, + }: ModalState): Promise => { + await updateEmailAddressesMutation({ + variables: { + input: { + accountListId, + attributes: { + id: personId, + emailAddresses: [ + { + id: id, + destroy: true, + }, + ], + }, + }, + }, + update: (cache) => { + cache.evict({ id: `EmailAddress:${id}` }); + cache.gc(); + }, + onCompleted: () => { + enqueueSnackbar(t(`Successfully deleted email address ${email}`), { + variant: 'success', + }); + handleDeleteModalClose(); + }, + onError: () => { + enqueueSnackbar(t(`Error deleting email address ${email}`), { + variant: 'error', + }); + }, + }); }; // Update the state with the textfield's value @@ -187,25 +220,6 @@ export const FixEmailAddresses: React.FC = ({ setDataState(temp); }; - // Delete function called after confirming with the delete modal - const handleDelete = (): void => { - const temp = { ...dataState }; - const deleting = temp[deleteModalState.personId].emailAddresses.splice( - deleteModalState.emailIndex, - 1, - )[0]; - deleting.destroy = true; - deleting.primary && - (temp[deleteModalState.personId].emailAddresses[0].primary = true); // If the deleted email was primary, set the new first index to primary - deleting.id && - temp[deleteModalState.personId].toDelete.push({ - destroy: true, - id: deleting.id, - }); //Only destroy the email if it already exists (has an ID) - setDataState(temp); - handleDeleteModalClose(); - }; - // Change the primary address in the state const handleChangePrimary = (personId: string, emailIndex: number): void => { const temp = { ...dataState }; @@ -281,7 +295,6 @@ export const FixEmailAddresses: React.FC = ({ person={person} key={person.id} dataState={dataState} - toDelete={dataState[person.id]?.toDelete} handleChange={handleChange} handleDelete={handleDeleteModalOpen} handleChangePrimary={handleChangePrimary} @@ -312,11 +325,13 @@ export const FixEmailAddresses: React.FC = ({ style={{ marginTop: theme.spacing(3) }} /> )} - + {deleteModalState && ( + + )} ); }; From f93cc21c9404116540fc189158b96f0539fa2053 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 19 Jul 2024 17:08:32 -0400 Subject: [PATCH 07/14] Do a bit of cleanup of the code --- .../Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx | 1 - .../Tool/FixEmailAddresses/EmailValidationForm.tsx | 3 +-- .../FixEmailAddressPerson/FixEmailAddressPerson.tsx | 7 ++----- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx index cafb47221..8c3c8aa0e 100644 --- a/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx +++ b/src/components/Tool/FixEmailAddresses/DeleteModal/DeleteModal.tsx @@ -112,7 +112,6 @@ const DeleteModal: React.FC = ({ onClick={() => handleDelete(modalState)} sx={{ marginRight: 0 }} > - {/*TODO: make "newAddress" field in address false so it says "edit" instead of "add" */} {t('Delete')} diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index 8fed17857..07d5e136c 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -28,7 +28,6 @@ interface EmailValidationFormEmail { } interface EmailValidationFormProps { - index: number; personId: string; } @@ -69,7 +68,7 @@ const EmailValidationForm = ({ personId }: EmailValidationFormProps) => { }, ], }, - accountListId: accountListId || '', + accountListId: accountListId ?? '', }, }, update: (cache, { data: addEmailAddressData }) => { diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index c0a7d35e4..51ecca5f2 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -182,7 +182,7 @@ export const FixEmailAddressPerson: React.FC = ({ {emails.map((email, index) => ( - + = ({ justifyContent="flex-start" px={2} > - { - //TODO: index will need to be mapped to the correct personId - } - + From 7e3678cb43a68cd171f797a45f5c97327f422075 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 19 Jul 2024 17:13:21 -0400 Subject: [PATCH 08/14] Fix up the tests now that delete is working --- .../FixEmailAddresses.test.tsx | 26 ++++++++++++------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 907485397..07bcfc0d5 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -90,21 +90,23 @@ describe('FixPhoneNumbers-Home', () => { expect(getByTestId('starOutlineIcon-testid-0')).toBeInTheDocument(); }); - //TODO: Fix during MPDX-7936 - it.skip('delete third email from first person', async () => { + it('delete third email from first person', async () => { const { getByTestId, queryByTestId } = render(); const delete02 = await waitFor(() => getByTestId('delete-testid-2')); userEvent.click(delete02); - const deleteButton = getByTestId('modal-delete-button'); + const deleteButton = await waitFor(() => + getByTestId('modal-delete-button'), + ); userEvent.click(deleteButton); - expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); + }); }); - //TODO: Fix during MPDX-7936 - it.skip('change second email for second person to primary then delete it', async () => { + it('change second email for second person to primary then delete it', async () => { const { getByTestId, queryByTestId } = render(); const star11 = await waitFor(() => @@ -112,14 +114,18 @@ describe('FixPhoneNumbers-Home', () => { ); userEvent.click(star11); - const delete11 = getByTestId('delete-testid2-1'); + const delete11 = await waitFor(() => getByTestId('delete-testid2-1')); userEvent.click(delete11); - const deleteButton = getByTestId('modal-delete-button'); + const deleteButton = await waitFor(() => + getByTestId('modal-delete-button'), + ); userEvent.click(deleteButton); - expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); - expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); + expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); + }); }); describe('add email address', () => { From 122cd9bf81453c622525d9ba3ca6772e9758334e Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Wed, 24 Jul 2024 08:55:38 -0400 Subject: [PATCH 09/14] Implement single confirm button --- .../FixEmailAddressPerson.test.tsx | 2 + .../FixEmailAddressPerson.tsx | 19 ++- .../FixEmailAddresses.test.tsx | 144 +++++++++++++++--- .../FixEmailAddresses/FixEmailAddresses.tsx | 48 ++++++ 4 files changed, 192 insertions(+), 21 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx index a199cd7d3..3b64149a7 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -42,6 +42,7 @@ const person: PersonInvalidEmailFragment = { }; const setContactFocus = jest.fn(); +const handleSingleConfirm = jest.fn(); const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { const handleChangeMock = jest.fn(); @@ -68,6 +69,7 @@ const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { handleChange={handleChangeMock} handleDelete={handleDeleteModalOpenMock} handleChangePrimary={handleChangePrimaryMock} + handleSingleConfirm={handleSingleConfirm} setContactFocus={setContactFocus} /> diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index 51ecca5f2..3c134a68a 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -22,7 +22,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 }) => ({ @@ -102,6 +102,10 @@ export interface FixEmailAddressPersonProps { ) => void; handleDelete: (personId: string, id: string, email: string) => void; handleChangePrimary: (personId: string, emailIndex: number) => void; + handleSingleConfirm: ( + person: PersonInvalidEmailFragment, + emails: EmailAddressData[], + ) => void; setContactFocus: SetContactFocus; } @@ -111,6 +115,7 @@ export const FixEmailAddressPerson: React.FC = ({ handleChange, handleDelete, handleChangePrimary, + handleSingleConfirm, setContactFocus, }) => { const { t } = useTranslation(); @@ -130,7 +135,7 @@ export const FixEmailAddressPerson: React.FC = ({ ...email, isValid: false, personId: id, - isPrimary: email.primary, + primary: email.primary, })) || [] ); }, [person, dataState]); @@ -202,7 +207,7 @@ export const FixEmailAddressPerson: React.FC = ({ )})`} - {email.isPrimary ? ( + {email.primary ? ( @@ -289,7 +294,13 @@ 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 07bcfc0d5..f975b9c47 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -4,10 +4,14 @@ import { ThemeProvider } from '@mui/material/styles'; import { render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ApolloErgonoMockMap, ErgonoMockShape } 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'; @@ -27,6 +31,18 @@ const router = { const setContactFocus = jest.fn(); +const mockEnqueue = jest.fn(); +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + const Components = ({ mocks = { GetInvalidEmailAddresses: { @@ -39,22 +55,25 @@ const Components = ({ cache?: ApolloCache; }) => ( - - - - mocks={mocks} - cache={cache} - > - - - - + + + + + mocks={mocks} + cache={cache} + > + + + + + ); @@ -304,4 +323,95 @@ describe('FixPhoneNumbers-Home', () => { expect(setContactFocus).toHaveBeenCalledWith(contactId); }); }); + + describe('handleSingleConfirm', () => { + it('should successfully submit changes to multiple emails', async () => { + const cache = new InMemoryCache(); + const personName = 'Test Contact'; + + const updatePerson = { + person: { + id: mockInvalidEmailAddressesResponse[0].id, + emailAddresses: { + nodes: [ + { + ...contactOneEmailAddressNodes[0], + email: 'different@email.com', + }, + { + ...contactOneEmailAddressNodes[1], + email: 'different2@email.com', + }, + { + ...contactOneEmailAddressNodes[2], + }, + ], + }, + }, + } as ErgonoMockShape; + + const { getAllByRole, queryByTestId, queryByText } = render( + , + ); + + await waitFor(() => + expect(queryByTestId('loading')).not.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(); + }); + }); + + it('should handle an error', async () => { + const cache = new InMemoryCache(); + + const { getAllByRole, queryByTestId } = render( + { + throw new Error('Server Error'); + }, + }} + cache={cache} + />, + ); + + await waitFor(() => + expect(queryByTestId('loading')).not.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 98dca54fc..888902eb3 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -13,9 +13,11 @@ import { useSnackbar } from 'notistack'; import { Trans, useTranslation } from 'react-i18next'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; import { + PersonInvalidEmailFragment, useGetInvalidEmailAddressesQuery, useUpdateEmailAddressesMutation, } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; +import { PersonEmailAddressInput } from 'src/graphql/types.generated'; import theme from '../../../theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import NoData from '../NoData'; @@ -240,6 +242,51 @@ 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}` }); + }, + 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 ? ( @@ -298,6 +345,7 @@ export const FixEmailAddresses: React.FC = ({ handleChange={handleChange} handleDelete={handleDeleteModalOpen} handleChangePrimary={handleChangePrimary} + handleSingleConfirm={handleSingleConfirm} setContactFocus={setContactFocus} /> ))} From ad84715448e974aadd2fe61c64cf89e7dd0342b2 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Wed, 24 Jul 2024 09:18:28 -0400 Subject: [PATCH 10/14] Allow for selection of primary when all email addresses are primary --- .../FixEmailAddressPerson.tsx | 5 +- .../FixEmailAddresses.test.tsx | 75 +++++++++++++++++-- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index 3c134a68a..5b9a9e5e8 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -208,7 +208,10 @@ export const FixEmailAddressPerson: React.FC = ({ {email.primary ? ( - + handleChangePrimary(id, index)} + > ) : ( diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index f975b9c47..084c4a0a9 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -98,15 +98,76 @@ describe('FixPhoneNumbers-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(); + }); + + it('should choose primary and deselect primary from others', async () => { + const { getByTestId, queryByTestId } = render( + , + ); - const star1 = await waitFor(() => getByTestId('starOutlineIcon-testid-1')); - userEvent.click(star1); + 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('delete third email from first person', async () => { From 27c2f639a3cdb46b17d9e97883ed5ba926021652 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 26 Jul 2024 16:12:40 -0400 Subject: [PATCH 11/14] Disable confirm button if there is not exactly 1 primary email for the person --- .../FixEmailAddressPerson.test.tsx | 79 +++++++++++++++++-- .../FixEmailAddressPerson.tsx | 5 ++ .../FixEmailAddresses.test.tsx | 14 ++-- 3 files changed, 85 insertions(+), 13 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx index 3b64149a7..09ef80e4c 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -44,15 +44,20 @@ const person: PersonInvalidEmailFragment = { const setContactFocus = jest.fn(); const handleSingleConfirm = jest.fn(); -const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { - const handleChangeMock = jest.fn(); - const handleDeleteModalOpenMock = jest.fn(); - const handleChangePrimaryMock = jest.fn(); - const dataState = { +const TestComponent = ({ + mocks, + dataState = { contactTestId: { emailAddresses: person.emailAddresses.nodes as EmailAddressData[], }, - } as { [key: string]: PersonEmailAddresses }; + }, +}: { + mocks?: ApolloErgonoMockMap; + dataState?: { [key: string]: PersonEmailAddresses }; +}) => { + const handleChangeMock = jest.fn(); + const handleDeleteModalOpenMock = jest.fn(); + const handleChangePrimaryMock = jest.fn(); return ( @@ -201,4 +206,66 @@ 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(); + + await waitFor(() => { + expect(queryByRole('loading')).not.toBeInTheDocument(); + expect(getByRole('button', { name: 'Confirm' })).not.toBeDisabled(); + }); + }); + }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index 5b9a9e5e8..26148031a 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -144,6 +144,10 @@ export const FixEmailAddressPerson: React.FC = ({ setContactFocus(contactId); }; + const hasOnePrimaryEmail = (): boolean => { + return emails.filter((email) => email.primary)?.length === 1; + }; + return ( @@ -303,6 +307,7 @@ export const FixEmailAddressPerson: React.FC = ({ onClick={() => handleSingleConfirm(person, emails as EmailAddressData[]) } + disabled={!hasOnePrimaryEmail()} > {t('Confirm')} diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 084c4a0a9..ae9839153 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -411,7 +411,7 @@ describe('FixPhoneNumbers-Home', () => { }, } as ErgonoMockShape; - const { getAllByRole, queryByTestId, queryByText } = render( + const { getAllByRole, getByTestId, queryByText } = render( { />, ); - await waitFor(() => - expect(queryByTestId('loading')).not.toBeInTheDocument(), - ); + await waitFor(() => { + expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(); + }); const confirmButton = getAllByRole('button', { name: 'Confirm' })[0]; userEvent.click(confirmButton); @@ -439,12 +439,12 @@ describe('FixPhoneNumbers-Home', () => { ); expect(queryByText(personName)).not.toBeInTheDocument(); }); - }); + }, 999999); it('should handle an error', async () => { const cache = new InMemoryCache(); - const { getAllByRole, queryByTestId } = render( + const { getAllByRole, getByTestId } = render( { ); await waitFor(() => - expect(queryByTestId('loading')).not.toBeInTheDocument(), + expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(), ); const confirmButton = getAllByRole('button', { name: 'Confirm' })[0]; From 86eaa3944b1e8ee9fd3e25feba754973fab5811a Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 7 Aug 2024 13:57:35 -0400 Subject: [PATCH 12/14] Fixing small issues with the code. Nothing crazy --- .../FixEmailAddresses/EmailValidationForm.tsx | 8 +-- .../FixEmailAddressPerson.test.tsx | 6 +++ .../FixEmailAddressPerson.tsx | 5 +- .../FixEmailAddresses.test.tsx | 49 +++++-------------- .../FixEmailAddresses/FixEmailAddresses.tsx | 5 +- 5 files changed, 29 insertions(+), 44 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 b57b2a49c..af547cde6 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -340,10 +340,16 @@ describe('FixEmailAddressPerson', () => { 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 c16d3ec93..e644aac85 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -370,7 +370,10 @@ export const FixEmailAddressPerson: React.FC = ({ justifyContent="flex-start" px={2} > - + diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index a393fe7cc..a9adae804 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { ApolloCache, InMemoryCache } from '@apollo/client'; import { ThemeProvider } from '@mui/material/styles'; import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ApolloErgonoMockMap, ErgonoMockShape } from 'graphql-ergonomock'; +import { ApolloErgonoMockMap } from 'graphql-ergonomock'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import TestWrapper from '__tests__/util/TestWrapper'; @@ -51,10 +50,9 @@ const defaultGraphQLMock = { interface ComponentsProps { mocks?: ApolloErgonoMockMap; - cache?: ApolloCache; } -const Components = ({ mocks = defaultGraphQLMock, cache }: ComponentsProps) => ( +const Components = ({ mocks = defaultGraphQLMock }: ComponentsProps) => ( @@ -66,7 +64,6 @@ const Components = ({ mocks = defaultGraphQLMock, cache }: ComponentsProps) => ( }> mocks={mocks} onCall={mutationSpy} - cache={cache} > { }); it('delete third email from first person', async () => { - const { getByTestId, queryByTestId } = render(); + 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(); @@ -322,31 +321,8 @@ describe('FixEmailAddresses-Home', () => { describe('handleSingleConfirm', () => { it('should successfully submit changes to multiple emails', async () => { - const cache = new InMemoryCache(); const personName = 'Test Contact'; - - const updatePerson = { - person: { - id: mockInvalidEmailAddressesResponse[0].id, - emailAddresses: { - nodes: [ - { - ...contactOneEmailAddressNodes[0], - email: 'different@email.com', - }, - { - ...contactOneEmailAddressNodes[1], - email: 'different2@email.com', - }, - { - ...contactOneEmailAddressNodes[2], - }, - ], - }, - }, - } as ErgonoMockShape; - - const { getAllByRole, getByTestId, queryByText } = render( + const { getAllByRole, getByTestId, getByText, queryByText } = render( { nodes: mockInvalidEmailAddressesResponse, }, }, - UpdateEmailAddresses: { updatePerson }, }} - cache={cache} />, ); @@ -364,6 +338,8 @@ describe('FixEmailAddresses-Home', () => { expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(); }); + expect(getByText(personName)).toBeInTheDocument(); + const confirmButton = getAllByRole('button', { name: 'Confirm' })[0]; userEvent.click(confirmButton); @@ -377,8 +353,6 @@ describe('FixEmailAddresses-Home', () => { }, 999999); it('should handle an error', async () => { - const cache = new InMemoryCache(); - const { getAllByRole, getByTestId } = render( { throw new Error('Server Error'); }, }} - cache={cache} />, ); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 6ee4483d4..3f9c1a052 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -205,6 +205,7 @@ export const FixEmailAddresses: React.FC = ({ }, update: (cache) => { cache.evict({ id: `Person:${person.id}` }); + cache.gc(); }, onCompleted: () => { enqueueSnackbar( @@ -231,7 +232,7 @@ export const FixEmailAddresses: React.FC = ({ {t('Fix Email Addresses')} - {data.people.nodes.length && ( + {!!data.people.nodes.length && ( <> @@ -270,7 +271,7 @@ export const FixEmailAddresses: React.FC = ({ )} - {data.people.nodes.length > 0 ? ( + {!!data.people.nodes.length ? ( <> {data?.people.nodes.map((person) => ( From e736c47ce0ce34bb7c9a74d4ee2345598ba78c8e Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 7 Aug 2024 16:29:57 -0400 Subject: [PATCH 13/14] removing screen logs on tests --- .../FixEmailAddressPerson/FixEmailAddressPerson.test.tsx | 7 +------ .../Tool/FixEmailAddresses/FixEmailAddresses.test.tsx | 3 +-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx index af547cde6..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'; @@ -246,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(); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index a9adae804..3f90ddde5 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -1,6 +1,6 @@ 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'; @@ -238,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(); From 601b4e7462b46f659ddc992e0f4a4671cd5d56b4 Mon Sep 17 00:00:00 2001 From: Daniel Bisgrove Date: Wed, 7 Aug 2024 16:31:17 -0400 Subject: [PATCH 14/14] fixing minor issues --- src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 3f9c1a052..697f948d2 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -18,7 +18,7 @@ import { useUpdateEmailAddressesMutation, } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; import { PersonEmailAddressInput } from 'src/graphql/types.generated'; -import theme from '../../../theme'; +import theme from 'src/theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import NoData from '../NoData'; import { StyledInput } from '../StyledInput';