Skip to content

Commit

Permalink
MPDX-8005 Merge People UI, Styling & merge mutations (#966)
Browse files Browse the repository at this point in the history
MergePeople UI, reusable components and mergePeopleBulk proxy mutation
  • Loading branch information
caleballdrin authored Jun 27, 2024
1 parent 4b589bf commit b3e0d9a
Show file tree
Hide file tree
Showing 17 changed files with 711 additions and 592 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ const MergeContactsPage: React.FC = () => {
div.MuiBox-root {
overflow-x: visible;
overflow-y: visible;
},
}
div.MuiBox-root#scrollOverride {
overflow-y: auto;
}
`}</style>
}
>
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 { GetPersonDuplicatesQuery } from 'src/components/Tool/MergePeople/GetPersonDuplicates.generated';
import { getPersonDuplicatesMocks } from 'src/components/Tool/MergePeople/PersonDuplicatesMock';
import i18n from 'src/lib/i18n';
import theme from 'src/theme';
import MergePeoplePage 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<{
GetPersonDuplicates: GetPersonDuplicatesQuery;
}>
mocks={getPersonDuplicatesMocks}
>
<I18nextProvider i18n={i18n}>
<SnackbarProvider>
<MergePeoplePage />
</SnackbarProvider>
</I18nextProvider>
</GqlMockedProvider>
</TestRouter>
</ThemeProvider>
);

describe('MergePeoplePage', () => {
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('John Doe');

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

await waitFor(() => {
expect(pushFn).toHaveBeenCalledWith(
`/accountLists/${accountListId}/tools/mergePeople/${'contact-1'}`,
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@ const MergePeoplePage: React.FC = () => {
pageTitle={t('Merge People')}
pageUrl={pageUrl}
selectedMenuId="mergePeople"
styles={
<style>{`
div.MuiBox-root {
overflow-x: visible;
overflow-y: visible;
}
div.MuiBox-root#scrollOverride {
overflow-y: auto;
}
`}</style>
}
>
<MergePeople
accountListId={accountListId || ''}
Expand Down
7 changes: 7 additions & 0 deletions pages/api/Schema/MergePeople/mergePeopleBulk.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
extend type Mutation {
mergePeopleBulk(input: MergePeopleBulkInput!): [ID!]!
}

input MergePeopleBulkInput {
winnersAndLosers: [WinnersAndLosers!]!
}
15 changes: 15 additions & 0 deletions pages/api/Schema/MergePeople/resolvers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Resolvers } from '../../graphql-rest.page.generated';

const MergePeopleBulkResolvers: Resolvers = {
Mutation: {
mergePeopleBulk: (
_source,
{ input: { winnersAndLosers } },
{ dataSources },
) => {
return dataSources.mpdxRestApi.mergePeopleBulk(winnersAndLosers);
},
},
};

export { MergePeopleBulkResolvers };
3 changes: 3 additions & 0 deletions pages/api/Schema/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import ExportContactsTypeDefs from './ExportContacts/exportContacts.graphql';
import { ExportContactsResolvers } from './ExportContacts/resolvers';
import MergeContactsTypeDefs from './MergeContacts/mergeContacts.graphql';
import { MergeContactsResolvers } from './MergeContacts/resolvers';
import MergePeopleBulkTypeDefs from './MergePeople/mergePeopleBulk.graphql';
import { MergePeopleBulkResolvers } from './MergePeople/resolvers';
import { integrationSchema } from './SubgraphSchema/Integrations';
import { organizationSchema } from './SubgraphSchema/Organizations';
import { preferencesSchema } from './SubgraphSchema/Preferences';
Expand Down Expand Up @@ -76,6 +78,7 @@ const schema = buildSubgraphSchema([
},
{ typeDefs: ExportContactsTypeDefs, resolvers: ExportContactsResolvers },
{ typeDefs: MergeContactsTypeDefs, resolvers: MergeContactsResolvers },
{ typeDefs: MergePeopleBulkTypeDefs, resolvers: MergePeopleBulkResolvers },
{
typeDefs: FourteenMonthReportTypeDefs,
resolvers: FourteenMonthReportResolvers,
Expand Down
18 changes: 18 additions & 0 deletions pages/api/graphql-rest.page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ExportLabelTypeEnum,
ExportSortEnum,
MergeContactsInput,
MergePeopleBulkInput,
} from 'src/graphql/types.generated';
import schema from './Schema';
import { getAccountListAnalytics } from './Schema/AccountListAnalytics/dataHandler';
Expand Down Expand Up @@ -221,6 +222,23 @@ class MpdxRestApi extends RESTDataSource {
return response.map((contact) => contact.data.id);
}

async mergePeopleBulk(
winnersAndLosers: MergePeopleBulkInput['winnersAndLosers'],
) {
const response = await this.post('contacts/people/merges/bulk', {
data: winnersAndLosers.map((person) => ({
data: {
type: 'people',
attributes: {
loser_id: person.loserId,
winner_id: person.winnerId,
},
},
})),
});
return response.map((person) => person.data.id);
}

async getAccountListAnalytics(
accountListId: string,
dateRange?: string | null,
Expand Down
1 change: 1 addition & 0 deletions src/components/Layouts/SidePanelsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export const SidePanelsLayout: FC<SidePanelsLayoutProps> = ({
style={{
transform: rightOpen ? 'none' : 'translate(100%)',
}}
id="scrollOverride"
>
{rightOpen && rightPanel}
</RightPanelWrapper>
Expand Down
109 changes: 81 additions & 28 deletions src/components/Tool/MergeContacts/ContactPair.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { useLocale } from 'src/hooks/useLocale';
import { dateFormatShort } from 'src/lib/intlFormat';
import { contactPartnershipStatus } from 'src/utils/contacts/contactPartnershipStatus';
import theme from '../../../theme';
import { PersonInfoFragment } from '../MergePeople/GetPersonDuplicates.generated';
import { RecordInfoFragment } from './GetContactDuplicates.generated';

const useStyles = makeStyles()(() => ({
Expand Down Expand Up @@ -83,7 +84,7 @@ const ContactAvatar = styled(Avatar)(() => ({
height: theme.spacing(4),
}));
interface ContactItemProps {
contact: RecordInfoFragment;
contact: RecordInfoFragment | PersonInfoFragment;
side: string;
updateState: (side: string) => void;
selected: boolean;
Expand Down Expand Up @@ -145,11 +146,16 @@ const ContactItem: React.FC<ContactItemProps> = ({
},
},
}));
const InlineTypography = styled(Typography)(() => ({
display: 'inline',
}));
const { classes } = useStyles();
const locale = useLocale();
const handleContactNameClick = (contactId) => {
setContactFocus(contactId);
};
const isPersonType = contact.__typename === 'Person';
const isContactType = contact.__typename === 'Contact';
return (
<Card
className={`
Expand All @@ -176,12 +182,22 @@ const ContactItem: React.FC<ContactItemProps> = ({
underline="hover"
onClick={(e) => {
e.stopPropagation();
handleContactNameClick(contact.id);
handleContactNameClick(
isPersonType
? contact.contactId
: isContactType
? contact.id
: null,
);
}}
>
<Typography variant="subtitle1" sx={{ display: 'inline' }}>
{contact.name}
</Typography>
<InlineTypography variant="subtitle1">
{isPersonType
? `${contact.firstName} ${contact.lastName}`
: isContactType
? contact.name
: null}
</InlineTypography>
</Link>{' '}
{selected && (
<Typography variant="body2" className={classes.selected}>
Expand All @@ -191,44 +207,81 @@ const ContactItem: React.FC<ContactItemProps> = ({
</>
}
subheader={
<Typography variant="subtitle2">
{contact.status && contactPartnershipStatus[contact.status]}
</Typography>
isContactType && (
<Typography variant="subtitle2">
{contact?.status && contactPartnershipStatus[contact?.status]}
</Typography>
)
}
className={classes.minimalPadding}
/>
<CardContent className={classes.minimalPadding}>
{contact.primaryAddress && (
{isContactType && contact.primaryAddress && (
<Typography variant="body2">
{`${contact?.primaryAddress?.street}
${contact?.primaryAddress?.city}, ${contact?.primaryAddress?.state} ${contact?.primaryAddress?.postalCode}`}
</Typography>
)}
<Typography variant="body2">
<Trans
defaults="<bold>Source:</bold> {{where}}"
shouldUnescape
values={{ where: contact.source }}
components={{ bold: <strong /> }}
/>
</Typography>
<Typography
variant="body2"
sx={{ fontWeight: 'bold', display: 'inline' }}
>
{t('Created:')}{' '}
</Typography>
<Typography variant="body2" sx={{ display: 'inline' }}>
{dateFormatShort(DateTime.fromISO(contact.createdAt), locale)}
</Typography>
{isContactType && (
<Typography variant="body2">
<Trans
defaults="<bold>Source:</bold> {{where}}"
shouldUnescape
values={{ where: contact.source }}
components={{ bold: <strong /> }}
/>
</Typography>
)}
{isPersonType && contact.primaryPhoneNumber && (
<Box>
<InlineTypography variant="body2">
{`${contact?.primaryPhoneNumber?.number}`}
</InlineTypography>
<Tooltip title="Source" arrow placement="right">
<InlineTypography variant="body2">
<Trans
defaults=" ({{source}})"
shouldUnescape
values={{ source: contact.primaryPhoneNumber?.source }}
components={{ bold: <strong /> }}
/>
</InlineTypography>
</Tooltip>
</Box>
)}
{isPersonType && contact.primaryEmailAddress && (
<Box>
<InlineTypography variant="body2">
{`${contact?.primaryEmailAddress?.email}`}
</InlineTypography>
<Tooltip title="Source" arrow placement="right">
<InlineTypography variant="body2">
<Trans
defaults=" ({{where}})"
shouldUnescape
values={{ where: contact.primaryEmailAddress?.source }}
components={{ bold: <strong /> }}
/>
</InlineTypography>
</Tooltip>
</Box>
)}
<Box>
<InlineTypography variant="body2" sx={{ fontWeight: 'bold' }}>
{t('Created:')}{' '}
</InlineTypography>
<InlineTypography variant="body2">
{dateFormatShort(DateTime.fromISO(contact.createdAt), locale)}
</InlineTypography>
</Box>
</CardContent>
</Card>
);
};

interface Props {
contact1: RecordInfoFragment;
contact2: RecordInfoFragment;
contact1: RecordInfoFragment | PersonInfoFragment;
contact2: RecordInfoFragment | PersonInfoFragment;
update: (id1: string, id2: string, action: string) => void;
updating: boolean;
setContactFocus: SetContactFocus;
Expand Down
10 changes: 7 additions & 3 deletions src/components/Tool/MergeContacts/MergeContacts.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,9 +91,11 @@ describe('Tools - MergeContacts', () => {
expect(confirmButton).toBeDisabled();
userEvent.click(getByText('123 John St Orlando, FL 32832'));
expect(await findByText('Use this one')).toBeInTheDocument();
expect(confirmButton).not.toBeDisabled();
expect(
getByRole('button', { name: 'Confirm and Continue' }),
).not.toBeDisabled();

userEvent.click(confirmButton);
userEvent.click(getByRole('button', { name: 'Confirm and Continue' }));
await waitFor(() =>
expect(mockEnqueue).toHaveBeenCalledWith('Success!', {
variant: 'success',
Expand Down Expand Up @@ -135,7 +137,9 @@ describe('Tools - MergeContacts', () => {
expect(await findByText('Use this one')).toBeInTheDocument();
userEvent.click(queryAllByTestId('ignoreButton')[0]);
expect(queryByText('Use this one')).not.toBeInTheDocument();
expect(confirmButton).not.toBeDisabled();
expect(
getByRole('button', { name: 'Confirm and Continue' }),
).not.toBeDisabled();
});

describe('setContactFocus()', () => {
Expand Down
Loading

0 comments on commit b3e0d9a

Please sign in to comment.