Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,20 @@ describe('Preferences page', () => {
),
).toBeInTheDocument();

// Home Country
// Geographic Location
const skipButton = getByRole('button', { name: 'Skip Step' });
userEvent.click(skipButton);
expect(
await findByText('Are you within 50 miles of a major city?'),
).toBeInTheDocument();
expect(
await findByText(
'This should be the major city within 50 miles of you. If none apply, leave this blank.',
),
).toBeInTheDocument();

// Home Country
userEvent.click(skipButton);
expect(
await findByText(
'This should be the place from which you are living and sending out physical communications. This will be used in exports for mailing address information.',
Expand Down
17 changes: 16 additions & 1 deletion pages/accountLists/[accountListId]/settings/preferences.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { DefaultAccountAccordion } from 'src/components/Settings/preferences/accordions/DefaultAccountAccordion/DefaultAccountAccordion';
import { EarlyAdopterAccordion } from 'src/components/Settings/preferences/accordions/EarlyAdopterAccordion/EarlyAdopterAccordion';
import { ExportAllDataAccordion } from 'src/components/Settings/preferences/accordions/ExportAllDataAccordion/ExportAllDataAccordion';
import { GeographicLocationAccordion } from 'src/components/Settings/preferences/accordions/GeographicLocationAccordion/GeographicLocationAccordion';
import { HomeCountryAccordion } from 'src/components/Settings/preferences/accordions/HomeCountryAccordion/HomeCountryAccordion';
import { HourToSendNotificationsAccordion } from 'src/components/Settings/preferences/accordions/HourToSendNotificationsAccordion/HourToSendNotificationsAccordion';
import { LanguageAccordion } from 'src/components/Settings/preferences/accordions/LanguageAccordion/LanguageAccordion';
Expand Down Expand Up @@ -51,218 +52,232 @@
const setupAccordions = [
PreferenceAccordion.Locale,
PreferenceAccordion.MonthlyGoal,
PreferenceAccordion.GeographicLocation,
PreferenceAccordion.HomeCountry,
];
const [setup, setSetup] = useState(0);
const [expandedAccordion, setExpandedAccordion] =
useState<PreferenceAccordion | null>(
typeof query.selectedTab === 'string'
? (query.selectedTab as PreferenceAccordion)
: null,
);
const countries = getCountries();
const timeZones = useGetTimezones();

const [_, setSetupPosition] = useUserPreference({
key: 'setup_position',
defaultValue: '',
});

useEffect(() => {
const redirectToDownloadExportedData = (exportDataExportId: string) => {
const url = `${
process.env.REST_API_URL
}/account_lists/${accountListId}/exports/${encodeURIComponent(
exportDataExportId,
)}.xml?access_token=${session.apiToken}`;

window.location.replace(url);
};

if (query.exportId && typeof query.exportId === 'string') {
redirectToDownloadExportedData(query.exportId);
}
}, [query.exportId, accountListId]);

const { data: personalPreferencesData, loading: personalPreferencesLoading } =
useGetPersonalPreferencesQuery({
variables: {
accountListId,
},
});

const { data: accountPreferencesData, loading: accountPreferencesLoading } =
useGetAccountPreferencesQuery({
variables: {
accountListId,
},
});
const { data: canUserExportData } = useCanUserExportDataQuery({
variables: {
accountListId,
},
});

const { data: userOrganizationAccountsData } =
useGetUsersOrganizationsAccountsQuery();

useEffect(() => {
if (onSetupTour) {
setExpandedAccordion(setupAccordions[0]);
}
}, [onSetupTour]);

const resetWelcomeTour = async () => {
setSetupPosition('start');
push('/setup/start');
};

const handleSetupChange = async () => {
if (!onSetupTour) {
return;
}
const nextNav = setup + 1;

if (setupAccordions.length === nextNav) {
setSetupPosition('preferences.notifications');
push(`/accountLists/${accountListId}/settings/notifications`);
} else {
setSetup(nextNav);
setExpandedAccordion(setupAccordions[nextNav]);
}
};

const getSetupMessage = (setup: number) => {
switch (setup) {
case 0:
return t("Let's set your locale!");
case 1:
return t('Great progress comes from great goals!');
case 2:
return t('Are you within 50 miles of a major city?');
case 3:
return t('What country are you in?');
default:
return '';
}
};

return (
<SettingsWrapper
pageTitle={t('Preferences')}
pageHeading={t('Preferences')}
selectedMenuId={'preferences'}
>
{onSetupTour && (
<StickyBox>
<SetupBanner
button={
<Button variant="contained" onClick={handleSetupChange}>
{t('Skip Step')}
</Button>
}
title={getSetupMessage(setup)}
/>
</StickyBox>
)}
<ProfileInfo accountListId={accountListId} />
<AccordionGroup title={t('Personal Preferences')}>
{personalPreferencesLoading && (
<>
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
</>
)}
{!personalPreferencesLoading && (
<>
<LanguageAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
locale={personalPreferencesData?.user?.preferences?.locale || ''}
disabled={onSetupTour}
/>
<LocaleAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
localeDisplay={
personalPreferencesData?.user?.preferences?.localeDisplay || ''
}
disabled={onSetupTour && setup !== 0}
handleSetupChange={handleSetupChange}
/>
<DefaultAccountAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
data={personalPreferencesData}
defaultAccountListId={
personalPreferencesData?.user?.defaultAccountList || ''
}
disabled={onSetupTour}
/>
<TimeZoneAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
timeZone={
personalPreferencesData?.user?.preferences?.timeZone || ''
}
timeZones={timeZones}
disabled={onSetupTour}
/>
<HourToSendNotificationsAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
hourToSendNotifications={
personalPreferencesData?.user?.preferences
?.hourToSendNotifications || null
}
disabled={onSetupTour}
/>
</>
)}
</AccordionGroup>
<AccordionGroup title={t('Account Preferences')}>
{accountPreferencesLoading && (
<>
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
<AccordionLoading />
</>
)}
{!accountPreferencesLoading && (
<>
<AccountNameAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
name={accountPreferencesData?.accountList?.name || ''}
accountListId={accountListId}
disabled={onSetupTour}
/>
<MonthlyGoalAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
monthlyGoal={
accountPreferencesData?.accountList?.settings?.monthlyGoal ||
null
}
accountListId={accountListId}
currency={
accountPreferencesData?.accountList?.settings?.currency || ''
}
disabled={onSetupTour && setup !== 1}
handleSetupChange={handleSetupChange}
/>
<GeographicLocationAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
geographicLocation={
accountPreferencesData?.accountList?.settings
?.geographicLocation || ''
}
accountListId={accountListId}
disabled={onSetupTour && setup !== 2}
handleSetupChange={handleSetupChange}
/>
<HomeCountryAccordion
handleAccordionChange={setExpandedAccordion}
expandedAccordion={expandedAccordion}
homeCountry={
accountPreferencesData?.accountList?.settings?.homeCountry || ''
}
accountListId={accountListId}
countries={countries}
disabled={onSetupTour && setup !== 2}
disabled={onSetupTour && setup !== 3}

Check warning on line 280 in pages/accountLists/[accountListId]/settings/preferences.page.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Complex Method

Preferences:React.FC increases in cyclomatic complexity from 86 to 92, threshold = 20 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.
handleSetupChange={handleSetupChange}
/>
<CurrencyAccordion
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { useTranslation } from 'react-i18next';
import * as yup from 'yup';
import { useGoalCalculator } from 'src/components/HrTools/GoalCalculator/Shared/GoalCalculatorContext';
import { LocationInfoAlert } from 'src/components/HrTools/Shared/LocationInfoAlert/LocationInfoAlert';
import {
GoalCalculationAge,
GoalCalculationRole,
Expand Down Expand Up @@ -166,6 +167,7 @@
/>
)}
/>
<LocationInfoAlert />

Check warning on line 170 in src/components/HrTools/GoalCalculator/CalculatorSettings/Categories/InformationCategory/InformationCategoryForm/InformationCategoryPersonalForm.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ Getting worse: Complex Method

InformationCategoryPersonalForm:React.FC<InformationCategoryPersonalFormProps> already has high cyclomatic complexity, and now it increases in Lines of Code from 246 to 247 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.
</Grid>
)}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { GoalCalculatorTestWrapper } from '../../../GoalCalculatorTestWrapper';
import {
GoalCalculatorTestWrapper,
goalCalculationMock,
} from '../../../GoalCalculatorTestWrapper';
import { GoalApplicationButtonGroup } from './GoalApplicationButtonGroup';

const mutationSpy = jest.fn();
const TestComponent: React.FC = () => (
<GoalCalculatorTestWrapper onCall={mutationSpy}>
const TestComponent: React.FC<{ geographicLocation?: string | null }> = ({
geographicLocation,
}) => (
<GoalCalculatorTestWrapper
onCall={mutationSpy}
goalCalculation={
geographicLocation === undefined
? goalCalculationMock
: { ...goalCalculationMock, geographicLocation }
}
>
<GoalApplicationButtonGroup />
</GoalCalculatorTestWrapper>
);
Expand Down Expand Up @@ -65,4 +77,32 @@ describe('GoalApplicationButtonGroup', () => {
await findByText('Successfully updated your monthly goal to $16,139!'),
).toBeInTheDocument();
});

it('sends geographicLocation when the goal calculation has one', async () => {
const { getByRole, findByText } = render(
<TestComponent geographicLocation="Miami, FL" />,
);

const applyButton = getByRole('button', { name: /apply goal to mpdx/i });
await waitFor(() => expect(applyButton).toBeEnabled());
userEvent.click(applyButton);

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateAccountPreferences', {
input: {
id: 'account-list-1',
attributes: {
id: 'account-list-1',
settings: { geographicLocation: 'Miami, FL' },
},
},
}),
);

expect(
await findByText(
'Successfully updated your monthly goal to $16,139 and geographic location to Miami, FL!',
),
).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const GoalApplicationButtonGroup: React.FC = () => {
constants,
} = useGoalCalculator();
const monthlyGoal = Math.round(overallTotal);
const geographicLocation =
goalCalculationResult.data?.goalCalculation?.geographicLocation ?? null;
const [updateAccountPreferences, { loading }] =
useUpdateAccountPreferencesMutation();
const accountListId = useAccountListId() || '';
Expand All @@ -30,15 +32,30 @@ export const GoalApplicationButtonGroup: React.FC = () => {
id: accountListId,
attributes: {
id: accountListId,
settings: { monthlyGoal },
settings: {
monthlyGoal,
geographicLocation,
},
},
},
},
onCompleted: () => {
const formattedTotal = currencyFormat(monthlyGoal, 'USD', locale);
enqueueSnackbar(
t('Successfully updated your monthly goal to {{formattedTotal}}!', {
formattedTotal: currencyFormat(monthlyGoal, 'USD', locale),
}),
geographicLocation
? t(
'Successfully updated your monthly goal to {{formattedTotal}} and geographic location to {{geographicLocation}}!',
{
formattedTotal,
geographicLocation,
},
)
: t(
'Successfully updated your monthly goal to {{formattedTotal}}!',
{
formattedTotal,
},
),
{
variant: 'success',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { SnackbarProvider } from 'notistack';
import { DeepPartial } from 'ts-essentials';
import TestRouter from '__tests__/util/TestRouter';
import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
import { GetAccountPreferencesQuery } from 'src/components/Settings/preferences/GetAccountPreferences.generated';
import { GetUserQuery } from 'src/components/User/GetUser.generated';
import {
GoalCalculationAge,
Expand Down Expand Up @@ -59,6 +60,8 @@ export interface NsoMpdQuestionnaireTestWrapperProps {
mockPush?: jest.Mock;
/** Override the OneApp ministries list, e.g. `[]` to exercise the load-failure state. */
ministries?: MinistryMock[];
/** The account's saved Geographic Location preference. Defaults to null. */
accountGeographicLocation?: string | null;
children?: React.ReactNode;
}

Expand All @@ -70,6 +73,7 @@ export const NsoMpdQuestionnaireTestWrapper: React.FC<
onCall,
mockPush,
ministries = defaultMinistries,
accountGeographicLocation = null,
children,
}) => {
return (
Expand All @@ -80,11 +84,19 @@ export const NsoMpdQuestionnaireTestWrapper: React.FC<
GoalCalculatorConstants: GoalCalculatorConstantsQuery;
NewStaffQuestionnaire: NewStaffQuestionnaireQuery;
Ministries: MinistriesQuery;
GetAccountPreferences: GetAccountPreferencesQuery;
}>
mocks={{
GetUser: {
user: { avatar: 'avatar.jpg', staffAccountId: '000123456' },
},
GetAccountPreferences: {
accountList: {
settings: {
geographicLocation: accountGeographicLocation,
},
},
},
NewStaffQuestionnaire: {
newStaffQuestionnaire:
newStaffQuestionnaire === null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -240,4 +240,79 @@ describe('Summary', () => {
await waitFor(() => expect(submitButton).toBeEnabled());
expect(queryByRole('alert')).not.toBeInTheDocument();
});

describe('Geographic location preference', () => {
it('shows info alert and updates the account preference when it differs', async () => {
const { findByRole, getByRole, findByText } = render(
<TestComponent
onCall={mutationSpy}
newStaffQuestionnaire={{
...filledDebtFields,
geographicLocation: 'Miami, FL',
}}
accountGeographicLocation="Atlanta, GA"
/>,
);
const submitButton = await findByRole('button', { name: 'Submit' });
await waitFor(() => expect(submitButton).toBeEnabled());
userEvent.click(submitButton);

expect(
await findByText(
'Your geographic location will be updated as Miami, FL in your account settings.',
),
).toBeInTheDocument();

const dialog = getByRole('dialog');
userEvent.click(within(dialog).getByRole('button', { name: 'Submit' }));

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation('UpdateAccountPreferences', {
input: {
id: 'account-list-1',
attributes: {
id: 'account-list-1',
settings: { geographicLocation: 'Miami, FL' },
},
},
}),
);
});

it('does not show info alert or update the preference when it already matches', async () => {
const { findByRole, queryByText } = render(
<TestComponent
onCall={mutationSpy}
newStaffQuestionnaire={{
...filledDebtFields,
geographicLocation: 'Miami, FL',
}}
accountGeographicLocation="Miami, FL"
/>,
);
const submitButton = await findByRole('button', { name: 'Submit' });
await waitFor(() => expect(submitButton).toBeEnabled());
userEvent.click(submitButton);

const dialog = await findByRole('dialog');
await waitFor(() =>
expect(
queryByText(
'Your geographic location will be updated as Miami, FL in your account settings.',
),
).not.toBeInTheDocument(),
);

userEvent.click(within(dialog).getByRole('button', { name: 'Submit' }));

await waitFor(() =>
expect(mutationSpy).toHaveGraphqlOperation(
'CompleteNewStaffQuestionnaire',
),
);
expect(mutationSpy).not.toHaveGraphqlOperation(
'UpdateAccountPreferences',
);
});
});
});
50 changes: 47 additions & 3 deletions src/components/HrTools/NsoMpdQuestionnaire/Summary/Summary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { Alert, Box, Divider, Link, Stack, Typography } from '@mui/material';
import { useSnackbar } from 'notistack';
import { useTranslation } from 'react-i18next';
import { useGetAccountPreferencesQuery } from 'src/components/Settings/preferences/GetAccountPreferences.generated';
import { useUpdateAccountPreferencesMutation } from 'src/components/Settings/preferences/accordions/UpdateAccountPreferences.generated';
import { Confirmation } from 'src/components/Shared/Modal/Confirmation/Confirmation';
import { useAccountListId } from 'src/hooks/useAccountListId';
import { BackButton } from '../Shared/BackButton';
Expand Down Expand Up @@ -30,74 +32,116 @@
incompleteSteps.includes(section.step),
);

const { data } = useGetAccountPreferencesQuery({
variables: {
accountListId,
},
});
const [updateAccountPreferences] = useUpdateAccountPreferencesMutation();

const updateGeographicLocation =
data?.accountList?.settings?.geographicLocation !==
questionnaire?.geographicLocation;

// Complete the questionnaire, then redirect to the dashboard on success.
const handleSubmit = async () => {
await completeQuestionnaire();

if (updateGeographicLocation) {
Comment thread
kegrimes marked this conversation as resolved.
updateAccountPreferences({
variables: {
input: {
id: accountListId,
attributes: {
id: accountListId,
settings: {
geographicLocation: questionnaire?.geographicLocation,
},
},
},
},
});
}

enqueueSnackbar(t('Questionnaire submitted successfully.'), {
variant: 'success',
});
await router.push(`/accountLists/${accountListId}`);
};

const message = (
<>
{t(
"Once you submit, you won't be able to make any more changes. Do you want to continue?",
)}
{updateGeographicLocation && (
<Alert severity="info" sx={{ mt: 2 }}>
{t(
'Your geographic location will be updated as {{geographicLocation}} in your account settings.',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about "updated to"?

Suggested change
'Your geographic location will be updated as {{geographicLocation}} in your account settings.',
'Your geographic location will be updated to {{geographicLocation}} in your account settings.',

{ geographicLocation: questionnaire?.geographicLocation },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know you're still working out how to handle the None option, but I assume you'll want a fallback value here and in the other alerts and notifications.

Suggested change
{ geographicLocation: questionnaire?.geographicLocation },
{ geographicLocation: questionnaire?.geographicLocation ?? "None" },

)}
</Alert>
)}
</>
);

return (
<NsoMpdQuestionnaireLayout>
<Box mx={4} my={2}>
<Stack spacing={2}>
<Typography variant="h5">{t('Summary')}</Typography>
<Typography variant="body1">
{t(
'Please review your information below, then select Submit to finish. Once submitted, you will be redirected to the dashboard and will not be able to make any further changes.',
)}
</Typography>
</Stack>
<Divider sx={{ mx: -4, my: 4 }} />
<Stack spacing={4}>
{sections.map((section) => (
<SummarySection
key={section.title}
title={section.title}
rows={section.rows}
onEdit={() => handleStepChange(section.step)}
/>
))}
</Stack>
</Box>

{!canSubmit && (
<Alert severity="error" sx={{ mx: 4, '& ul': { m: 0, pl: 3 } }}>
{t('Your form is missing information.')}
<ul>
{incompleteSections.map((section) => (
<li key={section.step}>
<Link
component="button"
type="button"
onClick={() => handleStepChange(section.step)}
>
{section.title}
</Link>
</li>
))}
</ul>
</Alert>
)}

<Stack direction={{ xs: 'column', sm: 'row' }} spacing={2} mx={4}>
<BackButton onClick={handleBack} />
<QuestionnaireActionButton
onClick={() => setConfirmOpen(true)}
disabled={!canSubmit}
>
{t('Submit')}
</QuestionnaireActionButton>
</Stack>

<Confirmation
isOpen={confirmOpen}
title={t('Submit Questionnaire')}
message={t(
"Once you submit, you won't be able to make any more changes. Do you want to continue?",
)}
message={message}

Check warning on line 144 in src/components/HrTools/NsoMpdQuestionnaire/Summary/Summary.tsx

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Large Method

Summary:React.FC has 122 lines, threshold = 120 Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.
confirmLabel={t('Submit')}
cancelLabel={t('Cancel')}
mutation={handleSubmit}
Expand Down
Loading
Loading