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-7987 Merge Contacts #959

Merged
merged 14 commits into from
Jun 24, 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
3 changes: 3 additions & 0 deletions pages/accountLists/[accountListId]/tools/ToolsWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ interface ToolsWrapperProps {
pageUrl: string;
selectedMenuId: string; // for later use
children: ReactElement<unknown, string | JSXElementConstructor<unknown>>;
styles?: React.ReactNode;
}

export const ToolsWrapper: React.FC<ToolsWrapperProps> = ({
pageTitle,
pageUrl,
children,
styles,
}) => {
const { appName } = useGetAppSettings();
const { accountListId, selectedContactId, handleSelectContact } =
Expand All @@ -29,6 +31,7 @@ export const ToolsWrapper: React.FC<ToolsWrapperProps> = ({
<title>
{appName} | {pageTitle}
</title>
{styles}
</Head>
{accountListId ? (
<SidePanelsLayout
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { useRouter } from 'next/router';
import { ThemeProvider } from '@mui/material/styles';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { getSession } from 'next-auth/react';
import { SnackbarProvider } from 'notistack';
import { I18nextProvider } from 'react-i18next';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { GetContactDuplicatesQuery } from 'src/components/Tool/MergeContacts/GetContactDuplicates.generated';
import { getContactDuplicatesMocks } from 'src/components/Tool/MergeContacts/MergeContactsMock';
import i18n from 'src/lib/i18n';
import theme from 'src/theme';
import MergeContactsPage from './[[...contactId]].page';

jest.mock('next-auth/react');
jest.mock('next/router', () => ({
useRouter: jest.fn(),
}));
jest.mock('src/lib/helpScout', () => ({
suggestArticles: jest.fn(),
}));
jest.mock('notistack', () => ({
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
...jest.requireActual('notistack'),
useSnackbar: () => {
return {
enqueueSnackbar: jest.fn(),
};
},
}));

const pushFn = jest.fn();
const accountListId = 'account-list-1';
const session = {
expires: '2021-10-28T14:48:20.897Z',
user: {
email: 'Chair Library Bed',
image: null,
name: 'Dung Tapestry',
token: 'superLongJwtString',
},
};
const Components = () => (
<ThemeProvider theme={theme}>
<TestRouter>
<GqlMockedProvider<{
GetContactDuplicates: GetContactDuplicatesQuery;
}>
mocks={getContactDuplicatesMocks}
>
<I18nextProvider i18n={i18n}>
<SnackbarProvider>
<MergeContactsPage />
</SnackbarProvider>
</I18nextProvider>
</GqlMockedProvider>
</TestRouter>
</ThemeProvider>
);

describe('MergeContactsPage', () => {
beforeEach(() => {
(getSession as jest.Mock).mockResolvedValue(session);
(useRouter as jest.Mock).mockReturnValue({
query: {
accountListId,
},
isReady: true,
push: pushFn,
});
});

it('should open up contact details', async () => {
const { findByText, queryByTestId } = render(<Components />);
await waitFor(() =>
expect(queryByTestId('loading')).not.toBeInTheDocument(),
);

const contactName = await findByText('Doe, John');

expect(contactName).toBeInTheDocument();
userEvent.click(contactName);

await waitFor(() => {
expect(pushFn).toHaveBeenCalledWith(
`/accountLists/${accountListId}/tools/mergeContacts/${'contact-1'}`,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,14 @@ const MergeContactsPage: React.FC = () => {
pageTitle={t('Merge Contacts')}
pageUrl={pageUrl}
selectedMenuId="mergeContacts"
styles={
<style>{`
div.MuiBox-root {
overflow-x: visible;
overflow-y: visible;
},
`}</style>
}
>
<MergeContacts
accountListId={accountListId || ''}
Expand Down
17 changes: 7 additions & 10 deletions pages/api/Schema/MergeContacts/mergeContacts.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,14 @@ extend type Mutation {
"""
Returns the ids of the winner
"""
mergeContacts(input: MergeContactsInput!): ID!
mergeContacts(input: MergeContactsInput!): [ID!]!
}

input MergeContactsInput {
"""
The ids of the contacts to make the losers of the merge
"""
loserContactIds: [ID!]!
input WinnersAndLosers {
winnerId: ID!
loserId: ID!
}

"""
The id of the contact to make the winner of the merge
"""
winnerContactId: ID!
input MergeContactsInput {
winnersAndLosers: [WinnersAndLosers!]!
}
7 changes: 2 additions & 5 deletions pages/api/Schema/MergeContacts/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ const MergeContactsResolvers: Resolvers = {
Mutation: {
mergeContacts: (
_source,
{ input: { loserContactIds, winnerContactId } },
{ input: { winnersAndLosers } },
{ dataSources },
) => {
return dataSources.mpdxRestApi.mergeContacts(
loserContactIds,
winnerContactId,
);
return dataSources.mpdxRestApi.mergeContacts(winnersAndLosers);
},
},
};
Expand Down
15 changes: 9 additions & 6 deletions pages/api/graphql-rest.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ExportFormatEnum,
ExportLabelTypeEnum,
ExportSortEnum,
MergeContactsInput,
} from 'src/graphql/types.generated';
import schema from './Schema';
import { getAccountListAnalytics } from './Schema/AccountListAnalytics/dataHandler';
Expand Down Expand Up @@ -201,21 +202,23 @@ class MpdxRestApi extends RESTDataSource {
return `${process.env.REST_API_URL}contacts/exports${pathAddition}/${data.id}.${format}`;
}

async mergeContacts(loserContactIds: Array<string>, winnerContactId: string) {
async mergeContacts(
winnersAndLosers: MergeContactsInput['winnersAndLosers'],
) {
const response = await this.post('contacts/merges/bulk', {
data: loserContactIds.map((loserId) => ({
data: winnersAndLosers.map((contact) => ({
data: {
type: 'contacts',
attributes: {
loser_id: loserId,
winner_id: winnerContactId,
loser_id: contact.loserId,
winner_id: contact.winnerId,
},
},
})),
});

// Return the id of the winner
return response[0].data.id;
// Return the id of the winners
return response.map((contact) => contact.data.id);
}

async getAccountListAnalytics(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
mutation MassActionsMerge($loserContactIds: [ID!]!, $winnerContactId: ID!) {
mergeContacts(
input: {
loserContactIds: $loserContactIds
winnerContactId: $winnerContactId
}
)
mutation MassActionsMerge($input: MergeContactsInput!) {
mergeContacts(input: $input)
}

query GetContactsForMerging(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,14 @@ describe('MassActionsMergeModal', () => {
.filter(({ operationName }) => operationName === 'MassActionsMerge');
expect(mergeCalls).toHaveLength(1);
expect(mergeCalls[0].variables).toEqual({
loserContactIds: ['contact-2'],
winnerContactId: 'contact-1',
input: {
winnersAndLosers: [
{
loserId: 'contact-2',
winnerId: 'contact-1',
},
],
},
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,21 @@ export const MassActionsMergeModal: React.FC<MassActionsMergeModalProps> = ({
});

const mergeContacts = async () => {
const loserContactIds = ids.filter((id) => id !== primaryContactId);
const winnersAndLosers = ids
.filter((id) => id !== primaryContactId)
.map((id) => {
return { winnerId: primaryContactId, loserId: id };
});
await contactsMerge({
variables: {
loserContactIds,
winnerContactId: primaryContactId,
input: {
winnersAndLosers,
},
},
update: (cache) => {
// Delete the loser contacts and remove dangling references to them
loserContactIds.forEach((id) => {
cache.evict({ id: `Contact:${id}` });
winnersAndLosers.forEach((contact) => {
cache.evict({ id: `Contact:${contact.loserId}` });
});
cache.gc();
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/GlobalStyles/GlobalStyles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ const useStyles = makeStyles(() =>
'-moz-osx-font-smoothing': 'grayscale',
height: '100%',
width: '100%',
overflow: 'hidden',
},
body: {
height: '100%',
minHeight: '100vh',
width: '100%',
},
'#__next': {
Expand Down
4 changes: 2 additions & 2 deletions src/components/Layouts/Primary/Primary.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { ReactElement, ReactNode, useState } from 'react';
import { Box } from '@mui/material';
import { styled } from '@mui/material/styles';
import { NavBar } from 'src/components/Layouts/Primary/NavBar/NavBar';
import { useAccountListId } from 'src/hooks/useAccountListId';
Expand All @@ -15,10 +16,9 @@ const RootContainer = styled('div')(({ theme }) => ({

const ContentContainer = styled('div')(() => ({
display: 'flex',
overflow: 'hidden',
}));

const Content = styled('div')(() => ({
const Content = styled(Box)(() => ({
flex: '1 1 auto',
height: '100%',
overflow: 'auto',
Expand Down
Loading
Loading