Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MPDX-8008 Ignore duplicates for Merge Contact & Merge People #970

Merged
merged 6 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions src/components/Tool/MergeContacts/ContactPair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -282,14 +282,21 @@
interface Props {
contact1: RecordInfoFragment | PersonInfoFragment;
contact2: RecordInfoFragment | PersonInfoFragment;
update: (id1: string, id2: string, action: string) => void;
update: (
id1: string,
id2: string,
duplicateId: string,
action: string,
) => void;
updating: boolean;
setContactFocus: SetContactFocus;
duplicateId: string;
}

const ContactPair: React.FC<Props> = ({
contact1,
contact2,
duplicateId,
update,
updating,
setContactFocus,
Expand All @@ -306,19 +313,19 @@
switch (side) {
case 'left':
setSelected('left');
update(contact1.id, contact2.id, 'merge');
update(contact1.id, contact2.id, duplicateId, 'merge');
break;
case 'right':
setSelected('right');
update(contact2.id, contact1.id, 'merge');
update(contact2.id, contact1.id, duplicateId, 'merge');
break;
case 'cancel':
setSelected('cancel');
update(contact1.id, contact2.id, 'cancel');
case 'ignore':
setSelected('ignore');
update(contact1.id, contact2.id, duplicateId, 'ignore');
break;
default:
setSelected('');
update(contact1.id, contact2.id, 'cancel');
update(contact1.id, contact2.id, duplicateId, 'ignore');

Check warning on line 328 in src/components/Tool/MergeContacts/ContactPair.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/ContactPair.tsx#L328

Added line #L328 was not covered by tests
}
}
};
Expand Down Expand Up @@ -382,9 +389,9 @@
</Tooltip>
<Tooltip title={t('Ignore this Duplicate')} arrow>
<IconButton
onClick={() => updateState('cancel')}
onClick={() => updateState('ignore')}
className={
selected === 'cancel' ? classes.red : classes.grey
selected === 'ignore' ? classes.red : classes.grey
}
data-testid="ignoreButton"
>
Expand Down
109 changes: 101 additions & 8 deletions src/components/Tool/MergeContacts/MergeContacts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import React from 'react';
import { ThemeProvider } from '@mui/material/styles';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ApolloErgonoMockMap } from 'graphql-ergonomock';
import { SnackbarProvider } from 'notistack';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { ContactsProvider } from 'src/components/Contacts/ContactsContext/ContactsContext';
import { TypeEnum } from 'src/graphql/types.generated';
import theme from 'src/theme';
import { GetContactDuplicatesQuery } from './GetContactDuplicates.generated';
import MergeContacts from './MergeContacts';
Expand All @@ -29,18 +31,20 @@ jest.mock('notistack', () => ({

interface MergeContactsWrapperProps {
mutationSpy?: () => void;
mocks?: ApolloErgonoMockMap;
}

const MergeContactsWrapper: React.FC<MergeContactsWrapperProps> = ({
mutationSpy,
mocks = getContactDuplicatesMocks,
}) => {
return (
<ThemeProvider theme={theme}>
<TestRouter>
<GqlMockedProvider<{
GetContactDuplicates: GetContactDuplicatesQuery;
}>
mocks={getContactDuplicatesMocks}
mocks={mocks}
onCall={mutationSpy}
>
<ContactsProvider
Expand Down Expand Up @@ -79,7 +83,16 @@ describe('Tools - MergeContacts', () => {

const { getByText, queryAllByTestId, findByText, getByRole } = render(
<SnackbarProvider>
<MergeContactsWrapper mutationSpy={mutationSpy} />
<MergeContactsWrapper
mutationSpy={mutationSpy}
mocks={{
GetContactDuplicates:
getContactDuplicatesMocks.GetContactDuplicates,
MassActionsMerge: () => {
return { mergeContacts: ['contact-2'] };
},
}}
/>
</SnackbarProvider>,
);

Expand All @@ -89,15 +102,24 @@ describe('Tools - MergeContacts', () => {
const confirmButton = getByRole('button', { name: 'Confirm and Continue' });

expect(confirmButton).toBeDisabled();
userEvent.click(getByText('123 John St Orlando, FL 32832'));
userEvent.click(queryAllByTestId('ignoreButton')[1]);
userEvent.click(
getByText('(contact-2 address) 123 John St Orlando, FL 32832'),
);
userEvent.click(
getByText('(contact-1 address) 123 Main St Orlando, FL 32832'),
);
userEvent.click(
getByText('(contact-2 address) 123 John St Orlando, FL 32832'),
);
expect(await findByText('Use this one')).toBeInTheDocument();
expect(
getByRole('button', { name: 'Confirm and Continue' }),
).not.toBeDisabled();

userEvent.click(getByRole('button', { name: 'Confirm and Continue' }));
await waitFor(() =>
expect(mockEnqueue).toHaveBeenCalledWith('Success!', {
expect(mockEnqueue).toHaveBeenCalledWith('Updated 2 duplicate(s)', {
variant: 'success',
}),
);
Expand All @@ -110,8 +132,8 @@ describe('Tools - MergeContacts', () => {
input: {
winnersAndLosers: [
{
loserId: 'contact-1',
winnerId: 'contact-2',
loserId: 'contact-1',
},
],
},
Expand All @@ -121,7 +143,14 @@ describe('Tools - MergeContacts', () => {
it('should ignore contacts', async () => {
const mutationSpy = jest.fn();

const { queryByText, queryAllByTestId, findByText, getByRole } = render(
const {
queryByText,
queryByTestId,
queryAllByTestId,
findByText,
getByText,
getByRole,
} = render(
<SnackbarProvider>
<MergeContactsWrapper mutationSpy={mutationSpy} />
</SnackbarProvider>,
Expand All @@ -136,10 +165,74 @@ describe('Tools - MergeContacts', () => {
userEvent.click(queryAllByTestId('rightButton')[0]);
expect(await findByText('Use this one')).toBeInTheDocument();
userEvent.click(queryAllByTestId('ignoreButton')[0]);
userEvent.click(queryAllByTestId('ignoreButton')[1]);
expect(queryByText('Use this one')).not.toBeInTheDocument();

userEvent.click(getByRole('button', { name: 'Confirm and Continue' }));
await waitFor(() =>
expect(mockEnqueue).toHaveBeenCalledWith('Updated 2 duplicate(s)', {
variant: 'success',
}),
);

const mergeCalls = mutationSpy.mock.calls
.map(([{ operation }]) => operation)
.filter(({ operationName }) => operationName === 'MassActionsMerge');
expect(mergeCalls).toHaveLength(0);

const ignoreCalls = mutationSpy.mock.calls
.map(([{ operation }]) => operation)
.filter(({ operationName }) => operationName === 'UpdateDuplicate');
expect(ignoreCalls).toHaveLength(2);
expect(ignoreCalls[0].variables).toEqual({
input: {
attributes: {
ignore: true,
},
type: TypeEnum.Contact,
id: '1',
},
});
expect(queryByTestId('ignoreButton')).not.toBeInTheDocument();
expect(
getByRole('button', { name: 'Confirm and Continue' }),
).not.toBeDisabled();
getByText('No duplicate contacts need attention'),
).toBeInTheDocument();
caleballdrin marked this conversation as resolved.
Show resolved Hide resolved
});

it('should show error', async () => {
const mutationSpy = jest.fn();

const { queryAllByTestId, getByRole } = render(
<SnackbarProvider>
<MergeContactsWrapper
mutationSpy={mutationSpy}
mocks={{
GetContactDuplicates:
getContactDuplicatesMocks.GetContactDuplicates,
UpdateDuplicate: () => {
throw new Error('Server Error');
},
MassActionsMerge: () => {
throw new Error('Server Error');
},
}}
/>
</SnackbarProvider>,
);
await waitFor(() =>
expect(queryAllByTestId('MergeContactPair')).toHaveLength(2),
);
userEvent.click(queryAllByTestId('ignoreButton')[0]);

userEvent.click(getByRole('button', { name: 'Confirm and Continue' }));
await waitFor(() =>
expect(mockEnqueue).toHaveBeenCalledWith(
'Failed to update 1 duplicate(s)',
{
variant: 'error',
},
),
);
});

describe('setContactFocus()', () => {
Expand Down
80 changes: 32 additions & 48 deletions src/components/Tool/MergeContacts/MergeContacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,15 @@ import { Trans, useTranslation } from 'react-i18next';
import { makeStyles } from 'tss-react/mui';
import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper';
import { useMassActionsMergeMutation } from 'src/components/Contacts/MassActions/Merge/MassActionsMerge.generated';
import { TypeEnum } from 'src/graphql/types.generated';
import useGetAppSettings from 'src/hooks/useGetAppSettings';
import theme from '../../../theme';
import { useUpdateDuplicateMutation } from '../MergePeople/GetPersonDuplicates.generated';
import NoData from '../NoData';
import ContactPair from './ContactPair';
import { useGetContactDuplicatesQuery } from './GetContactDuplicates.generated';
import { StickyConfirmButtons } from './StickyConfirmButtons';
import { bulkUpdateDuplicates } from './mergeDuplicatesHelper';

const useStyles = makeStyles()(() => ({
container: {
Expand Down Expand Up @@ -66,20 +69,27 @@ const MergeContacts: React.FC<Props> = ({
});
const { appName } = useGetAppSettings();
const [contactsMerge, { loading: updating }] = useMassActionsMergeMutation();
const [updateDuplicates] = useUpdateDuplicateMutation();
const disabled = useMemo(
() => updating || !Object.entries(actions).length,
[actions, updating],
);
const totalCount = data?.contactDuplicates.totalCount || 0;
const duplicatesDisplayedCount = data?.contactDuplicates.nodes.length || 0;

const updateActions = (id1: string, id2: string, action: string): void => {
const updateActions = (
id1: string,
id2: string,
duplicateId: string,
action: string,
): void => {
if (!updating) {
if (action === 'cancel') {
if (action === 'ignore') {
setActions((prevState) => ({
...prevState,
[id1]: { action: '' },
[id2]: { action: '' },
[duplicateId]: { action: 'ignore' },
}));
} else {
setActions((prevState) => ({
Expand All @@ -91,40 +101,15 @@ const MergeContacts: React.FC<Props> = ({
}
};

const mergeContacts = async () => {
const mergeActions = Object.entries(actions).filter(
(action) => action[1].action === 'merge',
const handleSubmit = () => {
bulkUpdateDuplicates(
TypeEnum.Contact,
actions,
contactsMerge,
updateDuplicates,
enqueueSnackbar,
t,
);
if (mergeActions.length) {
const winnersAndLosers: { winnerId: string; loserId: string }[] =
mergeActions.map((action) => {
return { winnerId: action[0], loserId: action[1].mergeId || '' };
});
await contactsMerge({
variables: {
input: {
winnersAndLosers,
},
},
update: (cache) => {
// Delete the loser contacts and remove dangling references to them
winnersAndLosers.forEach((contact) => {
cache.evict({ id: `Contact:${contact.loserId}` });
});
cache.gc();
},
onCompleted: () => {
enqueueSnackbar(t('Success!'), {
variant: 'success',
});
},
onError: (err) => {
enqueueSnackbar(t('A server error occurred. {{err}}', { err }), {
variant: 'error',
});
},
});
}
};

return (
Expand Down Expand Up @@ -170,22 +155,21 @@ const MergeContacts: React.FC<Props> = ({
duplicatesDisplayedCount={duplicatesDisplayedCount}
disabled={disabled}
totalCount={totalCount}
confirmAction={mergeContacts}
confirmAction={handleSubmit}
setActions={setActions}
/>
<Grid item xs={12} sx={{ margin: '0px 2px 20px 2px' }}>
{data?.contactDuplicates.nodes
.map((duplicate) => (
<ContactPair
key={duplicate.id}
contact1={duplicate.recordOne}
contact2={duplicate.recordTwo}
update={updateActions}
updating={updating}
setContactFocus={setContactFocus}
/>
))
.reverse()}
{data?.contactDuplicates.nodes.map((duplicate) => (
<ContactPair
key={duplicate.id}
duplicateId={duplicate.id}
contact1={duplicate.recordOne}
contact2={duplicate.recordTwo}
update={updateActions}
updating={updating}
setContactFocus={setContactFocus}
/>
))}
</Grid>
</>
) : (
Expand Down
4 changes: 2 additions & 2 deletions src/components/Tool/MergeContacts/MergeContactsMock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export const getContactDuplicatesMocks = {
status: null,
primaryAddress: {
id: 'address-1',
street: '123 Main St',
street: '(contact-1 address) 123 Main St',
city: 'Orlando',
state: 'FL',
postalCode: '32832',
Expand All @@ -30,7 +30,7 @@ export const getContactDuplicatesMocks = {
status: null,
primaryAddress: {
id: 'address-1',
street: '123 John St',
street: '(contact-2 address) 123 John St',
city: 'Orlando',
state: 'FL',
postalCode: '32832',
Expand Down
Loading
Loading