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-8511 - Restoring the Relationship Code field on contacts #1250

Merged
merged 6 commits into from
Jan 7, 2025
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
2 changes: 1 addition & 1 deletion .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fragment ContactDonorAccounts on Contact {
source
likelyToGive
name
relationshipCode
primaryPerson {
id
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mutation UpdateContactPartnership(
}
}
likelyToGive
relationshipCode
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ import {
ContactDonorAccountsFragment,
ContactDonorAccountsFragmentDoc,
} from '../../ContactDonationsTab.generated';
import { UserOrganizationAccountsQuery } from '../PartnershipInfo.generated';
import {
organizationAccountsMock,
organizationAccountsWithCruSwitzerlandMock,
} from '../PartnershipInfoMocks';
import { EditPartnershipInfoModal } from './EditPartnershipInfoModal';

jest.mock('notistack', () => ({
Expand Down Expand Up @@ -115,14 +120,27 @@ const newContactMock = gqlMock<ContactDonorAccountsFragment>(

interface ComponentsProps {
isNewContact?: boolean;
includeCruSwitzerland?: boolean;
}

const Components = ({ isNewContact = false }: ComponentsProps) => (
const Components = ({
isNewContact = false,
includeCruSwitzerland = false,
}: ComponentsProps) => (
<LocalizationProvider dateAdapter={AdapterLuxon}>
<TestRouter>
<SnackbarProvider>
<ThemeProvider theme={theme}>
<GqlMockedProvider onCall={mutationSpy}>
<GqlMockedProvider<{
UserOrganizationAccounts: UserOrganizationAccountsQuery;
}>
mocks={{
UserOrganizationAccounts: includeCruSwitzerland
? organizationAccountsWithCruSwitzerlandMock
: organizationAccountsMock,
}}
onCall={mutationSpy}
>
<EditPartnershipInfoModal
contact={isNewContact ? newContactMock : contactMock}
handleClose={handleClose}
Expand Down Expand Up @@ -538,4 +556,52 @@ describe('EditPartnershipInfoModal', () => {
}),
);
});

describe('Relationship code', () => {
it('should not render relationshipCode', async () => {
const { queryByRole } = render(<Components />);

await waitFor(() => {
expect(mutationSpy).toHaveGraphqlOperation('UserOrganizationAccounts');
});

expect(
queryByRole('textbox', {
name: 'Relationship Code',
}),
).not.toBeInTheDocument();
});

it('should render relationshipCode', async () => {
const { findByRole } = render(<Components includeCruSwitzerland />);

expect(
await findByRole('textbox', {
name: 'Relationship Code',
}),
).toBeInTheDocument();
});

// Remove skip this when the UpdateContact mutation operation is updated to include the relationshipCode
it.skip('should update relationshipCode', async () => {
const { findByRole, getByText } = render(
<Components includeCruSwitzerland />,
);

const relationshipCode = await findByRole('textbox', {
name: 'Relationship Code',
});
userEvent.clear(relationshipCode);
userEvent.type(relationshipCode, '1234');
userEvent.click(getByText('Save'));

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateContactPartnership', {
attributes: {
relationshipCode: '1234',
},
}),
);
});
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import InfoIcon from '@mui/icons-material/InfoOutlined';
import {
Alert,
Expand Down Expand Up @@ -46,6 +46,8 @@
import { useApiConstants } from '../../../../../Constants/UseApiConstants';
import Modal from '../../../../../common/Modal/Modal';
import { ContactDonorAccountsFragment } from '../../ContactDonationsTab.generated';
import { isApartOfSwitzerlandOrganization } from '../PartnershipInfo';
import { useUserOrganizationAccountsQuery } from '../PartnershipInfo.generated';
import { useUpdateContactPartnershipMutation } from './EditPartnershipInfoModal.generated';

const ContactInputWrapper = styled(Box)(({ theme }) => ({
Expand Down Expand Up @@ -102,6 +104,7 @@
.nullable(),
pledgeFrequency: yup.mixed<PledgeFrequencyEnum>().nullable(),
likelyToGive: yup.mixed<LikelyToGiveEnum>().nullable(),
relationshipCode: yup.string().nullable(),
});

type Attributes = yup.InferType<typeof contactPartnershipSchema>;
Expand All @@ -118,432 +121,459 @@
const { appName } = useGetAppSettings();
const accountListId = useAccountListId();
const constants = useApiConstants();
const { enqueueSnackbar } = useSnackbar();
const { getLocalizedContactStatus, getLocalizedPledgeFrequency } =
useLocalizedConstants();

const phases = constants?.phases;
const [showRemoveCommitmentWarning, setShowRemoveCommitmentWarning] =
useState(false);

const { enqueueSnackbar } = useSnackbar();
const { data } = useUserOrganizationAccountsQuery();
const userOrganizationAccounts = data?.userOrganizationAccounts;

const showRelationshipCode = useMemo(
() => isApartOfSwitzerlandOrganization(userOrganizationAccounts),
[userOrganizationAccounts],
);

const [updateContactPartnership, { loading: updating }] =
useUpdateContactPartnershipMutation();
const pledgeCurrencies = constants?.pledgeCurrency;

const onSubmit = async (attributes: Attributes) => {
// Remove and just use "attributes" when the UpdateContact mutation operation is updated to include the relationshipCode
const { relationshipCode: _, ...newAttributes } = attributes;
await updateContactPartnership({
variables: {
accountListId: accountListId ?? '',
attributes: {
...attributes,
...newAttributes,
pledgeStartDate: attributes.pledgeStartDate?.toISODate() ?? null,
nextAsk: attributes.nextAsk?.toISODate() ?? null,
primaryPersonId: attributes.primaryPersonId,
},
},
});

enqueueSnackbar(t('Partnership information updated successfully.'), {
variant: 'success',
});
handleClose();
};

const updateStatus = (
newStatus: StatusEnum,
setFieldValue: (name: string, value: StatusEnum | number | null) => void,
oldStatus?: StatusEnum | null,
pledgeAmount?: number | null,
pledgeFrequency?: PledgeFrequencyEnum | null,
) => {
setFieldValue('status', newStatus);
if (
newStatus !== StatusEnum.PartnerFinancial &&
oldStatus === StatusEnum.PartnerFinancial &&
((pledgeAmount && pledgeAmount > 0) || pledgeFrequency)
) {
setShowRemoveCommitmentWarning(true);
}
};

const removeCommittedDetails = (
setFieldValue: (name: string, value: StatusEnum | number | null) => void,
) => {
setFieldValue('pledgeAmount', 0);
setFieldValue('pledgeFrequency', null);
setShowRemoveCommitmentWarning(false);
};

return (
<Modal
isOpen={true}
title={t('Edit Partnership')}
handleClose={handleClose}
>
<Formik<Attributes>
initialValues={{
id: contact.id,
status: contact.status,
pledgeAmount: contact.pledgeAmount,
pledgeFrequency: contact.pledgeFrequency,
pledgeReceived: contact.pledgeReceived,
pledgeCurrency: contact.pledgeCurrency,
pledgeStartDate: contact.pledgeStartDate
? DateTime.fromISO(contact.pledgeStartDate)
: null,
nextAsk: contact.nextAsk ? DateTime.fromISO(contact.nextAsk) : null,
noAppeals: Boolean(contact.noAppeals),
sendNewsletter: contact.sendNewsletter ?? SendNewsletterEnum.None,
likelyToGive: contact.likelyToGive,
name: contact.name,
primaryPersonId: contact?.primaryPerson?.id ?? '',
relationshipCode: contact.relationshipCode ?? '',
}}
validationSchema={contactPartnershipSchema}
onSubmit={onSubmit}
>
{({
values: {
status,
pledgeAmount,
pledgeFrequency,
pledgeCurrency,
pledgeReceived,
pledgeStartDate,
nextAsk,
noAppeals,
sendNewsletter,
likelyToGive,
name,
primaryPersonId,
relationshipCode,
},
handleSubmit,
handleChange,
setFieldValue,
isSubmitting,
isValid,
touched,
errors,
handleBlur,
}) => (
<form onSubmit={handleSubmit} noValidate>
{errors.pledgeFrequency}
<DialogContent dividers sx={{ maxHeight: '60vh' }}>
<Grid container>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<TextField
name="name"
label={t('Contact Name')}
value={name}
onChange={handleChange}
onBlur={handleBlur}
inputProps={{ 'aria-label': t('Contact') }}
error={!!errors.name && touched.name}
helperText={
errors.name &&
touched.name &&
t('Contact name is required')
}
fullWidth
/>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth={true}>
<InputLabel id="primary-person-select-label">
{t('Primary Person')}
</InputLabel>
<Select
label={t('Primary Person')}
labelId="primary-person-select-label"
value={primaryPersonId}
onChange={(e) =>
setFieldValue('primaryPersonId', e.target.value)
}
fullWidth={true}
>
{contact.people.nodes.map((person) => (
<MenuItem key={person.id} value={person.id}>{`${
person.firstName || ''
} ${person.lastName || ''}`}</MenuItem>
))}
</Select>
</FormControl>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth>
<InputLabel id="status-select-label">
{t('Status')}
</InputLabel>
<Select
label={t('Status')}
labelId="status-select-label"
value={status}
onChange={(e) =>
updateStatus(
e.target.value as StatusEnum,
setFieldValue,
status,
pledgeAmount,
pledgeFrequency,
)
}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
PaperProps: {
style: {
maxHeight: '300px',
overflow: 'auto',
},
},
}}
>
{phases?.map((phase) => [
<ListSubheader key={phase?.id}>
{phase?.name}
</ListSubheader>,
phase?.contactStatuses.map((status) => (
<MenuItem key={status} value={status}>
{getLocalizedContactStatus(status)}
</MenuItem>
)),
])}
</Select>
</FormControl>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth>
<InputLabel id="newsletter-select-label">
{t('Newsletter')}
</InputLabel>
<Select
label={t('Newsletter')}
labelId="newsletter-select-label"
value={sendNewsletter}
onChange={(e) =>
setFieldValue(
'sendNewsletter',
e.target.value as SendNewsletterEnum,
)
}
>
{Object.values(SendNewsletterEnum).map((value) => (
<MenuItem key={value} value={value}>
{getLocalizedSendNewsletter(t, value)}
</MenuItem>
))}
</Select>
</FormControl>
</ContactInputWrapper>
</Grid>
{showRemoveCommitmentWarning && (
<ContactInputWrapper data-testid="removeCommitmentMessage">
<Alert severity="warning">
<Typography>
{t(
'{{appName}} uses your contact status, commitment amount, and frequency together to calculate many things, including your progress towards your goal and notification alerts.',
{ appName },
)}
</Typography>
<Typography my={'10px'}>
{t(
'If you are switching this contact away from Partner - Financial status, their commitment amount and frequency will no longer be included in calculations. Would you like to remove their commitment amount and frequency, as well?',
)}
</Typography>
<RemoveCommitmentActions>
<Button
color="inherit"
size="small"
variant="contained"
onClick={() => setShowRemoveCommitmentWarning(false)}
>
{t('No')}
</Button>
<Button
color="primary"
size="small"
variant="contained"
onClick={() => removeCommittedDetails(setFieldValue)}
>
{t('Yes')}
</Button>
</RemoveCommitmentActions>
</Alert>
</ContactInputWrapper>
)}
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<TextFieldInteractive
label={t('Amount')}
isDisabled={status !== StatusEnum.PartnerFinancial}
value={pledgeAmount}
type="number"
disabled={status !== StatusEnum.PartnerFinancial}
aria-readonly={status !== StatusEnum.PartnerFinancial}
onChange={handleChange('pledgeAmount')}
inputProps={{ 'aria-label': t('Amount') }}
InputProps={{
endAdornment: (
<Tooltip
title={
<Typography>
{t(
'Commitments can only be set if status is Partner - Financial',
)}
</Typography>
}
>
<InfoIcon />
</Tooltip>
),
}}
fullWidth
/>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth>
<InputLabel id="frequency-select-label">
{t('Frequency')}
</InputLabel>
<SelectInteractive
label={t('Frequency')}
labelId="frequency-select-label"
value={pledgeFrequency ?? ''}
isDisabled={status !== StatusEnum.PartnerFinancial}
disabled={status !== StatusEnum.PartnerFinancial}
aria-readonly={status !== StatusEnum.PartnerFinancial}
onChange={(e) =>
setFieldValue('pledgeFrequency', e.target.value)
}
IconComponent={
status !== StatusEnum.PartnerFinancial
? () => (
<Tooltip
sx={{ marginRight: '14px' }}
title={
<Typography>
{t(
'Commitments can only be set if status is Partner - Financial',
)}
</Typography>
}
>
<InfoIcon />
</Tooltip>
)
: undefined
}
>
<MenuItem value={''} disabled></MenuItem>
{Object.values(PledgeFrequencyEnum).map((value) => (
<MenuItem key={value} value={value}>
{getLocalizedPledgeFrequency(value)}
</MenuItem>
))}
</SelectInteractive>
</FormControl>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth>
<InputLabel id="currency-select-label">
{t('Currency')}
</InputLabel>
{pledgeCurrencies && (
<Select
label={t('Currency')}
labelId="currency-select-label"
value={pledgeCurrency ?? ''}
onChange={(e) =>
setFieldValue('pledgeCurrency', e.target.value)
}
MenuProps={{
anchorOrigin: {
vertical: 'bottom',
horizontal: 'left',
},
transformOrigin: {
vertical: 'top',
horizontal: 'left',
},
PaperProps: {
style: {
maxHeight: '300px',
overflow: 'auto',
},
},
}}
>
<MenuItem value={''} disabled></MenuItem>
{getPledgeCurrencyOptions(pledgeCurrencies)}
</Select>
)}
</FormControl>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<CustomDateField
label={t('Start Date')}
value={pledgeStartDate}
onChange={(date) =>
setFieldValue('pledgeStartDate', date)
}
/>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<FormControl fullWidth>
<InputLabel id="likely-to-give-select-label">
{t('Likely To Give')}
</InputLabel>
<Select
label={t('Likely To Give')}
labelId="likely-to-give-select-label"
value={likelyToGive ?? ''}
onChange={(e) =>
setFieldValue(
'likelyToGive',
e.target.value as LikelyToGiveEnum,
)
}
>
{Object.values(LikelyToGiveEnum).map((val) => (
<MenuItem key={val} value={val}>
{getLocalizedLikelyToGive(t, val)}
</MenuItem>
))}
</Select>
</FormControl>
</ContactInputWrapper>
</Grid>
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<CustomDateField
label={t('Next Increase Ask')}
value={nextAsk}
onChange={(nextAsk) => setFieldValue('nextAsk', nextAsk)}
/>
</ContactInputWrapper>
</Grid>
{showRelationshipCode && (
<Grid item xs={12} sm={6}>
<ContactInputWrapper>
<TextField
name="relationshipCode"
label={t('Relationship Code')}
value={relationshipCode}
onChange={handleChange}
onBlur={handleBlur}
inputProps={{ 'aria-label': t('Relationship Code') }}
fullWidth
/>
</ContactInputWrapper>
</Grid>
)}
</Grid>

Check warning on line 576 in src/components/Contacts/ContactDetails/ContactDonationsTab/PartnershipInfo/EditPartnershipInfoModal/EditPartnershipInfoModal.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (main)

❌ Getting worse: Complex Method

EditPartnershipInfoModal:React.FC<EditPartnershipInfoModalProps> increases in cyclomatic complexity from 26 to 28, threshold = 10. This function has many conditional statements (e.g. if, for, while), leading to lower code health. Avoid adding more conditionals and code to it without refactoring.
<ContactInputWrapper>
<CheckboxLabel
control={
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
query UserOrganizationAccounts {
userOrganizationAccounts {
id
organization {
id
name
}
}
}
Loading
Loading