diff --git a/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql index e6fabd887c..c0eedcfce0 100644 --- a/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql +++ b/src/components/Tool/FixEmailAddresses/AddEmailAddress.graphql @@ -3,7 +3,7 @@ mutation EmailAddresses($input: PersonUpdateMutationInput!) { person { emailAddresses { nodes { - email + ...PersonEmailAddress } } } diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx deleted file mode 100644 index 9caf1caa82..0000000000 --- a/src/components/Tool/FixEmailAddresses/DeleteModal.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react'; -import { ThemeProvider } from '@mui/material/styles'; -import TestWrapper from '__tests__/util/TestWrapper'; -import { render } from '__tests__/util/testingLibraryReactMock'; -import theme from '../../../theme'; -import DeleteModal from './DeleteModal'; - -const testState = { - open: true, - personId: '', - emailIndex: 0, - emailAddress: 'test@test.com', -}; - -describe('FixEmailAddresses-DeleteModal', () => { - it('default', () => { - const handleClose = jest.fn(); - const handleDelete = jest.fn(); - - const { getByText } = render( - - - - - , - ); - - expect(getByText('Confirm')).toBeInTheDocument(); - expect( - getByText('Are you sure you wish to delete this email address:'), - ).toBeInTheDocument(); - expect(getByText('"test@test.com"')).toBeInTheDocument(); - expect(getByText('Cancel')).toBeInTheDocument(); - expect(getByText('Delete')).toBeInTheDocument(); - }); -}); diff --git a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx b/src/components/Tool/FixEmailAddresses/DeleteModal.tsx deleted file mode 100644 index c07cf09779..0000000000 --- a/src/components/Tool/FixEmailAddresses/DeleteModal.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import { mdiCloseThick } from '@mdi/js'; -import Icon from '@mdi/react'; -import { - Box, - Card, - CardActions, - CardContent, - CardHeader, - IconButton, - Modal, - Theme, - Typography, -} from '@mui/material'; -import { useTranslation } from 'react-i18next'; -import { makeStyles } from 'tss-react/mui'; -import { - CancelButton, - DeleteButton, -} from 'src/components/common/Modal/ActionButtons/ActionButtons'; -import theme from '../../../theme'; -import { ModalState } from './FixEmailAddresses'; - -const useStyles = makeStyles()((theme: Theme) => ({ - modal: { - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - overflow: 'auto', - }, - blue: { - color: 'white', - backgroundColor: theme.palette.mpdxBlue.main, - }, - iconButton: { - position: 'absolute', - top: -theme.spacing(2), - right: -theme.spacing(2), - '&:hover': { - color: 'red', - }, - }, - headerBox: { - padding: 0, - marginBottom: -theme.spacing(1), - position: 'relative', - }, -})); - -interface Props { - modalState: ModalState; - handleClose: () => void; - handleDelete: () => void; -} - -const DeleteModal: React.FC = ({ - modalState, - handleClose, - handleDelete, -}) => { - const { classes } = useStyles(); - const { t } = useTranslation(); - return ( - - - - - {t('Confirm')} - - - - - - } - /> - - - - {t('Are you sure you wish to delete this email address:')} - - {`"${modalState.emailAddress}"`} - - - - - - {/*TODO: make "newAddress" field in address false so it says "edit" instead of "add" */} - {t('Delete')} - - - - - ); -}; - -export default DeleteModal; diff --git a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx index 98636c499c..0ef12d7970 100644 --- a/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx +++ b/src/components/Tool/FixEmailAddresses/EmailValidationForm.tsx @@ -8,7 +8,7 @@ import { AddIcon } from 'src/components/Contacts/ContactDetails/ContactDetailsTa import { useAccountListId } from 'src/hooks/useAccountListId'; import i18n from 'src/lib/i18n'; import { useEmailAddressesMutation } from './AddEmailAddress.generated'; -import { RowWrapper } from './FixEmailAddressPerson'; +import { RowWrapper } from './FixEmailAddressPerson/FixEmailAddressPerson'; import { GetInvalidEmailAddressesDocument, GetInvalidEmailAddressesQuery, @@ -30,7 +30,6 @@ interface EmailValidationFormEmail { } interface EmailValidationFormProps { - index: number; personId: string; } diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx deleted file mode 100644 index e76b9c00aa..0000000000 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.test.tsx +++ /dev/null @@ -1,208 +0,0 @@ -import React from 'react'; -import { ThemeProvider } from '@mui/material/styles'; -import userEvent from '@testing-library/user-event'; -import { ApolloErgonoMockMap } from 'graphql-ergonomock'; -import { DateTime } from 'luxon'; -import TestWrapper from '__tests__/util/TestWrapper'; -import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; -import { render, waitFor } from '__tests__/util/testingLibraryReactMock'; -import { PersonEmailAddressInput } from 'src/graphql/types.generated'; -import theme from '../../../theme'; -import { EmailAddressesMutation } from './AddEmailAddress.generated'; -import { - FixEmailAddressPerson, - FixEmailAddressPersonProps, -} from './FixEmailAddressPerson'; -import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; -import { GetInvalidEmailAddressesQuery } from './FixEmailAddresses.generated'; -import { mockInvalidEmailAddressesResponse } from './FixEmailAddressesMocks'; - -const testData = { - name: 'Test Contact', - personId: 'testid', - contactId: 'contactTestId', - emailAddresses: [ - { - source: 'DonorHub', - updatedAt: DateTime.fromISO('2021-06-21').toString(), - email: 'test1@test1.com', - primary: true, - isValid: false, - personId: 'testid', - } as EmailAddressData, - { - source: 'MPDX', - updatedAt: DateTime.fromISO('2021-06-22').toString(), - email: 'test2@test1.com', - primary: false, - isValid: false, - personId: 'testid', - } as EmailAddressData, - ], -} as FixEmailAddressPersonProps; - -const setContactFocus = jest.fn(); -const handleChangeMock = jest.fn(); -const handleDeleteModalOpenMock = jest.fn(); -const handleChangePrimaryMock = jest.fn(); - -const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { - const toDelete = [] as PersonEmailAddressInput[]; - const dataState = { - id: { - emailAddresses: testData.emailAddresses as EmailAddressData[], - toDelete, - }, - } as { [key: string]: PersonEmailAddresses }; - - return ( - - - - mocks={mocks} - > - - - - - ); -}; - -describe('FixEmailAddressPerson', () => { - it('default', () => { - const { getByText, getByTestId, getByDisplayValue } = render( - , - ); - - expect(getByText(testData.name)).toBeInTheDocument(); - expect(getByText('DonorHub (6/21/2021)')).toBeInTheDocument(); - expect(getByTestId('textfield-testid-0')).toBeInTheDocument(); - expect(getByDisplayValue('test1@test1.com')).toBeInTheDocument(); - expect(getByText('MPDX (6/22/2021)')).toBeInTheDocument(); - expect(getByTestId('textfield-testid-1')).toBeInTheDocument(); - expect(getByDisplayValue('test2@test1.com')).toBeInTheDocument(); - expect(getByTestId('starIcon-testid-0')).toBeInTheDocument(); - }); - - it('input reset after adding an email address', async () => { - const { getByTestId, getByLabelText } = render( - , - ); - - const addInput = getByLabelText('New Email Address'); - const addButton = getByTestId('addButton-testid'); - - userEvent.type(addInput, 'new@new.com'); - await waitFor(() => { - expect(addInput).toHaveValue('new@new.com'); - }); - userEvent.click(addButton); - await waitFor(() => { - expect(addInput).toHaveValue(''); - }); - }); - - describe('validation', () => { - it('should show an error message if there is no email', async () => { - const { getByLabelText, getByTestId, getByText } = render( - , - ); - - const addInput = getByLabelText('New Email Address'); - userEvent.click(addInput); - userEvent.tab(); - - const addButton = getByTestId('addButton-testid'); - await waitFor(() => { - expect(addButton).toBeDisabled(); - expect(getByText('Please enter a valid email address')).toBeVisible(); - }); - }); - - it('should show an error message if there is an invalid email', async () => { - const { getByLabelText, getByTestId, getByText } = render( - , - ); - - const addInput = getByLabelText('New Email Address'); - userEvent.type(addInput, 'ab'); - userEvent.tab(); - - const addButton = getByTestId('addButton-testid'); - await waitFor(() => { - expect(addButton).toBeDisabled(); - expect(getByText('Invalid Email Address Format')).toBeVisible(); - }); - }); - - it('should not disable the add button', async () => { - const { getByLabelText, getByTestId } = render( - , - ); - - const addInput = getByLabelText('New Email Address'); - userEvent.type(addInput, 'new@new.com'); - userEvent.tab(); - - const addButton = getByTestId('addButton-testid'); - await waitFor(() => { - expect(addButton).not.toBeDisabled(); - }); - }); - }); -}); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx deleted file mode 100644 index 7cb3b27ce7..0000000000 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, { Fragment, useMemo } from 'react'; -import { mdiDelete, mdiLock, mdiStar, mdiStarOutline } from '@mdi/js'; -import { Icon } from '@mdi/react'; -import { - Avatar, - Box, - Button, - Grid, - Hidden, - Link, - TextField, - Theme, - Typography, -} from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { DateTime } from 'luxon'; -import { useTranslation } from 'react-i18next'; -import { makeStyles } from 'tss-react/mui'; -import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; -import { PersonEmailAddressInput } from 'src/graphql/types.generated'; -import { useLocale } from 'src/hooks/useLocale'; -import { dateFormatShort } from 'src/lib/intlFormat'; -import theme from '../../../theme'; -import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; -import EmailValidationForm from './EmailValidationForm'; -import { EmailAddressData, PersonEmailAddresses } from './FixEmailAddresses'; - -const PersonCard = styled(Box)(({ theme }) => ({ - [theme.breakpoints.up('md')]: { - border: `1px solid ${theme.palette.cruGrayMedium.main}`, - }, -})); - -const Container = styled(Grid)(({ theme }) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-end', - [theme.breakpoints.down('sm')]: { - border: `1px solid ${theme.palette.cruGrayMedium.main}`, - }, -})); - -const EmailAddressListWrapper = styled(Grid)(({ theme }) => ({ - backgroundColor: theme.palette.cruGrayLight.main, - width: '100%', - [theme.breakpoints.down('xs')]: { - paddingTop: theme.spacing(2), - }, -})); - -const ConfirmButtonWrapper = styled(Box)(({ theme }) => ({ - marginLeft: theme.spacing(2), - [theme.breakpoints.down('sm')]: { - marginLeft: theme.spacing(1), - marginRight: theme.spacing(2), - marginTop: theme.spacing(2), - marginBottom: theme.spacing(2), - }, - '& .MuiButton-root': { - backgroundColor: theme.palette.mpdxBlue.main, - color: 'white', - }, -})); - -const BoxWithResponsiveBorder = styled(Box)(({ theme }) => ({ - [theme.breakpoints.down('xs')]: { - paddingBottom: theme.spacing(2), - borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`, - }, -})); - -const ColumnHeaderWrapper = styled(Grid)(({ theme }) => ({ - paddingTop: theme.spacing(2), - paddingBottom: theme.spacing(2), -})); - -export const RowWrapper = styled(Grid)(({ theme }) => ({ - paddingBottom: theme.spacing(2), -})); - -const HoverableIcon = styled(Icon)(({ theme }) => ({ - '&:hover': { - color: theme.palette.mpdxBlue.main, - cursor: 'pointer', - }, -})); - -const useStyles = makeStyles()((theme: Theme) => ({ - avatar: { - width: theme.spacing(7), - height: theme.spacing(7), - }, -})); - -export interface FixEmailAddressPersonProps { - name: string; - emailAddresses?: EmailAddressData[]; - personId: string; - dataState: { [key: string]: PersonEmailAddresses }; - toDelete: PersonEmailAddressInput[]; - contactId: string; - handleChange: ( - personId: string, - numberIndex: number, - event: React.ChangeEvent, - ) => void; - handleDelete: (personId: string, emailAddress: number) => void; - handleChangePrimary: (personId: string, emailIndex: number) => void; - setContactFocus: SetContactFocus; -} - -export const FixEmailAddressPerson: React.FC = ({ - name, - emailAddresses, - personId, - dataState, - contactId, - handleChange, - handleDelete, - handleChangePrimary, - setContactFocus, -}) => { - const { t } = useTranslation(); - const locale = useLocale(); - const { classes } = useStyles(); - - const emails = useMemo( - () => - emailAddresses?.map((email) => ({ - ...email, - isValid: false, - personId: personId, - isPrimary: email.primary, - })) || [], - [emailAddresses, dataState], - ); - - const handleContactNameClick = () => { - setContactFocus(contactId); - }; - - return ( - - - - - - - - - - - {name} - - - - - - - - - - - {t('Source')} - - - {t('Primary')} - - - - - - - {t('Address')} - - - - - {emails.map((email, index) => ( - - - - - - - {t('Source')}: - - - - {`${email.source} (${dateFormatShort( - DateTime.fromISO(email.updatedAt), - locale, - )})`} - - - {email.isPrimary ? ( - - - - ) : ( - - handleChangePrimary(personId, index) - } - > - - - )} - - - - - , - ) => handleChange(personId, index, event)} - value={email.email} - disabled={email.source !== 'MPDX'} - /> - - {email.source === 'MPDX' ? ( - handleDelete(personId, index)} - > - - - ) : ( - - )} - - - - ))} - - - - - - {t('Source')}: - - - MPDX - - - - - - { - //TODO: index will need to be mapped to the correct personId - } - - - - - - - - - - - - - - - - - - ); -}; diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx new file mode 100644 index 0000000000..74316430cf --- /dev/null +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.test.tsx @@ -0,0 +1,276 @@ +import React from 'react'; +import { ThemeProvider } from '@mui/material/styles'; +import userEvent from '@testing-library/user-event'; +import { ApolloErgonoMockMap } from 'graphql-ergonomock'; +import { DateTime } from 'luxon'; +import { SnackbarProvider } from 'notistack'; +import TestWrapper from '__tests__/util/TestWrapper'; +import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; +import { + render, + screen, + waitFor, +} from '__tests__/util/testingLibraryReactMock'; +import theme from 'src/theme'; +import { EmailAddressesMutation } from '../AddEmailAddress.generated'; +import { EmailAddressData, PersonEmailAddresses } from '../FixEmailAddresses'; +import { + GetInvalidEmailAddressesQuery, + PersonInvalidEmailFragment, +} from '../FixEmailAddresses.generated'; +import { mockInvalidEmailAddressesResponse } from '../FixEmailAddressesMocks'; +import { FixEmailAddressPerson } from './FixEmailAddressPerson'; + +const accountListId = 'accountListId'; +const person: PersonInvalidEmailFragment = { + id: 'contactTestId', + firstName: 'Test', + lastName: 'Contact', + contactId: 'contactTestId', + emailAddresses: { + nodes: [ + { + id: 'email1', + source: 'DonorHub', + updatedAt: DateTime.fromISO('2021-06-21').toString(), + email: 'test1@test1.com', + primary: true, + }, + { + id: 'email2', + source: 'MPDX', + updatedAt: DateTime.fromISO('2021-06-22').toString(), + email: 'test2@test1.com', + primary: false, + }, + ], + }, +}; + +const setContactFocus = jest.fn(); +const mutationSpy = jest.fn(); +const mockEnqueue = jest.fn(); + +jest.mock('notistack', () => ({ + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + ...jest.requireActual('notistack'), + useSnackbar: () => { + return { + enqueueSnackbar: mockEnqueue, + }; + }, +})); + +const TestComponent = ({ mocks }: { mocks: ApolloErgonoMockMap }) => { + const handleChangeMock = jest.fn(); + const handleChangePrimaryMock = jest.fn(); + const dataState = { + contactTestId: { + emailAddresses: person.emailAddresses.nodes as EmailAddressData[], + }, + } as { [key: string]: PersonEmailAddresses }; + + return ( + + + + + mocks={mocks} + onCall={mutationSpy} + > + + + + + + ); +}; + +describe('FixEmailAddressPerson', () => { + it('default', () => { + const { getByText, getByTestId, getByDisplayValue } = render( + , + ); + + expect( + getByText(`${person.firstName} ${person.lastName}`), + ).toBeInTheDocument(); + expect(getByText('DonorHub (6/21/2021)')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-0')).toBeInTheDocument(); + expect(getByDisplayValue('test1@test1.com')).toBeInTheDocument(); + expect(getByText('MPDX (6/22/2021)')).toBeInTheDocument(); + expect(getByTestId('textfield-contactTestId-1')).toBeInTheDocument(); + expect(getByDisplayValue('test2@test1.com')).toBeInTheDocument(); + }); + + it('input reset after adding an email address', async () => { + const { getByTestId, getByLabelText } = render( + , + ); + + const addInput = getByLabelText('New Email Address'); + const addButton = getByTestId('addButton-contactTestId'); + + userEvent.type(addInput, 'new@new.com'); + await waitFor(() => { + expect(addInput).toHaveValue('new@new.com'); + }); + userEvent.click(addButton); + await waitFor(() => { + expect(addInput).toHaveValue(''); + }); + }); + + describe('validation', () => { + it('should show an error message if there is no email', async () => { + const { getByLabelText, getByTestId, getByText } = render( + , + ); + + const addInput = getByLabelText('New Email Address'); + userEvent.click(addInput); + userEvent.tab(); + + const addButton = getByTestId('addButton-contactTestId'); + await waitFor(() => { + expect(addButton).toBeDisabled(); + expect(getByText('Please enter a valid email address')).toBeVisible(); + }); + }); + + it('should show an error message if there is an invalid email', async () => { + const { getByLabelText, getByTestId, getByText } = render( + , + ); + + const addInput = getByLabelText('New Email Address'); + userEvent.type(addInput, 'ab'); + userEvent.tab(); + + const addButton = getByTestId('addButton-contactTestId'); + await waitFor(() => { + expect(addButton).toBeDisabled(); + expect(getByText('Invalid Email Address Format')).toBeVisible(); + }); + }); + + it('should not disable the add button', async () => { + const { getByLabelText, getByTestId } = render( + , + ); + + const addInput = getByLabelText('New Email Address'); + userEvent.type(addInput, 'new@new.com'); + userEvent.tab(); + + const addButton = getByTestId('addButton-contactTestId'); + await waitFor(() => { + expect(addButton).not.toBeDisabled(); + }); + }); + + it('should show delete confirmation', async () => { + const { getByTestId, getByRole } = render( + , + ); + await waitFor(() => getByTestId('delete-contactTestId-1')); + + userEvent.click(getByTestId('delete-contactTestId-1')); + screen.logTestingPlaygroundURL(); + await waitFor(() => { + expect(getByRole('heading', { name: 'Confirm' })).toBeInTheDocument(); + }); + userEvent.click(getByRole('button', { name: 'Yes' })); + + const { id, email } = person.emailAddresses.nodes[1]; + + await waitFor(() => { + expect(mutationSpy.mock.lastCall[0].operation.operationName).toEqual( + 'UpdateEmailAddresses', + ); + expect(mutationSpy.mock.lastCall[0].operation.variables).toEqual({ + input: { + accountListId, + attributes: { + id: person.id, + emailAddresses: [ + { + id, + destroy: true, + }, + ], + }, + }, + }); + }); + + await waitFor(() => + expect(mockEnqueue).toHaveBeenCalledWith( + `Successfully deleted email address ${email}`, + { + variant: 'success', + }, + ), + ); + }); + }); +}); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx new file mode 100644 index 0000000000..e791e5efd8 --- /dev/null +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressPerson/FixEmailAddressPerson.tsx @@ -0,0 +1,402 @@ +import React, { Fragment, useMemo } from 'react'; +import { mdiDelete, mdiLock, mdiStar, mdiStarOutline } from '@mdi/js'; +import { Icon } from '@mdi/react'; +import { + Avatar, + Box, + Button, + Grid, + Hidden, + Link, + TextField, + Theme, + Typography, +} from '@mui/material'; +import { styled } from '@mui/material/styles'; +import { DateTime } from 'luxon'; +import { useSnackbar } from 'notistack'; +import { useTranslation } from 'react-i18next'; +import { makeStyles } from 'tss-react/mui'; +import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; +import { useUpdateEmailAddressesMutation } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; +import { Confirmation } from 'src/components/common/Modal/Confirmation/Confirmation'; +import { useLocale } from 'src/hooks/useLocale'; +import { dateFormatShort } from 'src/lib/intlFormat'; +import theme from 'src/theme'; +import { ConfirmButtonIcon } from '../../ConfirmButtonIcon'; +import EmailValidationForm from '../EmailValidationForm'; +import { PersonEmailAddresses } from '../FixEmailAddresses'; +import { PersonInvalidEmailFragment } from '../FixEmailAddresses.generated'; + +const PersonCard = styled(Box)(({ theme }) => ({ + [theme.breakpoints.up('md')]: { + border: `1px solid ${theme.palette.cruGrayMedium.main}`, + }, +})); + +const Container = styled(Grid)(({ theme }) => ({ + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-end', + [theme.breakpoints.down('sm')]: { + border: `1px solid ${theme.palette.cruGrayMedium.main}`, + }, +})); + +const EmailAddressListWrapper = styled(Grid)(({ theme }) => ({ + backgroundColor: theme.palette.cruGrayLight.main, + width: '100%', + [theme.breakpoints.down('xs')]: { + paddingTop: theme.spacing(2), + }, +})); + +const ConfirmButtonWrapper = styled(Box)(({ theme }) => ({ + marginLeft: theme.spacing(2), + [theme.breakpoints.down('sm')]: { + marginLeft: theme.spacing(1), + marginRight: theme.spacing(2), + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + '& .MuiButton-root': { + backgroundColor: theme.palette.mpdxBlue.main, + color: 'white', + }, +})); + +const BoxWithResponsiveBorder = styled(Box)(({ theme }) => ({ + [theme.breakpoints.down('xs')]: { + paddingBottom: theme.spacing(2), + borderBottom: `1px solid ${theme.palette.cruGrayMedium.main}`, + }, +})); + +const ColumnHeaderWrapper = styled(Grid)(({ theme }) => ({ + paddingTop: theme.spacing(2), + paddingBottom: theme.spacing(2), +})); + +export const RowWrapper = styled(Grid)(({ theme }) => ({ + paddingBottom: theme.spacing(2), +})); + +const HoverableIcon = styled(Icon)(({ theme }) => ({ + '&:hover': { + color: theme.palette.mpdxBlue.main, + cursor: 'pointer', + }, +})); + +const useStyles = makeStyles()((theme: Theme) => ({ + avatar: { + width: theme.spacing(7), + height: theme.spacing(7), + }, +})); + +export interface FixEmailAddressPersonProps { + person: PersonInvalidEmailFragment; + dataState: { [key: string]: PersonEmailAddresses }; + accountListId: string; + handleChange: ( + personId: string, + numberIndex: number, + event: React.ChangeEvent, + ) => void; + handleChangePrimary: (personId: string, emailIndex: number) => void; + setContactFocus: SetContactFocus; +} + +interface Email { + isValid: boolean; + personId: string; + isPrimary: boolean; + id: string; + primary: boolean; + updatedAt: string; + source: string; + email: string; + destroy?: boolean | undefined; +} + +interface EmailToDelete { + id: string; + email: Email; +} + +export const FixEmailAddressPerson: React.FC = ({ + person, + dataState, + accountListId, + handleChange, + handleChangePrimary, + setContactFocus, +}) => { + const { t } = useTranslation(); + const locale = useLocale(); + const { classes } = useStyles(); + const { enqueueSnackbar } = useSnackbar(); + const [updateEmailAddressesMutation] = useUpdateEmailAddressesMutation(); + const [deleteModalOpen, setDeleteModalOpen] = React.useState(false); + const [emailToDelete, setEmailToDelete] = + React.useState(null); + + const { id, contactId } = person; + const name = `${person.firstName} ${person.lastName}`; + + const emails: Email[] = useMemo(() => { + if (!dataState[id]?.emailAddresses.length) { + return []; + } + + return ( + dataState[id]?.emailAddresses.map((email) => ({ + ...email, + isValid: false, + personId: id, + isPrimary: email.primary, + })) || [] + ); + }, [person, dataState]); + + const handleDelete = async (): Promise => { + if (!emailToDelete) { + return; + } + const { id: personId, email } = emailToDelete; + await updateEmailAddressesMutation({ + variables: { + input: { + accountListId, + attributes: { + id: personId, + emailAddresses: [ + { + id: email.id, + destroy: true, + }, + ], + }, + }, + }, + update: (cache) => { + cache.evict({ id: `EmailAddress:${email.id}` }); + cache.gc(); + }, + onCompleted: () => { + enqueueSnackbar( + t(`Successfully deleted email address ${email.email}`), + { + variant: 'success', + }, + ); + handleDeleteEmailModalClose(); + }, + onError: () => { + enqueueSnackbar(t(`Error deleting email address ${email.email}`), { + variant: 'error', + }); + }, + }); + }; + + const handleContactNameClick = () => { + setContactFocus(contactId); + }; + + const handleDeleteEmailOpen = ({ id, email }: EmailToDelete) => { + setDeleteModalOpen(true); + setEmailToDelete({ id, email }); + }; + const handleDeleteEmailModalClose = (): void => { + setDeleteModalOpen(false); + setEmailToDelete(null); + }; + + return ( + <> + + + + + + + + + + + {name} + + + + + + + + + + + {t('Source')} + + + {t('Primary')} + + + + + + + {t('Address')} + + + + + {emails.map((email, index) => ( + + + + + + + {t('Source')}: + + + + {`${email.source} (${dateFormatShort( + DateTime.fromISO(email.updatedAt), + locale, + )})`} + + + {email.isPrimary ? ( + + + + ) : ( + handleChangePrimary(id, index)} + > + + + )} + + + + + , + ) => handleChange(id, index, event)} + value={email.email} + disabled={email.source !== 'MPDX'} + /> + + {email.source === 'MPDX' ? ( + + handleDeleteEmailOpen({ id, email }) + } + > + + + ) : ( + + )} + + + + ))} + + + + + + {t('Source')}: + + + MPDX + + + + + + + + + + + + + + + + + + + + + + + + {deleteModalOpen && emailToDelete && ( + + {t('Are you sure you wish to delete this email address:')}{' '} + {emailToDelete?.email.email} + + } + mutation={handleDelete} + handleClose={handleDeleteEmailModalClose} + /> + )} + + ); +}; diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql index f4534a40f1..4282ab56ef 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.graphql @@ -30,3 +30,15 @@ fragment PersonEmailAddress on EmailAddress { updatedAt source } + +mutation UpdateEmailAddresses($input: PersonUpdateMutationInput!) { + updatePerson(input: $input) { + person { + emailAddresses { + nodes { + ...PersonEmailAddress + } + } + } + } +} diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx index 33668ed05e..c1d08e0e36 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.test.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { ApolloCache, InMemoryCache } from '@apollo/client'; import { ThemeProvider } from '@mui/material/styles'; -import { render, waitFor } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ApolloErgonoMockMap, ErgonoMockShape } from 'graphql-ergonomock'; +import { ApolloErgonoMockMap } from 'graphql-ergonomock'; import { SnackbarProvider } from 'notistack'; import TestRouter from '__tests__/util/TestRouter'; import { GqlMockedProvider } from '__tests__/util/graphqlMocking'; @@ -13,10 +12,6 @@ import { EmailAddressesMutation } from './AddEmailAddress.generated'; import { FixEmailAddresses } from './FixEmailAddresses'; import { contactId, - contactOneEmailAddressNodes, - contactTwoEmailAddressNodes, - mockCacheWriteData, - mockCacheWriteDataContactTwo, mockInvalidEmailAddressesResponse, newEmail, } from './FixEmailAddressesMocks'; @@ -50,10 +45,9 @@ const defaultGraphQLMock = { interface ComponentsProps { mocks?: ApolloErgonoMockMap; - cache?: ApolloCache; } -const Components = ({ mocks = defaultGraphQLMock, cache }: ComponentsProps) => ( +const Components = ({ mocks = defaultGraphQLMock }: ComponentsProps) => ( @@ -62,7 +56,6 @@ const Components = ({ mocks = defaultGraphQLMock, cache }: ComponentsProps) => ( EmailAddresses: EmailAddressesMutation; }> mocks={mocks} - cache={cache} onCall={mutationSpy} > { const delete02 = await waitFor(() => getByTestId('delete-testid-2')); userEvent.click(delete02); - const deleteButton = getByTestId('modal-delete-button'); + const deleteButton = await waitFor(() => + getByTestId('modal-delete-button'), + ); userEvent.click(deleteButton); - expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); + await waitFor(() => { + expect(queryByTestId('textfield-testid-2')).not.toBeInTheDocument(); + }); }); - //TODO: Fix during MPDX-7936 - it.skip('change second email for second person to primary then delete it', async () => { - const { getByTestId, queryByTestId } = render(); + it('change second email for second person to primary then delete it', async () => { + const { getByTestId, queryByTestId, getByText, getByRole } = render( + , + ); const star11 = await waitFor(() => getByTestId('starOutlineIcon-testid2-1'), ); userEvent.click(star11); - const delete11 = getByTestId('delete-testid2-1'); + const delete11 = await waitFor(() => getByTestId('delete-testid2-1')); userEvent.click(delete11); - const deleteButton = getByTestId('modal-delete-button'); - userEvent.click(deleteButton); + await waitFor(() => + getByText(`Are you sure you wish to delete this email address:`), + ); + userEvent.click(getByRole('button', { name: 'Yes' })); - expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); - expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); + screen.logTestingPlaygroundURL(); + await waitFor(() => { + expect(queryByTestId('starIcon-testid2-1')).not.toBeInTheDocument(); + expect(getByTestId('starIcon-testid2-0')).toBeInTheDocument(); + }); }); it('should render no contacts with no data', async () => { @@ -219,133 +222,32 @@ describe('FixEmailAddresses-Home', () => { }); describe('Add email address - Testing cache', () => { - interface AddEmailAddressProps { - postSaveResponse: object; - emailAddressNodes: object[]; - elementToWaitFor: string; - textFieldIndex: number; - addButtonId: string; - cache: InMemoryCache; - } - - const addEmailAddress = async ({ - postSaveResponse, - emailAddressNodes, - elementToWaitFor, - textFieldIndex, - addButtonId, - cache, - }: AddEmailAddressProps) => { - let cardinality = 0; - jest.spyOn(cache, 'readQuery').mockReturnValue(postSaveResponse); - jest.spyOn(cache, 'writeQuery'); - - const updatePerson = { - person: { - emailAddresses: { - nodes: [ - ...emailAddressNodes, - { - email: newEmail.email, - }, - ], - }, - }, - } as ErgonoMockShape; - - const { getByTestId, getAllByLabelText } = render( - { - let queryResult; - if (cardinality === 0) { - queryResult = { - people: { - nodes: mockInvalidEmailAddressesResponse, - }, - }; - } else { - queryResult = postSaveResponse; - } - cardinality++; - return queryResult; - }, - EmailAddresses: { updatePerson }, - }} - cache={cache} - />, + it('should add an email address to the first person', async () => { + const { getAllByTestId, getByTestId, queryByTestId, getAllByRole } = + render(); + + const emailInput = await waitFor( + () => getAllByRole('textbox', { name: 'New Email Address' })[0], ); - await waitFor(() => { - expect(getByTestId(elementToWaitFor)).toBeInTheDocument(); - }); + const addButton = getAllByTestId('addButton-testid')[0]; + expect(queryByTestId('starOutlineIcon-testid-2')).not.toBeInTheDocument(); - const textFieldNew = - getAllByLabelText('New Email Address')[textFieldIndex]; - userEvent.type(textFieldNew, newEmail.email); - const addButton = getByTestId(addButtonId); + expect(addButton).toBeDisabled(); + userEvent.type(emailInput, 'test@cru.org'); + expect(addButton).not.toBeDisabled(); userEvent.click(addButton); - }; - - it('should add an email address to the first person', async () => { - const cache = new InMemoryCache(); - const postSaveResponse = { - people: { - nodes: [ - { - ...mockInvalidEmailAddressesResponse[0], - emailAddresses: { - nodes: [...contactOneEmailAddressNodes, newEmail], - }, - }, - { ...mockInvalidEmailAddressesResponse[1] }, - ], - }, - }; - await addEmailAddress({ - postSaveResponse, - emailAddressNodes: contactOneEmailAddressNodes, - elementToWaitFor: 'textfield-testid-0', - textFieldIndex: 0, - addButtonId: 'addButton-testid', - cache, - }); await waitFor(() => { - expect(cache.writeQuery).toHaveBeenLastCalledWith( - expect.objectContaining({ data: mockCacheWriteData }), - ); + expect(mockEnqueue).toHaveBeenCalledWith('Added email address', { + variant: 'success', + }); }); - }); - it('should add an email address to the second person', async () => { - const cache = new InMemoryCache(); - const postSaveResponse = { - people: { - nodes: [ - { ...mockInvalidEmailAddressesResponse[0] }, - { - ...mockInvalidEmailAddressesResponse[1], - emailAddresses: { - nodes: [...contactTwoEmailAddressNodes, newEmail], - }, - }, - ], - }, - }; - await addEmailAddress({ - postSaveResponse, - emailAddressNodes: contactTwoEmailAddressNodes, - elementToWaitFor: 'textfield-testid2-0', - textFieldIndex: 1, - addButtonId: 'addButton-testid2', - cache, - }); + await waitFor(() => + expect(getByTestId('starOutlineIcon-testid-2')).toBeInTheDocument(), + ); - await waitFor(() => { - expect(cache.writeQuery).toHaveBeenLastCalledWith( - expect.objectContaining({ data: mockCacheWriteDataContactTwo }), - ); - }); + screen.logTestingPlaygroundURL(); }); }); }); diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx index 34516aeb60..dcf8849b89 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddresses.tsx @@ -12,13 +12,11 @@ import { styled } from '@mui/material/styles'; import { Trans, useTranslation } from 'react-i18next'; import { SetContactFocus } from 'pages/accountLists/[accountListId]/tools/useToolsHelper'; import { useGetInvalidEmailAddressesQuery } from 'src/components/Tool/FixEmailAddresses/FixEmailAddresses.generated'; -import { PersonEmailAddressInput } from 'src/graphql/types.generated'; -import theme from '../../../theme'; +import theme from 'src/theme'; import { ConfirmButtonIcon } from '../ConfirmButtonIcon'; import NoData from '../NoData'; import { StyledInput } from '../StyledInput'; -import DeleteModal from './DeleteModal'; -import { FixEmailAddressPerson } from './FixEmailAddressPerson'; +import { FixEmailAddressPerson } from './FixEmailAddressPerson/FixEmailAddressPerson'; const Container = styled(Box)(() => ({ padding: theme.spacing(3), @@ -81,22 +79,8 @@ const DefaultSourceWrapper = styled(Box)(({ theme }) => ({ }, })); -export interface ModalState { - open: boolean; - personId: string; - emailIndex: number; - emailAddress: string; -} - -const defaultDeleteModalState = { - open: false, - personId: '', - emailIndex: 0, - emailAddress: '', -}; - export interface EmailAddressData { - id?: string; + id: string; primary: boolean; updatedAt: string; source: string; @@ -106,7 +90,6 @@ export interface EmailAddressData { export interface PersonEmailAddresses { emailAddresses: EmailAddressData[]; - toDelete: PersonEmailAddressInput[]; } interface FixEmailAddressesProps { @@ -119,15 +102,11 @@ export const FixEmailAddresses: React.FC = ({ setContactFocus, }) => { const [defaultSource, setDefaultSource] = useState('MPDX'); - const [deleteModalState, setDeleteModalState] = useState( - defaultDeleteModalState, - ); const { t } = useTranslation(); const { data, loading } = useGetInvalidEmailAddressesQuery({ variables: { accountListId }, }); - const [dataState, setDataState] = useState<{ [key: string]: PersonEmailAddresses; }>({}); @@ -150,32 +129,15 @@ export const FixEmailAddresses: React.FC = ({ email: emailAddress.email, }), ), - toDelete: [], }, }), {}, ) : {}, ), - [loading], + [loading, data], ); - const handleDeleteModalOpen = ( - personId: string, - emailIndex: number, - ): void => { - setDeleteModalState({ - open: true, - personId: personId, - emailIndex: emailIndex, - emailAddress: dataState[personId].emailAddresses[emailIndex].email, - }); - }; - - const handleDeleteModalClose = (): void => { - setDeleteModalState(defaultDeleteModalState); - }; - // Update the state with the textfield's value const handleChange = ( personId: string, @@ -187,34 +149,17 @@ export const FixEmailAddresses: React.FC = ({ setDataState(temp); }; - // Delete function called after confirming with the delete modal - const handleDelete = (): void => { - const temp = { ...dataState }; - const deleting = temp[deleteModalState.personId].emailAddresses.splice( - deleteModalState.emailIndex, - 1, - )[0]; - deleting.destroy = true; - deleting.primary && - (temp[deleteModalState.personId].emailAddresses[0].primary = true); // If the deleted email was primary, set the new first index to primary - deleting.id && - temp[deleteModalState.personId].toDelete.push({ - destroy: true, - id: deleting.id, - }); //Only destroy the email if it already exists (has an ID) - setDataState(temp); - handleDeleteModalClose(); - }; - // Change the primary address in the state const handleChangePrimary = (personId: string, emailIndex: number): void => { const temp = { ...dataState }; - temp[personId].emailAddresses = temp[personId].emailAddresses.map( - (email, index) => ({ - ...email, - primary: index === emailIndex, - }), - ); + if (temp[personId]) { + temp[personId].emailAddresses = temp[personId].emailAddresses.map( + (email, index) => ({ + ...email, + primary: index === emailIndex, + }), + ); + } setDataState(temp); }; @@ -232,7 +177,7 @@ export const FixEmailAddresses: React.FC = ({ {t('Fix Email Addresses')} - {data.people.nodes.length > 0 && ( + {data.people.nodes.length && ( <> @@ -276,15 +221,11 @@ export const FixEmailAddresses: React.FC = ({ {data?.people.nodes.map((person) => ( @@ -313,11 +254,6 @@ export const FixEmailAddresses: React.FC = ({ style={{ marginTop: theme.spacing(3) }} /> )} - ); }; diff --git a/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts b/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts index 040722f630..6ab77b75c2 100644 --- a/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts +++ b/src/components/Tool/FixEmailAddresses/FixEmailAddressesMocks.ts @@ -77,65 +77,3 @@ export const mockInvalidEmailAddressesResponse: ErgonoMockShape[] = [ }, }, ]; - -export const mockCacheWriteData = { - people: { - nodes: [ - { - ...mockInvalidEmailAddressesResponse[0], - emailAddresses: { - nodes: [ - { - __typename: contactOneEmailAddressNodes[0].__typename, - email: contactOneEmailAddressNodes[0].email, - }, - { - __typename: contactOneEmailAddressNodes[1].__typename, - email: contactOneEmailAddressNodes[1].email, - }, - { - __typename: contactOneEmailAddressNodes[2].__typename, - email: contactOneEmailAddressNodes[2].email, - }, - { - __typename: newEmail.__typename, - email: newEmail.email, - }, - ], - }, - }, - { - ...mockInvalidEmailAddressesResponse[1], - }, - ], - }, -}; - -export const mockCacheWriteDataContactTwo = { - people: { - nodes: [ - { - ...mockInvalidEmailAddressesResponse[0], - }, - { - ...mockInvalidEmailAddressesResponse[1], - emailAddresses: { - nodes: [ - { - __typename: contactTwoEmailAddressNodes[0].__typename, - email: contactTwoEmailAddressNodes[0].email, - }, - { - __typename: contactTwoEmailAddressNodes[1].__typename, - email: contactTwoEmailAddressNodes[1].email, - }, - { - __typename: newEmail.__typename, - email: newEmail.email, - }, - ], - }, - }, - ], - }, -};