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 @@ -170,6 +171,7 @@
/>
)}
/>
<LocationInfoAlert />

Check warning on line 174 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 247 to 248 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
Expand Up @@ -13,6 +13,7 @@ import {
PrimaryBudgetCategoryEnum,
SubBudgetCategoryEnum,
} from 'src/graphql/types.generated';
import { AccountGeographicLocationQuery } from 'src/hooks/AccountGeographicLocation.generated';
import {
GoalCalculatorConstantsDocument,
GoalCalculatorConstantsQuery,
Expand Down Expand Up @@ -338,21 +339,24 @@ interface MockedGoalCalculatorTestWrapperProps {
* mock every year with the static `constantsMock`.
*/
constantsByYear?: Record<number, GoalCalculatorConstantsQuery['constant']>;
/** The account's currently saved geographic location preference. */
accountGeographicLocation?: string | null;
children?: React.ReactNode;
}

interface NoMocksGoalCalculatorTestWrapperProps {
/**
* Skip the `GqlMockedProvider` entirely (the test supplies its own Apollo
* provider). `onCall`, `readOnly`, `goalCalculation`, and `constantsByYear`
* only configure the mocked provider, so they are disallowed here — they
* would silently no-op.
* provider). `onCall`, `readOnly`, `goalCalculation`, `constantsByYear`, and
* `accountGeographicLocation` only configure the mocked provider, so they
* are disallowed here — they would silently no-op.
*/
noMocks: true;
onCall?: never;
readOnly?: never;
goalCalculation?: never;
constantsByYear?: never;
accountGeographicLocation?: never;
children?: React.ReactNode;
}

Expand All @@ -368,6 +372,7 @@ export const GoalCalculatorTestWrapper: React.FC<
readOnly = false,
goalCalculation = goalCalculationMock,
constantsByYear,
accountGeographicLocation = null,
children,
}) => {
const content = <GoalCalculatorProvider>{children}</GoalCalculatorProvider>;
Expand All @@ -388,6 +393,7 @@ export const GoalCalculatorTestWrapper: React.FC<
<GqlMockedProvider<{
GoalCalculation: GoalCalculationQuery;
GoalCalculatorConstants: GoalCalculatorConstantsQuery;
AccountGeographicLocation: AccountGeographicLocationQuery;
}>
mocks={{
GoalCalculation: {
Expand All @@ -400,6 +406,13 @@ export const GoalCalculatorTestWrapper: React.FC<
? mockConstantsByYear(constantsByYear)
: constantsMock,
},
AccountGeographicLocation: {
accountList: {
settings: {
geographicLocation: accountGeographicLocation,
},
},
},
}}
onCall={onCall}
>
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
@@ -1,50 +1,26 @@
import React, { useState } from 'react';
import { Button, CircularProgress, Stack } from '@mui/material';
import { useSnackbar } from 'notistack';
import { useTranslation } from 'react-i18next';
import { useUpdateAccountPreferencesMutation } from 'src/components/Settings/preferences/accordions/UpdateAccountPreferences.generated';
import { useAccountListId } from 'src/hooks/useAccountListId';
import { useLocale } from 'src/hooks/useLocale';
import { currencyFormat } from 'src/lib/intlFormat';
import { useApplyGoalAndLocation } from 'src/hooks/useApplyGoalAndLocation';
import { useGoalCalculator } from '../../../Shared/GoalCalculatorContext';

export const GoalApplicationButtonGroup: React.FC = () => {
const { t } = useTranslation();
const locale = useLocale();
const { enqueueSnackbar } = useSnackbar();
const {
goalCalculationResult,
goalTotals: { overallTotal },
constants,
} = useGoalCalculator();
const monthlyGoal = Math.round(overallTotal);
const [updateAccountPreferences, { loading }] =
useUpdateAccountPreferencesMutation();
const accountListId = useAccountListId() || '';
const geographicLocation =
goalCalculationResult.data?.goalCalculation?.geographicLocation ?? null;
const { applyMonthlyGoal, loading } =
useApplyGoalAndLocation(geographicLocation);
const [buttonsHidden, setButtonsHidden] = useState(false);

const onSave = async () => {
await updateAccountPreferences({
variables: {
input: {
id: accountListId,
attributes: {
id: accountListId,
settings: { monthlyGoal },
},
},
},
onCompleted: () => {
enqueueSnackbar(
t('Successfully updated your monthly goal to {{formattedTotal}}!', {
formattedTotal: currencyFormat(monthlyGoal, 'USD', locale),
}),
{
variant: 'success',
},
);
},
});
await applyMonthlyGoal(monthlyGoal);
setButtonsHidden(true);
};

if (buttonsHidden) {
Expand All @@ -62,10 +38,7 @@ export const GoalApplicationButtonGroup: React.FC = () => {
>
<Button
variant="contained"
onClick={() => {
onSave();
setButtonsHidden(true);
}}
onClick={onSave}
disabled={
goalCalculationResult.loading ||
// Without the year's constants the total would be understated, so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
GoalCalculationAge,
NewStaffQuestionnaireMaritalStatusEnum,
} from 'src/graphql/types.generated';
import { AccountGeographicLocationQuery } from 'src/hooks/AccountGeographicLocation.generated';
import { GoalCalculatorConstantsQuery } from 'src/hooks/goalCalculatorConstants.generated';
import theme from 'src/theme';
import { MinistriesQuery } from './MinistryInformation/Ministries.generated';
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;
AccountGeographicLocation: AccountGeographicLocationQuery;
}>
mocks={{
GetUser: {
user: { avatar: 'avatar.jpg', staffAccountId: '000123456' },
},
AccountGeographicLocation: {
accountList: {
settings: {
geographicLocation: accountGeographicLocation,
},
},
},
NewStaffQuestionnaire: {
newStaffQuestionnaire:
newStaffQuestionnaire === null
Expand Down
Loading
Loading