diff --git a/pages/accountLists/[accountListId]/tools/fix/phoneNumbers/[[...contactId]].page.test.tsx b/pages/accountLists/[accountListId]/tools/fix/phoneNumbers/[[...contactId]].page.test.tsx index 23bae70e9..109d6806c 100644 --- a/pages/accountLists/[accountListId]/tools/fix/phoneNumbers/[[...contactId]].page.test.tsx +++ b/pages/accountLists/[accountListId]/tools/fix/phoneNumbers/[[...contactId]].page.test.tsx @@ -87,11 +87,4 @@ describe('FixPhoneNumbersPage', () => { ); }); }); - - it('should show errors', async () => { - const { findAllByRole, findByText } = render(); - - userEvent.clear((await findAllByRole('textbox'))[0]); - expect(await findByText('This field is required')).toBeInTheDocument(); - }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx index 2274f5e7b..2fca8126e 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -544,10 +544,10 @@ export const FixEmailAddressPerson: React.FC = ({ title={t('Confirm')} isOpen={true} message={ - + <> {t('Are you sure you wish to delete this email address:')}{' '} {emailToDelete?.email.email} - + } mutation={handleDelete} handleClose={handleDeleteEmailModalClose} diff --git a/src/components/Tool/FixPhoneNumbers/Contact.test.tsx b/src/components/Tool/FixPhoneNumbers/Contact.test.tsx index b59b0b5e8..660e2de53 100644 --- a/src/components/Tool/FixPhoneNumbers/Contact.test.tsx +++ b/src/components/Tool/FixPhoneNumbers/Contact.test.tsx @@ -1,154 +1,323 @@ import React from 'react'; import { ThemeProvider } from '@mui/material/styles'; import userEvent from '@testing-library/user-event'; +import { ApolloErgonoMockMap } from 'graphql-ergonomock'; +import { DateTime } from 'luxon'; +import { SnackbarProvider } from 'notistack'; import TestWrapper from '__tests__/util/TestWrapper'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; -import theme from '../../../theme'; -import Contact from './Contact'; +import { AppSettingsProvider } from 'src/components/common/AppSettings/AppSettingsProvider'; +import theme from 'src/theme'; +import Contact, { PhoneNumberData } from './Contact'; +import { mockInvalidPhoneNumbersResponse } from './FixPhoneNumbersMocks'; +import { + GetInvalidPhoneNumbersQuery, + PersonInvalidNumberFragment, +} from './GetInvalidPhoneNumbers.generated'; +import { UpdatePhoneNumberMutation } from './UpdateInvalidPhoneNumbers.generated'; -const testData = { - name: 'Test Contact', +const accountListId = 'accountListId'; +const person: PersonInvalidNumberFragment = { + id: 'contactTestId', firstName: 'Test', lastName: 'Contact', - avatar: 'https://www.example.com', - id: 'testid', - contactId: 'testid', - isNewPhoneNumber: false, - newPhoneNumber: '', + contactId: 'contactTestId', + avatar: '', phoneNumbers: { nodes: [ { - id: '123', - updatedAt: '2019-12-03', - number: '3533895895', + id: 'number1', + source: 'DonorHub', + updatedAt: DateTime.fromISO('2021-06-21').toString(), + number: '123456', primary: true, - source: 'MPDX', }, { - id: '1234', - updatedAt: '2019-12-04', - number: '623533895895', - primary: false, + id: 'number2', source: 'MPDX', + updatedAt: DateTime.fromISO('2021-06-22').toString(), + number: '78910', + primary: false, }, ], }, }; const setContactFocus = jest.fn(); -const handleDeleteModalOpenMock = jest.fn(); -const updatePhoneNumber = jest.fn(); -const setValuesMock = jest.fn(); +const mutationSpy = jest.fn(); +const handleSingleConfirm = 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 defaultDataState = { + contactTestId: { + phoneNumbers: person.phoneNumbers.nodes, + }, +} as { [key: string]: PhoneNumberData }; + +type TestComponentProps = { + mocks?: ApolloErgonoMockMap; + dataState?: { [key: string]: PhoneNumberData }; +}; + +const TestComponent = ({ + mocks, + dataState = defaultDataState, +}: TestComponentProps) => { + const handleChangeMock = jest.fn(); + const handleChangePrimaryMock = jest.fn(); -const errors = {}; + return ( + + + + + + mocks={mocks} + onCall={mutationSpy} + > + + + + + + + ); +}; -describe('FixPhoneNumbers-Contact', () => { +describe('Fix PhoneNumber Contact', () => { it('default', () => { const { getByText, getByTestId, getByDisplayValue } = render( - - - - - , + , ); - expect(getByText(testData.name)).toBeInTheDocument(); - expect(getByTestId('textfield-testid-0')).toBeInTheDocument(); - expect(getByDisplayValue('3533895895')).toBeInTheDocument(); - expect(getByTestId('textfield-testid-1')).toBeInTheDocument(); - expect(getByDisplayValue('623533895895')).toBeInTheDocument(); + expect( + getByText(`${person.firstName} ${person.lastName}`), + ).toBeInTheDocument(); + expect(getByText('DonorHub (6/21/2021)')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-number1')).toBeInTheDocument(); + expect(getByDisplayValue('123456')).toBeInTheDocument(); + expect(getByText('MPDX (6/22/2021)')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-number2')).toBeInTheDocument(); + expect(getByDisplayValue('78910')).toBeInTheDocument(); }); - it('input reset after adding an phone number', async () => { - const { getByTestId } = render( - - - - - , + it('input reset after adding a phone number', async () => { + const { getByTestId, getByLabelText } = render( + , ); - const addInput = getByTestId( - 'addNewNumberInput-testid', - ) as HTMLInputElement; - const addButton = getByTestId('addButton-testid'); - - userEvent.type(addInput, '1'); + const addInput = getByLabelText('New Phone Number'); + const addButton = getByTestId('addButton-contactTestId'); + userEvent.type(addInput, '000'); + expect(addInput).toHaveValue('000'); + userEvent.click(addButton); await waitFor(() => { - expect(setValuesMock).toHaveBeenCalledWith({ - people: [ - { - ...testData, - isNewPhoneNumber: true, - newPhoneNumber: '1', - }, - ], + expect(addInput).toHaveValue(''); + }); + }); + + describe('validation', () => { + it('should show an error message if there is no number', async () => { + const { getByLabelText, getByTestId, findByText } = render( + , + ); + + const addInput = getByLabelText('New Phone Number'); + userEvent.click(addInput); + userEvent.tab(); + + const addButton = getByTestId('addButton-contactTestId'); + expect(addButton).toBeDisabled(); + + expect( + await findByText('This field is not a valid phone number'), + ).toBeVisible(); + }); + + it('should show an error message if there is an invalid number', async () => { + const { getByLabelText, getByTestId, getByText } = render( + , + ); + + const addInput = getByLabelText('New Phone Number'); + userEvent.type(addInput, 'ab'); + userEvent.tab(); + + const addButton = getByTestId('addButton-contactTestId'); + await waitFor(() => { + expect(addButton).toBeDisabled(); }); + + expect(getByText('This field is not a valid phone number')).toBeVisible(); }); - userEvent.click(addButton); - expect(addInput.value).toBe(''); - }); + it('should not disable the add button', async () => { + const { getByLabelText, getByTestId } = render( + , + ); - it('should call mock functions', async () => { - const { getByTestId } = render( - - - - - , - ); + const addInput = getByLabelText('New Phone Number'); + userEvent.type(addInput, '123'); + userEvent.tab(); - const firstInput = getByTestId('textfield-testid-0') as HTMLInputElement; - expect(firstInput.value).toBe('3533895895'); - userEvent.type(firstInput, '1'); + const addButton = getByTestId('addButton-contactTestId'); + await waitFor(() => { + expect(addButton).not.toBeDisabled(); + }); + }); - await waitFor(() => { - expect(setValuesMock).toHaveBeenCalledWith({ - people: [ - { - ...testData, - phoneNumbers: { - nodes: [ - { ...testData.phoneNumbers.nodes[0], number: '35338958951' }, - testData.phoneNumbers.nodes[1], + it('should show delete confirmation', async () => { + const { getByTestId, getByRole } = render( + , + ); + const deleteButton = getByTestId('delete-contactTestId-number2'); + userEvent.click(deleteButton); + expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(); + userEvent.click(getByRole('button', { name: 'Yes' })); + + const { id, number } = person.phoneNumbers.nodes[1]; + + await waitFor(() => { + expect(mutationSpy.mock.lastCall[0].operation.operationName).toEqual( + 'UpdatePhoneNumber', + ); + expect(mutationSpy.mock.lastCall[0].operation.variables).toEqual({ + input: { + accountListId, + attributes: { + id: person.id, + phoneNumbers: [ + { + id, + destroy: true, + }, ], }, }, - ], + }); + }); + + await waitFor(() => + expect(mockEnqueue).toHaveBeenCalledWith( + `Successfully deleted phone number ${number}`, + { + variant: 'success', + }, + ), + ); + }); + }); + + describe('confirm button', () => { + it('should not disable confirm button if there is exactly one primary number', 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); + }); + }); + describe('submit button', () => { + it('should submit form without errors', async () => { + const { getByTestId } = render(); + const textField = getByTestId('textfield-contactTestId-number2'); + userEvent.type(textField, '123'); + expect(textField).toHaveValue('78910123'); + }); + }); + + it('should submit form with errors', async () => { + const { getByTestId, getAllByTestId } = render(); + const textInput = getByTestId('textfield-contactTestId-number2'); + userEvent.clear(textInput); + expect(textInput).toHaveValue(''); + userEvent.type(textInput, 'p'); + expect(textInput).toHaveValue('p'); + userEvent.click(textInput); + + await waitFor(() => { + expect(getAllByTestId('statusSelectError')[1]).toHaveTextContent( + 'This field is not a valid phone number', + ); }); - userEvent.click(getByTestId('delete-testid-1')); - expect(handleDeleteModalOpenMock).toHaveBeenCalled(); - userEvent.click(getByTestId(`confirmButton-${testData.id}`)); - expect(updatePhoneNumber).toHaveBeenCalled(); }); }); diff --git a/src/components/Tool/FixPhoneNumbers/Contact.tsx b/src/components/Tool/FixPhoneNumbers/Contact.tsx index c6af90bb0..f403693df 100644 --- a/src/components/Tool/FixPhoneNumbers/Contact.tsx +++ b/src/components/Tool/FixPhoneNumbers/Contact.tsx @@ -1,6 +1,6 @@ -import React, { Fragment } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import styled from '@emotion/styled'; -import { mdiCheckboxMarkedCircle, mdiDelete, mdiLock, mdiPlus } from '@mdi/js'; +import { mdiCheckboxMarkedCircle, mdiDelete, mdiLock } from '@mdi/js'; import { Icon } from '@mdi/react'; import StarIcon from '@mui/icons-material/Star'; import StarOutlineIcon from '@mui/icons-material/StarOutline'; @@ -22,21 +22,21 @@ import { Typography, } from '@mui/material'; import clsx from 'clsx'; -import { FormikErrors } from 'formik'; +import { Formik } from 'formik'; import { DateTime } from 'luxon'; +import { useSnackbar } from 'notistack'; import { useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; +import * as yup from 'yup'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; -import { TabKey } from 'src/components/Contacts/ContactDetails/ContactDetails'; +import { Confirmation } from 'src/components/common/Modal/Confirmation/Confirmation'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import { useLocale } from 'src/hooks/useLocale'; import { dateFormatShort } from 'src/lib/intlFormat'; import theme from '../../../theme'; -import { FormValues, FormValuesPerson } from './FixPhoneNumbers'; -import { - PersonInvalidNumberFragment, - PersonPhoneNumberFragment, -} from './GetInvalidPhoneNumbers.generated'; +import { PersonInvalidNumberFragment } from './GetInvalidPhoneNumbers.generated'; +import PhoneValidationForm from './PhoneNumberValidationForm'; +import { useUpdatePhoneNumberMutation } from './UpdateInvalidPhoneNumbers.generated'; const useStyles = makeStyles()((theme: Theme) => ({ left: {}, @@ -96,12 +96,20 @@ const useStyles = makeStyles()((theme: Theme) => ({ paddingB2: { paddingBottom: theme.spacing(1), }, + phoneNumberContainer: { + width: '100%', + }, hoverHighlight: { '&:hover': { color: theme.palette.mpdxBlue.main, cursor: 'pointer', }, }, + ContactIconContainer: { + margin: theme.spacing(0, 1), + width: theme.spacing(4), + height: theme.spacing(4), + }, })); const ContactHeader = styled(CardHeader)(() => ({ @@ -115,465 +123,482 @@ const ContactAvatar = styled(Avatar)(() => ({ height: theme.spacing(4), })); +export interface PhoneNumber { + id: string; + number: string; + primary: boolean; + source: string; + updatedAt: string; +} + +export interface PhoneNumberData { + phoneNumbers: PhoneNumber[]; +} + +interface NumberToDelete { + personId: string; + phoneNumber: PhoneNumber; +} + interface Props { - handleDelete: ( - personIndex: number, + person: PersonInvalidNumberFragment; + handleChange: ( + personId: string, numberIndex: number, - phoneNumber: string, + newNumber: string, ) => void; setContactFocus: SetContactFocus; - handleUpdate: ( - values: FormValues, - personId: string, - personIndex: number, + handleSingleConfirm: ( + person: PersonInvalidNumberFragment, + numbers: PhoneNumber[], ) => void; - errors: FormikErrors; - setValues: (values: FormValues) => void; - values: FormValues; - person: PersonInvalidNumberFragment; - personIndex: number; + dataState: { [key: string]: PhoneNumberData }; + handleChangePrimary: (personId: string, numberIndex: number) => void; + accountListId: string; } const Contact: React.FC = ({ - handleDelete, - setContactFocus, - handleUpdate, - errors, - setValues, - values, person, - personIndex, + handleChange, + setContactFocus, + handleSingleConfirm, + dataState, + handleChangePrimary, + accountListId, }) => { const { t } = useTranslation(); const locale = useLocale(); + const { enqueueSnackbar } = useSnackbar(); const { classes } = useStyles(); const { appName } = useGetAppSettings(); + const [updatePhoneNumber] = useUpdatePhoneNumberMutation(); + const [numberToDelete, setNumberToDelete] = useState( + null, + ); + const { id: personId, contactId } = person; + + const numbers: PhoneNumber[] = useMemo(() => { + return ( + dataState[personId]?.phoneNumbers.map((number) => ({ + ...number, + isValid: false, + personId: personId, + isPrimary: number.primary, + })) || [] + ); + }, [person, dataState]); - const numbers: PersonPhoneNumberFragment[] = person.phoneNumbers.nodes || []; const name: string = `${person.firstName} ${person.lastName}`; + const validationSchema = yup.object({ + newPhone: yup + .string() + .test( + 'is-phone-number', + t('This field is not a valid phone number'), + (val) => typeof val === 'string' && /\d/.test(val), + ) + .required(t('This field is required')), + }); + const handleContactNameClick = () => { - setContactFocus(person.contactId, TabKey.ContactDetails); + setContactFocus(contactId); + }; + + const handleDeleteNumberOpen = ({ + personId, + phoneNumber, + }: NumberToDelete) => { + setNumberToDelete({ personId, phoneNumber }); + }; + + const handleDelete = async (): Promise => { + if (!numberToDelete) { + return; + } + const { personId, phoneNumber } = numberToDelete; + await updatePhoneNumber({ + variables: { + input: { + accountListId: accountListId ?? '', + attributes: { + id: personId, + phoneNumbers: [ + { + id: phoneNumber.id, + destroy: true, + }, + ], + }, + }, + }, + update: (cache) => { + cache.evict({ id: `PhoneNumber:${phoneNumber.id}` }); + cache.gc(); + }, + onCompleted: () => { + enqueueSnackbar( + t(`Successfully deleted phone number {{phoneNumber}}`, { + phoneNumber: phoneNumber.number, + }), + { + variant: 'success', + }, + ); + handleDeleteNumberModalClose(); + }, + onError: () => { + enqueueSnackbar( + t(`Error deleting phone number {{phoneNumber}}`, { + phoneNumber: phoneNumber.number, + }), + { + variant: 'error', + }, + ); + }, + }); + }; + + const handleDeleteNumberModalClose = (): void => { + setNumberToDelete(null); }; return ( - - - - - - - - } - title={ - - - {name} - - - } - action={ - - } - > - - - - - - - - - {t('Source')} - - - - - - - {t('Primary')} + } + title={ + + + {name} - - - - + } + action={ + + } + > + + + + + - - - - {t('Source')}: - - - - {`${phoneNumber.source} (${dateFormatShort( - DateTime.fromISO(phoneNumber.updatedAt), - locale, - )})`} - - + + {t('Source')} + - + - - {phoneNumber.primary ? ( - <> - - - {t('Source')}: - - - - - ) : ( - <> - - - {t('Source')}: - - - - { - const updatedValues = { - people: values.people.map( - (personValue: FormValuesPerson) => - personValue === person - ? { - ...personValue, - phoneNumbers: { - nodes: numbers.map( - ( - number: PersonPhoneNumberFragment, - ) => ({ - ...number, - primary: - number === - phoneNumber, - }), - ), - }, - } - : personValue, - ), - }; - setValues(updatedValues); - }} - /> - - - )} + + {t('Primary')} - + - - , - ) => { - const updatedValues = { - people: values.people.map( - (personValue: FormValuesPerson) => - personValue === person - ? { - ...personValue, - phoneNumbers: { - nodes: numbers.map( - ( - number: PersonPhoneNumberFragment, - ) => ({ - ...number, - number: - number === phoneNumber - ? event.target.value - : number.number, - }), - ), - }, - } - : personValue, - ), - }; - setValues(updatedValues); - }} - value={phoneNumber.number} - disabled={phoneNumber.source !== 'MPDX'} - /> - - - {phoneNumber.source === 'MPDX' ? ( - - handleDelete( - personIndex, - index, - phoneNumber.number || '', - ) - } - className={classes.paddingX} + + {t('Phone Number')} + + + + + {numbers.map((phoneNumber, index) => ( + { + handleChange(personId, index, values.newPhone); + }} + > + {({ + values: { newPhone }, + setFieldValue, + handleSubmit, + errors, + }): ReactElement => ( + <> + - - - - - ) : ( - + + + {t('Source')}: + + + + {`${phoneNumber.source} (${dateFormatShort( + DateTime.fromISO(phoneNumber.updatedAt), + locale, + )})`} + + + + + - - - )} - - - {errors?.people?.[personIndex]?.phoneNumbers?.nodes?.[ - index - ]?.number && ( - <> - - - + + {phoneNumber.primary ? ( + <> + + + {t('Source')}: + + + + + ) : ( + <> + + + {t('Source')}: + + + + + handleChangePrimary(personId, index) + } + /> + + + )} + + + + - { - errors?.people?.[personIndex]?.phoneNumbers - ?.nodes?.[index]?.number - } - - - - )} - - ))} - - - - - - {t('Source')}: - - - - {appName} - - - - - - - - , - ) => { - const updatedValues = { - people: values.people.map( - (personValue: FormValuesPerson) => - personValue === person - ? { - ...personValue, - isNewPhoneNumber: true, - newPhoneNumber: event.target.value, + + + { + setFieldValue('newPhone', e.target.value); + handleSubmit(); + }} + disabled={phoneNumber.source !== appName} + /> + + {errors.newPhone} + + + {phoneNumber.source === 'MPDX' ? ( + + + handleDeleteNumberOpen({ + personId, + phoneNumber, + }) } - : personValue, - ), - }; - setValues(updatedValues); - }} - inputProps={{ - 'data-testid': `addNewNumberInput-${person.id}`, - }} - value={values.people[personIndex].newPhoneNumber} - /> - + className={classes.ContactIconContainer} + > + + + + + + ) : ( + + + + + + )} + + + + )} + + ))} + { - const updatedValues = { - people: values.people.map( - (personValue: PersonInvalidNumberFragment) => - personValue === person - ? { - ...person, - phoneNumbers: { - nodes: [ - ...person.phoneNumbers.nodes, - { - updatedAt: DateTime.local().toISO(), - primary: false, - source: appName, - number: - values.people[personIndex] - .newPhoneNumber, - }, - ], - }, - isNewPhoneNumber: false, - newPhoneNumber: '', - } - : personValue, - ), - }; - if (values.people[personIndex].newPhoneNumber) { - setValues(updatedValues as FormValues); - } - }} - data-testid={`addButton-${person.id}`} + justifyContent="space-between" + className={classes.paddingX} > - - - + + + + {t('Source')}: + + + + {appName} + + - + + - {errors?.people?.[personIndex]?.newPhoneNumber && ( - <> - - - - {errors?.people?.[personIndex]?.newPhoneNumber} - - - - )} - - - - - + + + + + - + {numberToDelete && ( + + {t('Are you sure you wish to delete this number:')}{' '} + {numberToDelete?.phoneNumber.number} + + } + mutation={handleDelete} + handleClose={handleDeleteNumberModalClose} + /> + )} + ); }; diff --git a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.test.tsx b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.test.tsx index b2137954f..e72442c77 100644 --- a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.test.tsx +++ b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.test.tsx @@ -8,6 +8,7 @@ import TestRouter from '__tests__/util/TestRouter'; import TestWrapper from '__tests__/util/TestWrapper'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; +import { AppSettingsProvider } from 'src/components/common/AppSettings/AppSettingsProvider'; import theme from '../../../theme'; import FixPhoneNumbers from './FixPhoneNumbers'; import { GetInvalidPhoneNumbersMocks } from './FixPhoneNumbersMocks'; @@ -40,39 +41,41 @@ const Components: React.FC<{ data?: ErgonoMockShape[]; cache?: ApolloCache; }> = ({ data = testData, cache }) => ( - - - - - - mocks={{ - GetInvalidPhoneNumbers: { - people: { - totalCount: 2, - nodes: data, + + + + + + + mocks={{ + GetInvalidPhoneNumbers: { + people: { + totalCount: 2, + nodes: data, + }, }, - }, - }} - cache={cache} - > - - - - - - + }} + cache={cache} + > + + + + + + + ); describe('FixPhoneNumbers-Home', () => { it('default with test data', async () => { const { getByText, queryByTestId, findByText } = render(); - await expect( + expect( await findByText('You have 2 phone numbers to confirm.'), ).toBeInTheDocument(); expect(getByText('Confirm 2 as MPDX')).toBeInTheDocument(); @@ -82,120 +85,144 @@ describe('FixPhoneNumbers-Home', () => { }); it('change primary of first number', async () => { - const { getByTestId, queryByTestId } = render(); + const { getByTestId, queryByTestId, findByTestId } = render(); - const star1 = await waitFor(() => getByTestId('starOutlineIcon-testid-1')); + const star1 = await findByTestId('starOutlineIcon-testid-id2'); userEvent.click(star1); - - expect(queryByTestId('starIcon-testid-0')).not.toBeInTheDocument(); - expect(getByTestId('starIcon-testid-1')).toBeInTheDocument(); - expect(getByTestId('starOutlineIcon-testid-0')).toBeInTheDocument(); + expect(queryByTestId('starIcon-testid-id2')).toBeInTheDocument(); + expect(getByTestId('starOutlineIcon-testid-id1')).toBeInTheDocument(); + expect(getByTestId('starOutlineIcon-testid-id3')).toBeInTheDocument(); }); it('delete third number from first person', async () => { - const { getByTestId, queryByTestId } = render(); + const { queryByTestId, findByText, findByTestId } = render(); - const delete02 = await waitFor(() => getByTestId('delete-testid-2')); + const delete02 = await findByTestId('delete-testid-id3'); userEvent.click(delete02); - const deleteButton = getByTestId('modal-delete-button'); + const deleteButton = await findByText('Yes'); userEvent.click(deleteButton); waitFor(() => { - expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); + expect(queryByTestId('textfield-testid-id3')).not.toBeInTheDocument(); }); }); it('change second number for second person to primary then delete it', async () => { - const { getByTestId, queryByTestId } = render(); - - const star11 = await waitFor(() => - getByTestId('starOutlineIcon-testid2-1'), + const { getByTestId, findByText, queryByTestId, findByTestId } = render( + , ); + + const star11 = await findByTestId('starOutlineIcon-testid2-id5'); userEvent.click(star11); - const delete11 = getByTestId('delete-testid2-1'); + expect(queryByTestId('starIcon-testid2-id5')).toBeInTheDocument(); + + const delete11 = getByTestId('delete-testid2-id5'); userEvent.click(delete11); - const deleteButton = getByTestId('modal-delete-button'); + const deleteButton = await findByText('Yes'); userEvent.click(deleteButton); - expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); - expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); + await waitFor(() => { + expect(queryByTestId('starIcon-testid2-id5')).not.toBeInTheDocument(); + }); }); it('add a phone number to first person', async () => { - const { getByTestId, getByDisplayValue } = render(); - await waitFor(() => - expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(), - ); - expect(getByTestId('textfield-testid2-0')).toBeInTheDocument(); + const { getByTestId, getAllByTestId, getAllByLabelText, findByTestId } = + render(); - const textfieldNew1 = getByTestId( - 'addNewNumberInput-testid2', - ) as HTMLInputElement; + expect(await findByTestId('starIcon-testid-id1')).toBeInTheDocument(); + expect(getByTestId('textfield-testid-id1')).toBeInTheDocument(); + expect(getAllByTestId('phoneNumbers')).toHaveLength(5); + const textfieldNew1 = getAllByLabelText('New Phone Number')[0]; userEvent.type(textfieldNew1, '+12345'); - const addButton1 = getByTestId('addButton-testid2'); - userEvent.click(addButton1); - - expect(textfieldNew1.value).toBe(''); - expect(getByTestId('textfield-testid2-1')).toBeInTheDocument(); - expect(getByDisplayValue('+12345')).toBeInTheDocument(); + const addButton = getByTestId('addButton-testid'); + expect(textfieldNew1).toHaveValue('+12345'); + userEvent.click(addButton); + await waitFor(() => + expect(mockEnqueue).toHaveBeenCalledWith('Added phone number', { + variant: 'success', + }), + ); }); it('should render no contacts with no data', async () => { - const { getByText, getByTestId } = render(); - await waitFor(() => - expect(getByTestId('fixPhoneNumbers-null-state')).toBeInTheDocument(), - ); + const { getByText, findByTestId } = render(); + + expect( + await findByTestId('fixPhoneNumbers-null-state'), + ).toBeInTheDocument(); + expect( getByText('No people with phone numbers need attention'), ).toBeInTheDocument(); }); it('should modify first number of first contact', async () => { - const { getByTestId } = render(); - await waitFor(() => { - expect(getByTestId('textfield-testid-0')).toBeInTheDocument(); - }); - const firstInput = getByTestId('textfield-testid-0') as HTMLInputElement; + const { getByTestId, findByTestId } = render(); + + expect(await findByTestId('textfield-testid-id1')).toBeInTheDocument(); + const firstInput = getByTestId('textfield-testid-id1') as HTMLInputElement; - expect(firstInput.value).toBe('+3533895895'); + expect(firstInput.value).toBe('+353'); userEvent.type(firstInput, '123'); - expect(firstInput.value).toBe('+3533895895123'); + await waitFor(() => { + expect(firstInput).toHaveValue('+353123'); + }); }); it('should hide contact from view', async () => { - const { getByTestId, getByText } = render(); - await waitFor(() => { - expect(getByText(`Simba Lion`)).toBeInTheDocument(); - }); + const { getByTestId, findByText } = render(); + expect(await findByText(`Simba Lion`)).toBeInTheDocument(); userEvent.click(getByTestId('confirmButton-testid')); + await waitFor(() => { - expect(mockEnqueue).toHaveBeenCalledWith('Phone numbers updated!', { - variant: 'success', - }); + expect(mockEnqueue).toHaveBeenCalledWith( + 'Successfully updated phone numbers for Test Contact', + { + variant: 'success', + }, + ); }); }); it('should bulk confirm all phone numbers', async () => { - const { getByTestId, queryByTestId, getByText } = render(); + const { getByTestId, queryByTestId, getByText, findByTestId, getByRole } = + render(); await waitFor(() => { expect(queryByTestId('loading')).not.toBeInTheDocument(); - expect(getByTestId('starOutlineIcon-testid-1')).toBeInTheDocument(); }); - userEvent.click(getByTestId(`starOutlineIcon-testid-1`)); + expect( + await findByTestId('starOutlineIcon-testid-id2'), + ).toBeInTheDocument(); + + userEvent.click(getByTestId(`starOutlineIcon-testid-id2`)); + + const primarySource = getByRole('combobox'); + + userEvent.click(primarySource); + userEvent.click(getByRole('option', { name: 'DataServer' })); + + expect(primarySource).toHaveTextContent('DataServer'); + + expect(getByTestId('source-button')).toHaveTextContent( + 'Confirm 2 as DataServer', + ); const confirmAllButton = getByTestId('source-button'); userEvent.click(confirmAllButton); + userEvent.click(getByText('Yes')); await waitFor(() => { expect(mockEnqueue).toHaveBeenCalledWith(`Phone numbers updated!`, { variant: 'success', }); - expect( - getByText('No people with phone numbers need attention'), - ).toBeVisible(); }); + + expect( + getByText('No people with phone numbers need attention'), + ).toBeVisible(); }); }); diff --git a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.tsx b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.tsx index 3f1c21416..76924658d 100644 --- a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.tsx +++ b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbers.tsx @@ -1,28 +1,28 @@ -import React, { ReactElement, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { mdiCheckboxMarkedCircle } from '@mdi/js'; import Icon from '@mdi/react'; import { Box, Button, CircularProgress, + FormControl, Grid, + InputLabel, MenuItem, Select, SelectChangeEvent, Typography, } from '@mui/material'; -import { Formik } from 'formik'; import { useSnackbar } from 'notistack'; import { Trans, useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; -import * as yup from 'yup'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; +import { Confirmation } from 'src/components/common/Modal/Confirmation/Confirmation'; import useGetAppSettings from 'src/hooks/useGetAppSettings'; import theme from '../../../theme'; import NoData from '../NoData'; import { ToolsGridContainer } from '../styledComponents'; -import Contact from './Contact'; -import DeleteModal from './DeleteModal'; +import Contact, { PhoneNumber, PhoneNumberData } from './Contact'; import { PersonInvalidNumberFragment, PersonPhoneNumberFragment, @@ -75,7 +75,6 @@ const useStyles = makeStyles()(() => ({ }, select: { minWidth: theme.spacing(20), - marginLeft: theme.spacing(2), marginRight: theme.spacing(2), [theme.breakpoints.down('md')]: { @@ -93,13 +92,6 @@ export interface ModalState { phoneNumber: string; } -const defaultDeleteModalState = { - open: false, - personIndex: 0, - numberIndex: 0, - phoneNumber: '', -}; - export interface PersonPhoneNumbers { phoneNumbers: PersonPhoneNumberFragment[]; } @@ -125,55 +117,68 @@ const FixPhoneNumbers: React.FC = ({ const { classes } = useStyles(); const { enqueueSnackbar } = useSnackbar(); const { appName } = useGetAppSettings(); - const [defaultSource, setDefaultSource] = useState( - appName || 'MPDX', - ); - const [deleteModalState, setDeleteModalState] = useState( - defaultDeleteModalState, - ); + const [defaultSource, setDefaultSource] = useState(appName || 'MPDX'); + const [updateInvalidPhoneNumbers] = useUpdateInvalidPhoneNumbersMutation(); - const { data, loading } = useGetInvalidPhoneNumbersQuery({ + const { data } = useGetInvalidPhoneNumbersQuery({ variables: { accountListId }, }); const { t } = useTranslation(); - const initialValues: FormValues = { - people: - data?.people?.nodes.map((person) => ({ - ...person, - isNewPhoneNumber: false, - newPhoneNumber: '', - })) || [], - }; + const [dataState, setDataState] = useState<{ + [key: string]: PhoneNumberData; + }>({}); - const handleSourceChange = (event: SelectChangeEvent): void => { - setDefaultSource(event.target.value); - }; + const [sourceOptions, setSourceOptions] = useState([appName]); + const [showBulkConfirmModal, setShowBulkConfirmModal] = useState(false); + + // Create a mutable copy of the query data and store in the state + useEffect(() => { + const existingSources = new Set(); + + if (appName) { + existingSources.add(appName); + } - const handleDeleteModalClose = (): void => { - setDeleteModalState(defaultDeleteModalState); + const newDataState = data + ? data.people.nodes?.reduce( + (map, person) => ({ + ...map, + [person.id]: { + phoneNumbers: person.phoneNumbers.nodes.map((phoneNumber) => { + existingSources.add(phoneNumber.source); + return { ...phoneNumber }; + }), + }, + }), + {}, + ) + : {}; + setDataState(newDataState); + setSourceOptions([...existingSources]); + }, [data]); + + const handleSourceChange = (event: SelectChangeEvent): void => { + setDefaultSource(event.target.value); }; - const handleDeleteModalOpen = ( - personIndex: number, + const handleChange = ( + personId: string, numberIndex: number, - phoneNumber: string, + newNumber: string, ): void => { - setDeleteModalState({ - open: true, - personIndex, - numberIndex, - phoneNumber, - }); + const temp = { ...dataState }; + dataState[personId].phoneNumbers[numberIndex].number = newNumber; + setDataState(temp); }; - const handleBulkConfirm = async (values: FormValues) => { - const dataToSend = determineBulkDataToSend( - values?.people, - defaultSource ?? '', - ); + const handleBulkConfirm = async () => { + const dataToSend = determineBulkDataToSend(dataState, defaultSource ?? ''); if (!dataToSend.length) { + enqueueSnackbar(t(`No phone numbers were updated`), { + variant: 'warning', + }); return; } @@ -202,234 +207,191 @@ const FixPhoneNumbers: React.FC = ({ }); }; - const updatePhoneNumber = async ( - values: FormValues, - personId: string, - personIndex: number, - ): Promise => { - const attributes = [ - { - phoneNumbers: values.people[personIndex].phoneNumbers.nodes.map( - (phoneNumber: PersonPhoneNumberFragment) => ({ - id: phoneNumber.id, - primary: phoneNumber.primary, - number: phoneNumber.number, - validValues: true, - }), - ), - id: personId, - }, - ]; + const handleSingleConfirm = async ( + person: PersonInvalidNumberFragment, + numbers: PhoneNumber[], + ) => { + const personName = `${person.firstName} ${person.lastName}`; + const phoneNumbers = numbers.map((phoneNumber) => ({ + id: phoneNumber.id, + primary: phoneNumber.primary, + number: phoneNumber.number, + validValues: true, + })); await updateInvalidPhoneNumbers({ variables: { input: { accountListId, - attributes, + attributes: [ + { + id: person.id, + phoneNumbers, + }, + ], }, }, update: (cache) => { - cache.evict({ id: `Person:${personId}` }); - }, - onError: () => { - enqueueSnackbar(t('Error updating phone numbers'), { - variant: 'error', - }); + cache.evict({ id: `Person:${person.id}` }); + cache.gc(); }, onCompleted: () => { - enqueueSnackbar(t('Phone numbers updated!'), { - variant: 'success', - }); + enqueueSnackbar( + t(`Successfully updated phone numbers for {{name}}`, { + name: personName, + }), + { + variant: 'success', + }, + ); + }, + onError: () => { + enqueueSnackbar( + t(`Error updating phone numbers for {{name}}`, { name: personName }), + { + variant: 'error', + }, + ); }, }); }; - const handleDelete = ( - values: FormValues, - setValues: (values: FormValues) => void, - ): void => { - const temp = JSON.parse(JSON.stringify(values)); - - const deleting = temp?.people[ - deleteModalState.personIndex - ].phoneNumbers?.nodes.splice(deleteModalState.numberIndex, 1)[0]; - - deleting.destroy = true; - - if ( - deleting.primary && - temp?.people[deleteModalState.personIndex]?.phoneNumbers?.nodes.length - ) { - temp.people[deleteModalState.personIndex].phoneNumbers.nodes[0].primary = - true; + const handleChangePrimary = (personId: string, numberIndex: number): void => { + if (!dataState[personId]) { + return; } - setValues(temp); - handleDeleteModalClose(); - }; + const temp = { ...dataState }; - const fixPhoneNumberSchema = yup.object({ - people: yup.array().of( - yup.object({ - phoneNumbers: yup.object({ - nodes: yup.array().of( - yup.object({ - id: yup.string().nullable(), - number: yup.string().when('destroy', { - is: true, - then: yup.string().nullable(), - otherwise: yup - .string() - .required(t('This field is required')) - .nullable() - .test( - 'is-phone-number', - t('This field is not a valid phone number'), - (val) => typeof val === 'string' && /\d/.test(val), - ), - }), - destroy: yup.boolean().default(false), - primary: yup.boolean().required('please select a primary number'), - historic: yup.boolean().default(false), - }), - ), - }), - isNewPhoneNumber: yup.boolean().default(false), - newPhoneNumber: yup.string().when('isNewPhoneNumber', { - is: false, - then: yup.string().nullable(), - otherwise: yup - .string() - .required(t('This field is required')) - .nullable() - .test( - 'is-phone-number', - t('This field is not a valid phone number'), - (val) => typeof val === 'string' && /\d/.test(val), - ), - }), + temp[personId].phoneNumbers = temp[personId].phoneNumbers.map( + (number, index) => ({ + ...number, + primary: index === numberIndex, }), - ), - }); + ); + + setDataState(temp); + }; return ( - {!loading && data ? ( - <> - {data?.people.nodes.length > 0 ? ( - {}} - validationSchema={fixPhoneNumberSchema} - > - {({ errors, setValues, values }): ReactElement => ( + {data ? ( + + + + {!!data.people.nodes.length && ( <> - - - - - {t('You have {{amount}} phone numbers to confirm.', { - amount: data.people.totalCount, - })} - - - {t( - 'Choose below which phone number will be set as primary.', - )} - - - - {t('Default Primary Source:')} - + + {t('You have {{amount}} phone numbers to confirm.', { + amount: data.people.totalCount, + })} + + + {t( + 'Choose below which phone number will be set as primary.', + )} + - - + )} - + + + {!!data.people.nodes.length ? ( + <> + + {data?.people.nodes.map( + (person: PersonInvalidNumberFragment) => ( + + ), + )} + + + + + + }} + /> + + + + ) : ( )} - ; - + ) : ( - + )} + setShowBulkConfirmModal(false)} + mutation={handleBulkConfirm} + title={t('Confirm')} + message={t( + `You are updating all contacts visible on this page, setting the first {{defaultSource}} phone number as the + primary phone number. If no such phone number exists, the contact will not be updated. + Are you sure you want to do this?`, + { defaultSource }, + )} + /> ); }; diff --git a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbersMocks.ts b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbersMocks.ts index 533926c7e..2d90a2c73 100644 --- a/src/components/Tool/FixPhoneNumbers/FixPhoneNumbersMocks.ts +++ b/src/components/Tool/FixPhoneNumbers/FixPhoneNumbersMocks.ts @@ -1,3 +1,5 @@ +import { ErgonoMockShape } from 'graphql-ergonomock'; + export const GetInvalidPhoneNumbersMocks = { GetInvalidPhoneNumbers: { people: { @@ -13,7 +15,7 @@ export const GetInvalidPhoneNumbersMocks = { { id: 'id1', updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '+3533895895', + number: '+353', primary: true, source: 'MPDX', }, @@ -46,7 +48,7 @@ export const GetInvalidPhoneNumbersMocks = { updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), number: '+3535785056', primary: true, - source: 'MPDX', + source: 'DataServer', }, { id: 'id5', @@ -62,3 +64,72 @@ export const GetInvalidPhoneNumbersMocks = { }, }, }; + +export const contactOnePhoneNumberNodes = [ + { + __typename: 'EmailAddress', + id: 'id1', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '1111', + primary: true, + source: 'MPDX', + }, + { + __typename: 'EmailAddress', + id: 'id12', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '1112', + primary: false, + source: 'DataServer', + }, + { + __typename: 'EmailAddress', + id: 'id3', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '1113', + primary: false, + source: 'MPDX', + }, +]; + +export const contactTwoPhoneNumberNodes = [ + { + __typename: 'EmailAddress', + id: 'id4', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '1114', + primary: true, + source: 'MPDX', + }, + { + __typename: 'EmailAddress', + id: 'id5', + updatedAt: new Date('2021-06-22T03:40:05-06:00').toISOString(), + number: '1115', + primary: false, + source: 'MPDX', + }, +]; + +export const mockInvalidPhoneNumbersResponse: ErgonoMockShape[] = [ + { + id: 'testid', + firstName: 'Test', + lastName: 'Contact', + contactId: 'contactId1', + avatar: '', + phoneNumbers: { + nodes: contactOnePhoneNumberNodes, + }, + }, + { + id: 'testid2', + firstName: 'Simba', + lastName: 'Lion', + contactId: 'contactId2', + avatar: '', + phoneNumbers: { + nodes: contactTwoPhoneNumberNodes, + }, + }, +]; diff --git a/src/components/Tool/FixPhoneNumbers/PhoneNumberValidationForm.tsx b/src/components/Tool/FixPhoneNumbers/PhoneNumberValidationForm.tsx new file mode 100644 index 000000000..0084eeb3e --- /dev/null +++ b/src/components/Tool/FixPhoneNumbers/PhoneNumberValidationForm.tsx @@ -0,0 +1,226 @@ +import { + Box, + FormControl, + FormHelperText, + Grid, + IconButton, + TextField, + Theme, + Tooltip, +} from '@mui/material'; +import clsx from 'clsx'; +import { Formik } from 'formik'; +import { useSnackbar } from 'notistack'; +import { useTranslation } from 'react-i18next'; +import { makeStyles } from 'tss-react/mui'; +import * as yup from 'yup'; +import { AddIcon } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/StyledComponents'; +import { + GetInvalidPhoneNumbersDocument, + GetInvalidPhoneNumbersQuery, +} from './GetInvalidPhoneNumbers.generated'; +import { useUpdatePhoneNumberMutation } from './UpdateInvalidPhoneNumbers.generated'; + +const useStyles = makeStyles()((theme: Theme) => ({ + responsiveBorder: { + [theme.breakpoints.down('xs')]: { + paddingBottom: theme.spacing(2), + borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`, + }, + }, + paddingX: { + paddingLeft: theme.spacing(2), + paddingRight: theme.spacing(2), + }, + paddingB2: { + paddingBottom: theme.spacing(1), + }, + ContactIconContainer: { + margin: theme.spacing(0, 1), + width: theme.spacing(4), + height: theme.spacing(4), + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + }, +})); + +interface PhoneValidationFormPhone { + number: string; + isPrimary: boolean; + updatedAt: string; + source: string; + personId: string; + isValid: boolean; +} + +interface PhoneNumberValidationFormProps { + personId: string; + accountListId: string; +} + +const PhoneValidationForm = ({ + personId, + accountListId, +}: PhoneNumberValidationFormProps) => { + const { t } = useTranslation(); + const [UpdatePhoneNumber] = useUpdatePhoneNumberMutation(); + const { enqueueSnackbar } = useSnackbar(); + const { classes } = useStyles(); + + const validationSchema = yup.object({ + number: yup + .string() + .test( + 'is-phone-number', + t('This field is not a valid phone number'), + (val) => typeof val === 'string' && /\d/.test(val), + ) + .required(t('This field is required')), + isPrimary: yup.bool().default(false), + updatedAt: yup.string(), + source: yup.string(), + personId: yup.string(), + isValid: yup.bool().default(false), + }); + + const initialPhone = { + number: '', + isPrimary: false, + updatedAt: '', + source: '', + personId, + isValid: false, + } as PhoneValidationFormPhone; + + const onSubmit = (values, actions) => { + UpdatePhoneNumber({ + variables: { + input: { + accountListId: accountListId ?? '', + attributes: { + id: personId, + phoneNumbers: [ + { + number: values.number, + }, + ], + }, + }, + }, + update: (cache, { data: addPhoneNumberData }) => { + actions.resetForm(); + const query = { + query: GetInvalidPhoneNumbersDocument, + variables: { + accountListId: accountListId, + }, + }; + const dataFromCache = + cache.readQuery(query); + if (dataFromCache) { + const peopleWithNewPhone = dataFromCache.people.nodes.map( + (person) => { + if ( + person.id === personId && + addPhoneNumberData?.updatePerson?.person?.phoneNumbers?.nodes + ) { + return { + ...person, + phoneNumbers: { + nodes: + addPhoneNumberData?.updatePerson?.person?.phoneNumbers + .nodes, + }, + }; + } else { + return person; + } + }, + ); + + cache.writeQuery({ + ...query, + data: { + people: { + ...dataFromCache.people, + nodes: peopleWithNewPhone, + }, + }, + }); + } + }, + onCompleted: () => { + enqueueSnackbar(t('Added phone number'), { variant: 'success' }); + }, + onError: () => { + enqueueSnackbar(t('Failed to add phone number'), { variant: 'error' }); + }, + }); + }; + + return ( + + {({ + values, + handleChange, + handleBlur, + touched, + errors, + handleSubmit, + isValid, + }) => ( + <> + + + + + + + handleSubmit()} + className={classes.ContactIconContainer} + disabled={!isValid || values.number === ''} + data-testid={`addButton-${initialPhone.personId}`} + > + + + + + + + + {touched.number && Boolean(errors.number) && ( + <> + + + + {errors.number} + + + + )} + + )} + + ); +}; + +export default PhoneValidationForm; diff --git a/src/components/Tool/FixPhoneNumbers/UpdateInvalidPhoneNumbers.graphql b/src/components/Tool/FixPhoneNumbers/UpdateInvalidPhoneNumbers.graphql index 40265174a..993ebedbe 100644 --- a/src/components/Tool/FixPhoneNumbers/UpdateInvalidPhoneNumbers.graphql +++ b/src/components/Tool/FixPhoneNumbers/UpdateInvalidPhoneNumbers.graphql @@ -13,3 +13,15 @@ mutation UpdateInvalidPhoneNumbers($input: PeopleUpdateMutationInput!) { } } } + +mutation UpdatePhoneNumber($input: PersonUpdateMutationInput!) { + updatePerson(input: $input) { + person { + phoneNumbers { + nodes { + ...PersonPhoneNumber + } + } + } + } +} diff --git a/src/components/Tool/FixPhoneNumbers/helper.test.ts b/src/components/Tool/FixPhoneNumbers/helper.test.ts index 0d085cb1a..526541360 100644 --- a/src/components/Tool/FixPhoneNumbers/helper.test.ts +++ b/src/components/Tool/FixPhoneNumbers/helper.test.ts @@ -1,86 +1,67 @@ import { determineBulkDataToSend } from './helper'; -const testData = [ - [ - { - id: 'testid1', - contactId: 'testid1', - firstName: 'Test', - lastName: 'Contact', - avatar: 'https://www.example.com', - isNewPhoneNumber: false, - newPhoneNumber: '', - phoneNumbers: { - nodes: [ - { - id: 'id1', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '+3533895891', - primary: true, - source: 'MPDX', - }, - { - id: 'id2', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '3533895892', - primary: false, - source: 'MPDX', - }, - { - id: 'id3', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '+623533895893', - primary: false, - source: 'MPDX', - }, - ], +const testData1 = { + 0: { + phoneNumbers: [ + { + id: 'id1', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '+3533895891', + primary: true, + source: 'MPDX', }, - }, - ], - [ - { - id: 'testid2', - contactId: 'testid2', - firstName: 'Test', - lastName: 'Contact', - avatar: 'https://www.example.com', - isNewPhoneNumber: false, - newPhoneNumber: '', - phoneNumbers: { - nodes: [ - { - id: 'id1', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '+3533895894', - primary: true, - source: 'DataServer', - }, - { - id: 'id2', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '3533895895', - primary: false, - source: 'DataServer', - }, - { - id: 'id3', - updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), - number: '+623533895896', - primary: false, - source: 'MPDX', - }, - ], + { + id: 'id2', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '3533895892', + primary: false, + source: 'MPDX', + }, + { + id: 'id3', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '+623533895893', + primary: false, + source: 'MPDX', + }, + ], + }, +}; + +const testData2 = { + 1: { + phoneNumbers: [ + { + id: 'id1', + updatedAt: '123', + number: '+3533895894', + primary: true, + source: 'DataServer', + }, + { + id: 'id2', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '3533895895', + primary: false, + source: 'DataServer', + }, + { + id: 'id3', + updatedAt: new Date('2021-06-21T03:40:05-06:00').toISOString(), + number: '+623533895896', + primary: false, + source: 'MPDX', }, - }, - ], -]; + ], + }, +}; describe('FixPhoneNumbers-helper', () => { it('Should return a Contact with all phoneNumbers', () => { - const result = determineBulkDataToSend(testData[0], 'MPDX'); + const result = determineBulkDataToSend(testData1, 'MPDX'); expect(result).toEqual([ { - id: 'testid1', + id: '0', phoneNumbers: [ { id: 'id1', @@ -105,10 +86,10 @@ describe('FixPhoneNumbers-helper', () => { ]); }); it('Should still return a Contact with all phoneNumbers', () => { - const result = determineBulkDataToSend(testData[1], 'MPDX'); + const result = determineBulkDataToSend(testData2, 'MPDX'); expect(result).toEqual([ { - id: 'testid2', + id: '1', phoneNumbers: [ { id: 'id1', @@ -133,7 +114,7 @@ describe('FixPhoneNumbers-helper', () => { ]); }); it('Should return an empty array', () => { - const result = determineBulkDataToSend(testData[0], 'DataServer'); + const result = determineBulkDataToSend(testData1, 'DataServer'); expect(result).toEqual([]); }); }); diff --git a/src/components/Tool/FixPhoneNumbers/helper.ts b/src/components/Tool/FixPhoneNumbers/helper.ts index fd3805e70..6f8eb31e3 100644 --- a/src/components/Tool/FixPhoneNumbers/helper.ts +++ b/src/components/Tool/FixPhoneNumbers/helper.ts @@ -1,27 +1,30 @@ import { PersonUpdateInput } from 'src/graphql/types.generated'; -import { FormValuesPerson } from './FixPhoneNumbers'; +import { PhoneNumberData } from './Contact'; export const determineBulkDataToSend = ( - values: FormValuesPerson[], + dataState: { + [key: string]: PhoneNumberData; + }, defaultSource: string, ): PersonUpdateInput[] => { const dataToSend = [] as PersonUpdateInput[]; - values.forEach((value) => { - const primaryNumber = value.phoneNumbers.nodes.find( + Object.entries(dataState).forEach(([id, data]) => { + const primaryNumber = data.phoneNumbers.find( (number) => number.source === defaultSource, ); if (primaryNumber) { dataToSend.push({ - id: value.id, - phoneNumbers: value.phoneNumbers.nodes.map((number) => ({ - id: number.id, - primary: number.id === primaryNumber.id, - number: number.number, + id, + phoneNumbers: data.phoneNumbers.map((phoneNumber) => ({ + id: phoneNumber.id, + primary: phoneNumber.id === primaryNumber.id, + number: phoneNumber.number, validValues: true, })), }); } }); + return dataToSend; };