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 4 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
44 changes: 40 additions & 4 deletions src/components/Tool/MergeContacts/MergeContacts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ 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 Down Expand Up @@ -97,7 +98,7 @@ describe('Tools - MergeContacts', () => {

userEvent.click(getByRole('button', { name: 'Confirm and Continue' }));
await waitFor(() =>
expect(mockEnqueue).toHaveBeenCalledWith('Success!', {
expect(mockEnqueue).toHaveBeenCalledWith('Updated 1 duplicate(s)', {
variant: 'success',
}),
);
Expand All @@ -121,7 +122,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 +144,38 @@ 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
});

describe('setContactFocus()', () => {
Expand Down
168 changes: 124 additions & 44 deletions src/components/Tool/MergeContacts/MergeContacts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
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';
Expand Down Expand Up @@ -66,20 +68,27 @@
});
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 +100,112 @@
}
};

const mergeContacts = async () => {
const mergeActions = Object.entries(actions).filter(
(action) => action[1].action === 'merge',
);
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}` });
const handleBulkUpdateDuplicates = async () => {
try {
const callsByDuplicate: (() => Promise<{ success: boolean }>)[] = [];

const mergeActions = Object.entries(actions).filter(
(action) => action[1].action === 'merge',
);

if (mergeActions.length) {
const winnersAndLosers: { winnerId: string; loserId: string }[] =
mergeActions.map((action) => {
return { winnerId: action[0], loserId: action[1].mergeId || '' };
});
cache.gc();
},
onCompleted: () => {
enqueueSnackbar(t('Success!'), {

const callMergeDuplicatesMutation = () =>
mergeDuplicates(winnersAndLosers);
callsByDuplicate.push(callMergeDuplicatesMutation);
}

const duplicatesToIgnore = Object.entries(actions)
.filter((action) => action[1].action === 'ignore')
.map((action) => action[0]);

if (duplicatesToIgnore.length) {
duplicatesToIgnore.forEach((duplicateId) => {
const callIgnoreDuplicateMutation = () =>
ignoreDuplicates(duplicateId);
callsByDuplicate.push(callIgnoreDuplicateMutation);
});
}

if (callsByDuplicate.length) {
const results = await Promise.all(
callsByDuplicate.map((call) => call()),
);

const failedUpdates = results.filter(
(result) => !result.success,
).length;
const successfulUpdates = results.length - failedUpdates;

if (successfulUpdates) {
enqueueSnackbar(t(`Updated ${successfulUpdates} duplicate(s)`), {
variant: 'success',
});
}
if (failedUpdates) {
enqueueSnackbar(

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

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/MergeContacts.tsx#L150

Added line #L150 was not covered by tests
t(`Error when updating ${failedUpdates} duplicate(s)`),
{
variant: 'error',
},
);
caleballdrin marked this conversation as resolved.
Show resolved Hide resolved
}
} else {
enqueueSnackbar(t(`No duplicates were updated`), {

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

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/MergeContacts.tsx#L157-L158

Added lines #L157 - L158 were not covered by tests
variant: 'warning',
});
}
} catch (error) {
enqueueSnackbar(t(`Error updating duplicates`), { variant: 'error' });

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

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/MergeContacts.tsx#L163

Added line #L163 was not covered by tests
}
};

const ignoreDuplicates = async (duplicateId: string) => {
await updateDuplicates({
variables: {
input: {
attributes: {
ignore: true,
},
type: TypeEnum.Contact,
id: duplicateId,
},
onError: (err) => {
enqueueSnackbar(t('A server error occurred. {{err}}', { err }), {
variant: 'error',
});
},
update: (cache) => {
// Delete the duplicate
cache.evict({ id: `ContactDuplicate:${duplicateId}` });
cache.gc();
},
onError: () => {
return { success: false };

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

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/MergeContacts.tsx#L183-L184

Added lines #L183 - L184 were not covered by tests
},
});
return { success: true };
};

const mergeDuplicates = async (winnersAndLosers) => {
await contactsMerge({
variables: {
input: {
winnersAndLosers,
},
});
}
},
update: (cache) => {
// Delete the contacts and remove dangling references to them
winnersAndLosers.forEach((contact) => {
cache.evict({ id: `Contact:${contact.loserId}` });
});
cache.gc();
},
onError: () => {
return { success: false };

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

View check run for this annotation

Codecov / codecov/patch

src/components/Tool/MergeContacts/MergeContacts.tsx#L204-L205

Added lines #L204 - L205 were not covered by tests
},
});
return { success: true };
};

return (
Expand Down Expand Up @@ -170,22 +251,21 @@
duplicatesDisplayedCount={duplicatesDisplayedCount}
disabled={disabled}
totalCount={totalCount}
confirmAction={mergeContacts}
confirmAction={handleBulkUpdateDuplicates}
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
10 changes: 9 additions & 1 deletion src/components/Tool/MergePeople/GetPersonDuplicates.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@ mutation MergePeopleBulk($input: MergePeopleBulkInput!) {
mergePeopleBulk(input: $input)
}

mutation UpdateDuplicate($input: DuplicatesUpdateMutationInput!) {
updateDuplicate(input: $input) {
duplicate {
id
}
}
}

query GetPersonDuplicates($accountListId: ID!) {
# TODO: Eventually needs pagination (Jira issue: MPDX-7642)
personDuplicates(accountListId: $accountListId, first: 50) {
personDuplicates(accountListId: $accountListId, ignore: false, first: 10) {
nodes {
id
reason
Expand Down
Loading
Loading