Skip to content

Commit

Permalink
MPDX-7987 Merge Contacts (#959)
Browse files Browse the repository at this point in the history
Add mergeContacts rest proxy, UI and Styling Changes and code refactoring
  • Loading branch information
caleballdrin authored Jun 24, 2024
1 parent f64dd8a commit 4b589bf
Show file tree
Hide file tree
Showing 17 changed files with 892 additions and 479 deletions.
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

0 comments on commit 4b589bf

Please sign in to comment.