diff --git a/src/components/Tool/FixMailingAddresses/Contact.tsx b/src/components/Tool/FixMailingAddresses/Contact.tsx index adda7cef4..990146302 100644 --- a/src/components/Tool/FixMailingAddresses/Contact.tsx +++ b/src/components/Tool/FixMailingAddresses/Contact.tsx @@ -38,11 +38,10 @@ import { useUpdateCache } from 'src/hooks/useUpdateCache'; import { dateFormatShort } from 'src/lib/intlFormat'; import { contactPartnershipStatus } from 'src/utils/contacts/contactPartnershipStatus'; import theme from '../../../theme'; -import { emptyAddress } from './FixMailingAddresses'; +import { HandleSingleConfirmProps, emptyAddress } from './FixMailingAddresses'; import { ContactAddressFragment } from './GetInvalidAddresses.generated'; const ContactHeader = styled(CardHeader)(() => ({ - cursor: 'pointer', '.MuiCardHeader-action': { alignSelf: 'center', }, @@ -117,6 +116,11 @@ interface Props { openEditAddressModal: (address: ContactAddressFragment, id: string) => void; openNewAddressModal: (address: ContactAddressFragment, id: string) => void; setContactFocus: SetContactFocus; + handleSingleConfirm: ({ + addresses, + id, + name, + }: HandleSingleConfirmProps) => void; } const Contact: React.FC = ({ @@ -128,6 +132,7 @@ const Contact: React.FC = ({ openEditAddressModal, openNewAddressModal, setContactFocus, + handleSingleConfirm, }) => { const { t } = useTranslation(); const locale = useLocale(); @@ -161,6 +166,10 @@ const Contact: React.FC = ({ }); }; + const handleConfirm = () => { + handleSingleConfirm({ addresses, id, name }); + }; + const handleContactNameClick = () => { setContactFocus(id); }; @@ -176,7 +185,11 @@ const Contact: React.FC = ({ /> } action={ - diff --git a/src/components/Tool/FixMailingAddresses/FixMailingAddresses.test.tsx b/src/components/Tool/FixMailingAddresses/FixMailingAddresses.test.tsx index 9cb1362cd..be34724c6 100644 --- a/src/components/Tool/FixMailingAddresses/FixMailingAddresses.test.tsx +++ b/src/components/Tool/FixMailingAddresses/FixMailingAddresses.test.tsx @@ -427,4 +427,230 @@ describe('FixSendNewsletter', () => { expect(setContactFocus).toHaveBeenCalledWith(contactId); }); }); + + describe('handleSingleConfirm()', () => { + const name = 'Baggins, Frodo'; + it('should handle error', async () => { + const { getAllByRole, getByText, queryByTestId } = render( + { + throw new Error('Server Error'); + }, + }} + />, + ); + await waitFor(() => + expect(queryByTestId('loading')).not.toBeInTheDocument(), + ); + userEvent.click(getAllByRole('button', { name: 'Confirm' })[0]); + + await waitFor(() => expect(getByText(name)).toBeInTheDocument()); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + `Error updating contact ${name}`, + { + variant: 'error', + autoHideDuration: 7000, + }, + ); + }); + expect(getByText(name)).toBeInTheDocument(); + }); + + it('should handle success and remove contact', async () => { + const { getAllByRole, getByText, queryByTestId, queryByText } = render( + , + ); + await waitFor(() => + expect(queryByTestId('loading')).not.toBeInTheDocument(), + ); + userEvent.click(getAllByRole('button', { name: 'Confirm' })[0]); + + await waitFor(() => expect(getByText(name)).toBeInTheDocument()); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith(`Updated contact ${name}`, { + variant: 'success', + }); + expect(queryByText(name)).not.toBeInTheDocument(); + }); + }); + }); + + describe('handleBulkConfirm()', () => { + const name1 = 'Baggins, Frodo'; + const name2 = 'Gamgee, Samwise'; + + it('should handle Error', async () => { + process.env.APP_NAME = 'MPDX'; + const { getByRole, getByText, queryByTestId } = render( + { + throw new Error('Server Error'); + }, + }} + />, + ); + await waitFor(() => + expect(queryByTestId('loading')).not.toBeInTheDocument(), + ); + + expect(getByText(name1)).toBeInTheDocument(); + expect(getByText(name2)).toBeInTheDocument(); + + userEvent.click(getByRole('button', { name: 'Confirm 2 as MPDX' })); + + await waitFor(() => + expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(), + ); + + userEvent.click(getByRole('button', { name: 'Yes' })); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith( + `Error updating contact ${name1}`, + { + variant: 'error', + autoHideDuration: 7000, + }, + ); + expect(mockEnqueue).toHaveBeenCalledWith( + `Error updating contact ${name2}`, + { + variant: 'error', + autoHideDuration: 7000, + }, + ); + expect(mockEnqueue).toHaveBeenCalledWith( + `Error when updating 2 contact(s)`, + { + variant: 'error', + }, + ); + + expect(getByText(name1)).toBeInTheDocument(); + expect(getByText(name2)).toBeInTheDocument(); + }); + }); + + it('should handle success and remove contacts', async () => { + process.env.APP_NAME = 'MPDX'; + const { getByRole, queryByTestId, queryByText } = render( + , + ); + await waitFor(() => + expect(queryByTestId('loading')).not.toBeInTheDocument(), + ); + userEvent.click(getByRole('button', { name: 'Confirm 2 as MPDX' })); + + await waitFor(() => + expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(), + ); + + userEvent.click(getByRole('button', { name: 'Yes' })); + + await waitFor(() => { + expect(mockEnqueue).toHaveBeenCalledWith(`Updated contact ${name1}`, { + variant: 'success', + }); + expect(mockEnqueue).toHaveBeenCalledWith(`Updated contact ${name2}`, { + variant: 'success', + }); + + expect(queryByText(name1)).not.toBeInTheDocument(); + expect(queryByText(name2)).not.toBeInTheDocument(); + }); + }); + }); + + it('should not fire handleSingleConfirm', async () => { + process.env.APP_NAME = 'MPDX'; + const { getByRole, queryByTestId, queryByText } = render( + , + ); + await waitFor(() => + expect(queryByTestId('loading')).not.toBeInTheDocument(), + ); + const name = 'Baggins, Frodo'; + userEvent.click(getByRole('combobox')); + userEvent.click(getByRole('option', { name: 'DataServer' })); + + userEvent.click(getByRole('button', { name: 'Confirm 1 as DataServer' })); + + await waitFor(() => + expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(), + ); + + userEvent.click(getByRole('button', { name: 'No' })); + + await waitFor(() => { + expect(mockEnqueue).not.toHaveBeenCalled(); + expect(queryByText(name)).toBeInTheDocument(); + }); + }); }); diff --git a/src/components/Tool/FixMailingAddresses/FixMailingAddresses.tsx b/src/components/Tool/FixMailingAddresses/FixMailingAddresses.tsx index 086bc13ea..5fc6a7cfd 100644 --- a/src/components/Tool/FixMailingAddresses/FixMailingAddresses.tsx +++ b/src/components/Tool/FixMailingAddresses/FixMailingAddresses.tsx @@ -13,11 +13,13 @@ import { SelectChangeEvent, Typography, } from '@mui/material'; +import { useSnackbar } from 'notistack'; import { Trans, useTranslation } from 'react-i18next'; import { makeStyles } from 'tss-react/mui'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; import { DynamicAddAddressModal } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/AddAddressModal/DynamicAddAddressModal'; import { DynamicEditContactAddressModal } from 'src/components/Contacts/ContactDetails/ContactDetailsTab/Mailing/EditContactAddressModal/DynamicEditContactAddressModal'; +import { Confirmation } from 'src/components/common/Modal/Confirmation/Confirmation'; import theme from '../../../theme'; import NoData from '../NoData'; import Contact from './Contact'; @@ -26,8 +28,16 @@ import { InvalidAddressesDocument, InvalidAddressesQuery, useInvalidAddressesQuery, + useUpdateContactAddressMutation, } from './GetInvalidAddresses.generated'; +export type HandleSingleConfirmProps = { + addresses: ContactAddressFragment[]; + id: string; + name: string; + onlyErrorOnce?: boolean; +}; + const useStyles = makeStyles()(() => ({ container: { padding: theme.spacing(3), @@ -131,9 +141,111 @@ const FixSendNewsletter: React.FC = ({ const [selectedAddress, setSelectedAddress] = useState(emptyAddress); const [selectedContactId, setSelectedContactId] = useState(''); const [defaultSource, setDefaultSource] = useState(appName); + const [openBulkConfirmModal, setOpenBulkConfirmModal] = useState(false); + const { data, loading } = useInvalidAddressesQuery({ variables: { accountListId }, }); + const [updateAddress] = useUpdateContactAddressMutation(); + const { enqueueSnackbar } = useSnackbar(); + + const handleSingleConfirm = async ({ + addresses, + id, + name, + }: HandleSingleConfirmProps) => { + let errorOccurred = false; + + for (let idx = 0; idx < addresses.length; idx++) { + const address = addresses[idx]; + + await updateAddress({ + variables: { + accountListId, + attributes: { + id: address.id, + validValues: true, + primaryMailingAddress: address.primaryMailingAddress, + }, + }, + update(cache) { + if (idx === addresses.length - 1 && !errorOccurred) { + cache.evict({ id: `Contact:${id}` }); + } + }, + onError() { + errorOccurred = true; + }, + }); + } + + if (errorOccurred) { + enqueueSnackbar(t(`Error updating contact ${name}`), { + variant: 'error', + autoHideDuration: 7000, + }); + return { success: false }; + } else { + enqueueSnackbar(t(`Updated contact ${name}`), { variant: 'success' }); + return { success: true }; + } + }; + + const handleBulkConfirm = async () => { + try { + const callsByContact: (() => Promise<{ success: boolean }>)[] = []; + data?.contacts?.nodes.forEach((contact) => { + const primaryAddress = contact.addresses.nodes.find( + (address) => + address.source === defaultSource || + (defaultSource === appName && address.source === 'MPDX'), + ); + if (primaryAddress) { + const addresses: ContactAddressFragment[] = []; + contact.addresses.nodes.forEach((address) => { + addresses.push({ + ...address, + primaryMailingAddress: address.id === primaryAddress?.id, + }); + }); + const callContactMutation = () => + handleSingleConfirm({ + addresses, + id: contact.id, + name: contact.name, + }); + callsByContact.push(callContactMutation); + } + }); + + if (callsByContact.length) { + const results = await Promise.all(callsByContact.map((call) => call())); + + const failedUpdates = results.filter( + (result) => !result.success, + ).length; + const successfulUpdates = results.length - failedUpdates; + + if (successfulUpdates) { + enqueueSnackbar(t(`Updated ${successfulUpdates} contact(s)`), { + variant: 'success', + }); + } + if (failedUpdates) { + enqueueSnackbar( + t(`Error when updating ${failedUpdates} contact(s)`), + { + variant: 'error', + }, + ); + } + } else { + enqueueSnackbar(t(`No contacts were updated`), { variant: 'warning' }); + } + } catch (error) { + enqueueSnackbar(t(`Error updating contacts`), { variant: 'error' }); + } + }; const handleUpdateCacheForDeleteAddress = useCallback( (cache: ApolloCache, data) => { @@ -213,6 +325,10 @@ const FixSendNewsletter: React.FC = ({ setDefaultSource(event.target.value); }; + const handleBulkConfirmModalClose = () => { + setOpenBulkConfirmModal(false); + }; + const totalContacts = data?.contacts?.nodes?.length || 0; return ( @@ -273,6 +389,7 @@ const FixSendNewsletter: React.FC = ({