diff --git a/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.test.tsx
index 46783d8fe..1e5947697 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.test.tsx
@@ -1,14 +1,16 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import theme from 'src/theme';
+import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { FinancialDetails } from './FinancialDetails';
-const TestComponent: React.FC = () => (
-
+const TestComponent: React.FC<{ onCall?: MockLinkCallHandler }> = ({
+ onCall,
+}) => (
+
-
+
);
const studentLoanQuestion =
@@ -31,6 +33,15 @@ describe('FinancialDetails', () => {
expect(getByRole('radio', { name: 'No' })).toBeInTheDocument();
});
+ it('shows the debt-question error once the group is touched without a selection', () => {
+ const { getByRole, getByText } = render();
+
+ getByRole('radio', { name: 'Yes' }).focus();
+ userEvent.tab();
+
+ expect(getByText('Please select an answer.')).toBeInTheDocument();
+ });
+
it('hides the payment fields until Yes is selected', () => {
const { queryByRole } = render();
@@ -90,4 +101,27 @@ describe('FinancialDetails', () => {
expect(queryByText(requiredError)).not.toBeInTheDocument();
});
+
+ it('saves zero for every debt field when the user has no debt', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render();
+
+ userEvent.click(getByRole('radio', { name: 'No' }));
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: {
+ studentLoanMonthlyPayment: 0,
+ carLoanMonthlyPayment: 0,
+ creditCardDebtMonthlyPayment: 0,
+ },
+ },
+ },
+ ),
+ );
+ });
});
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.tsx b/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.tsx
index 30550d385..bee57be3a 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/FinancialInformation/FinancialDetails.tsx
@@ -15,14 +15,16 @@ import {
import { TFunction, useTranslation } from 'react-i18next';
import * as yup from 'yup';
import { useOptionalAutosaveForm } from 'src/components/Shared/Autosave/AutosaveForm';
+import { useNsoMpdQuestionnaire } from '../Shared/NsoMpdQuestionnaireContext';
import { NumberQuestion } from '../Shared/NumberQuestion';
import { getAmountSchema } from '../Shared/helpers/getAmountSchema';
+import { QuestionnaireField } from '../Shared/useQuestionnaireAutoSave';
export const getFinancialDetailsSchema = (t: TFunction) =>
yup.object({
- studentLoanPayment: getAmountSchema(t),
- carPayment: getAmountSchema(t),
- creditCardPayment: getAmountSchema(t),
+ studentLoanMonthlyPayment: getAmountSchema(t),
+ carLoanMonthlyPayment: getAmountSchema(t),
+ creditCardDebtMonthlyPayment: getAmountSchema(t),
});
export const FinancialDetails: React.FC = () => {
@@ -30,10 +32,26 @@ export const FinancialDetails: React.FC = () => {
const schema = useMemo(() => getFinancialDetailsSchema(t), [t]);
+ const { saveField } = useNsoMpdQuestionnaire();
+
// UI only toggle
const [hasDebt, setHasDebt] = useState('');
+ const [hasDebtTouched, setHasDebtTouched] = useState(false);
const showDebtFields = hasDebt === 'Yes';
const hasDebtError = !hasDebt;
+ const showHasDebtError = hasDebtError && hasDebtTouched;
+
+ const handleHasDebtChange = (event: React.ChangeEvent) => {
+ const value = event.target.value;
+ setHasDebt(value);
+ if (value === 'No') {
+ saveField({
+ studentLoanMonthlyPayment: 0,
+ carLoanMonthlyPayment: 0,
+ creditCardDebtMonthlyPayment: 0,
+ });
+ }
+ };
const { markValid, markInvalid } = useOptionalAutosaveForm() ?? {};
useEffect(() => {
@@ -45,19 +63,23 @@ export const FinancialDetails: React.FC = () => {
return () => markValid?.('hasDebt');
}, [hasDebtError, markValid, markInvalid]);
- const debtFields = [
+ const debtFields: {
+ fieldName: QuestionnaireField;
+ debtType: string;
+ icon: React.ReactNode;
+ }[] = [
{
- fieldName: 'studentLoanPayment',
+ fieldName: 'studentLoanMonthlyPayment',
debtType: t('student loan debt'),
icon: ,
},
{
- fieldName: 'carPayment',
+ fieldName: 'carLoanMonthlyPayment',
debtType: t('car debt'),
icon: ,
},
{
- fieldName: 'creditCardPayment',
+ fieldName: 'creditCardDebtMonthlyPayment',
debtType: t('credit card debt'),
icon: ,
},
@@ -65,7 +87,7 @@ export const FinancialDetails: React.FC = () => {
return (
-
+
{t('Do you have any student loan, car, or credit card debt?')}
@@ -74,12 +96,13 @@ export const FinancialDetails: React.FC = () => {
sx={{ paddingInline: 2 }}
aria-labelledby="has-debt-label"
value={hasDebt}
- onChange={(event) => setHasDebt(event.target.value)}
+ onChange={handleHasDebtChange}
+ onBlur={() => setHasDebtTouched(true)}
>
} label={t('Yes')} />
} label={t('No')} />
- {hasDebtError && (
+ {showHasDebtError && (
{t('Please select an answer.')}
)}
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.test.tsx
index 290e7f20b..99117d6d2 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.test.tsx
@@ -1,31 +1,16 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import { GqlMockedProvider } from '__tests__/util/graphqlMocking';
-import { GoalCalculatorConstantsQuery } from 'src/hooks/goalCalculatorConstants.generated';
-import theme from 'src/theme';
+import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { MinistryDetails } from './MinistryDetails';
-const TestComponent: React.FC = () => (
-
-
- mocks={{
- GoalCalculatorConstants: {
- constant: {
- mpdGoalGeographicConstants: [
- { location: 'Atlanta, GA' },
- { location: 'Miami, FL' },
- ],
- },
- },
- }}
- >
-
-
-
+const TestComponent: React.FC<{ onCall?: MockLinkCallHandler }> = ({
+ onCall,
+}) => (
+
+
+
);
describe('MinistryDetails', () => {
@@ -103,6 +88,25 @@ describe('MinistryDetails', () => {
expect(getByRole('radio', { name: 'Office' })).toBeInTheDocument();
});
+ it('saves FIELD when the field assignment is chosen', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render();
+
+ userEvent.click(getByRole('radio', { name: 'Field' }));
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { assignmentType: 'FIELD' },
+ },
+ },
+ ),
+ );
+ });
+
it('shows a loading indicator in place of the city field until the constants load', () => {
const { getByRole, queryByRole } = render();
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.tsx b/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.tsx
index 0816599d1..c1fb5fe7e 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/MinistryInformation/MinistryDetails.tsx
@@ -36,11 +36,11 @@ export const MinistryDetails: React.FC = () => {
const schema = useMemo(
() =>
yup.object({
- ministry: yup.string().required(t('Please select a ministry')),
- assignmentLocation: yup
+ ministryName: yup.string().required(t('Please select a ministry')),
+ ministryLocation: yup
.string()
.required(t('Please enter an assignment location')),
- nearestCity: yup
+ geographicLocation: yup
.string()
.required(
t(
@@ -55,18 +55,18 @@ export const MinistryDetails: React.FC = () => {
);
const ministryProps = useQuestionnaireAutoSave({
- fieldName: 'ministry',
+ fieldName: 'ministryName',
schema,
saveOnChange: true,
});
const locationProps = useQuestionnaireAutoSave({
- fieldName: 'assignmentLocation',
+ fieldName: 'ministryLocation',
schema,
});
const cityProps = useQuestionnaireAutoSave({
- fieldName: 'nearestCity',
+ fieldName: 'geographicLocation',
schema,
saveOnChange: true,
});
@@ -148,8 +148,8 @@ export const MinistryDetails: React.FC = () => {
row
label={t('What type of assignment are you expecting?')}
options={[
- { value: 'Field', label: t('Field') },
- { value: 'Office', label: t('Office') },
+ { value: 'FIELD', label: t('Field') },
+ { value: 'OFFICE', label: t('Office') },
]}
/>
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.test.tsx
index 90ce6b067..47ce53733 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.test.tsx
@@ -1,14 +1,16 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import theme from 'src/theme';
+import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { NsoDetails } from './NsoDetails';
-const TestComponent: React.FC = () => (
-
+const TestComponent: React.FC<{ onCall?: MockLinkCallHandler }> = ({
+ onCall,
+}) => (
+
-
+
);
describe('NsoDetails', () => {
@@ -37,6 +39,50 @@ describe('NsoDetails', () => {
).toBeInTheDocument();
});
+ it('saves the housing enum constant', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render();
+
+ userEvent.click(getByRole('radio', { name: 'Family in a hotel/room' }));
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { nsoHousing: 'FAMILY_ROOM' },
+ },
+ },
+ ),
+ );
+ });
+
+ it('saves the childcare count as a number', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render();
+
+ userEvent.type(
+ getByRole('spinbutton', {
+ name: 'If you are a parent with children in Childcare, please enter how many.',
+ }),
+ '3',
+ );
+ userEvent.tab();
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { childcareChildrenCount: 3 },
+ },
+ },
+ ),
+ );
+ });
+
it('renders the sessions question with both options', () => {
const { getByRole } = render();
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.tsx b/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.tsx
index 222f32f98..560c56806 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/NsoInformation/NsoDetails.tsx
@@ -11,8 +11,8 @@ export const getNsoDetailsSchema = (t: TFunction) =>
yup.object({
nsoHousing: yup.string().required(t('Please select an answer.')),
nsoSessions: yup.string().required(t('Please select an answer.')),
- specialNeedsSupport: getAmountSchema(t),
- childcareChildren: getWholeNumberSchema(
+ nsoSpecialNeedsSupportReceived: getAmountSchema(t),
+ childcareChildrenCount: getWholeNumberSchema(
t,
t('Please enter a number, or 0 if you have none.'),
),
@@ -24,24 +24,15 @@ export const NsoDetails: React.FC = () => {
const schema = useMemo(() => getNsoDetailsSchema(t), [t]);
const housingOptions: RadioOption[] = [
- {
- value: 'Single in hotel/dorm room',
- label: t('Single in hotel/dorm room'),
- },
- {
- value: 'Sharing 2 in hotel/dorm room',
- label: t('Sharing 2 in hotel/dorm room'),
- },
- {
- value: 'Couple in hotel/dorm room',
- label: t('Couple in hotel/dorm room'),
- },
- { value: 'Family in a hotel/room', label: t('Family in a hotel/room') },
- { value: 'Local / Commuting', label: t('Local / Commuting') },
+ { value: 'SINGLE_ROOM', label: t('Single in hotel/dorm room') },
+ { value: 'SHARED_ROOM', label: t('Sharing 2 in hotel/dorm room') },
+ { value: 'COUPLE_ROOM', label: t('Couple in hotel/dorm room') },
+ { value: 'FAMILY_ROOM', label: t('Family in a hotel/room') },
+ { value: 'LOCAL_COMMUTING', label: t('Local / Commuting') },
];
const sessionOptions: RadioOption[] = [
- { value: 'IBS and NSO', label: t('IBS and NSO') },
+ { value: 'IBS_AND_NSO', label: t('IBS and NSO') },
{ value: 'NSO', label: t('NSO') },
];
@@ -62,7 +53,7 @@ export const NsoDetails: React.FC = () => {
/>
{
/>
(HcmDocument, {
@@ -58,13 +59,23 @@ export interface NsoMpdQuestionnaireTestWrapperProps {
hcmUser?: DeepPartial;
hcmSpouse?: DeepPartial;
hasSpouse?: boolean;
+ newStaffQuestionnaire?: DeepPartial<
+ NewStaffQuestionnaireQuery['newStaffQuestionnaire']
+ >;
onCall?: MockLinkCallHandler;
children?: React.ReactNode;
}
export const NsoMpdQuestionnaireTestWrapper: React.FC<
NsoMpdQuestionnaireTestWrapperProps
-> = ({ hcmUser, hcmSpouse, hasSpouse = true, onCall, children }) => {
+> = ({
+ hcmUser,
+ hcmSpouse,
+ hasSpouse = true,
+ newStaffQuestionnaire,
+ onCall,
+ children,
+}) => {
const hcmUserMerged = merge({}, hcmUserMock, hcmUser);
const hcmSpouseMerged = merge({}, hcmSpouseMock, hcmSpouse);
return (
@@ -74,11 +85,15 @@ export const NsoMpdQuestionnaireTestWrapper: React.FC<
Hcm: HcmQuery;
GetUser: GetUserQuery;
GoalCalculatorConstants: GoalCalculatorConstantsQuery;
+ NewStaffQuestionnaire: NewStaffQuestionnaireQuery;
}>
mocks={{
GetUser: {
user: { avatar: 'avatar.jpg', staffAccountId: '000123456' },
},
+ NewStaffQuestionnaire: {
+ newStaffQuestionnaire: newStaffQuestionnaire ?? null,
+ },
Hcm: {
hcm: hasSpouse
? [hcmUserMerged, hcmSpouseMerged]
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.test.tsx
index 944f0cb1d..1ee3683ee 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.test.tsx
@@ -1,14 +1,16 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
-import theme from 'src/theme';
+import { MockLinkCallHandler } from 'graphql-ergonomock/dist/apollo/MockLink';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { ContactInformation } from './ContactInformation';
-const TestComponent: React.FC = () => (
-
+const TestComponent: React.FC<{ onCall?: MockLinkCallHandler }> = ({
+ onCall,
+}) => (
+
-
+
);
describe('ContactInformation', () => {
@@ -26,6 +28,39 @@ describe('ContactInformation', () => {
expect(getByRole('textbox', { name: 'Cell Phone Number' })).toBeRequired();
});
+ it('saves the cell phone number through the upsert on blur', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render();
+
+ const input = getByRole('textbox', { name: 'Cell Phone Number' });
+ userEvent.type(input, '(123) 456-7890');
+ userEvent.tab();
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { phoneNumber: '(123) 456-7890' },
+ },
+ },
+ ),
+ );
+ });
+
+ it('seeds the cell phone number from the loaded questionnaire', async () => {
+ const { findByDisplayValue } = render(
+
+
+ ,
+ );
+
+ expect(await findByDisplayValue('(305) 111-2222')).toBeInTheDocument();
+ });
+
it('strips disallowed characters as the user types', () => {
const { getByRole } = render();
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.tsx b/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.tsx
index 7633eca76..20df5a85f 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/PersonalInformation/ContactInformation.tsx
@@ -14,13 +14,15 @@ export const ContactInformation: React.FC = () => {
const schema = useMemo(
() =>
yup.object({
- cellPhone: phoneNumber(t).required(t('Cell phone number is required')),
+ phoneNumber: phoneNumber(t).required(
+ t('Cell phone number is required'),
+ ),
}),
[t],
);
const fieldProps = useQuestionnaireAutoSave({
- fieldName: 'cellPhone',
+ fieldName: 'phoneNumber',
schema,
});
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NewStaffQuestionnaire.graphql b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NewStaffQuestionnaire.graphql
new file mode 100644
index 000000000..706f47094
--- /dev/null
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NewStaffQuestionnaire.graphql
@@ -0,0 +1,21 @@
+fragment NewStaffQuestionnaireFields on NewStaffQuestionnaire {
+ id
+ phoneNumber
+ ministryName
+ ministryLocation
+ geographicLocation
+ assignmentType
+ nsoHousing
+ nsoSessions
+ nsoSpecialNeedsSupportReceived
+ childcareChildrenCount
+ studentLoanMonthlyPayment
+ carLoanMonthlyPayment
+ creditCardDebtMonthlyPayment
+}
+
+query NewStaffQuestionnaire($accountListId: ID!) {
+ newStaffQuestionnaire(accountListId: $accountListId) {
+ ...NewStaffQuestionnaireFields
+ }
+}
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.test.tsx
index d33713cc0..3e6d1de95 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.test.tsx
@@ -1,10 +1,19 @@
import React, { useEffect } from 'react';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { NsoMpdQuestionnaireStepEnum } from '../NsoMpdQuestionnaireHelper';
import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { useNsoMpdQuestionnaire } from './NsoMpdQuestionnaireContext';
+const SaveComponent: React.FC = () => {
+ const { saveField } = useNsoMpdQuestionnaire();
+ return (
+
+ );
+};
+
interface InnerComponentProps {
initialStep?: NsoMpdQuestionnaireStepEnum;
}
@@ -102,4 +111,27 @@ describe('NsoMpdQuestionnaireContext', () => {
userEvent.click(getByRole('button', { name: 'Toggle Drawer' }));
expect(drawerState).toHaveAttribute('data-open', 'false');
});
+
+ it('fires the upsert mutation with coerced attributes', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render(
+
+
+ ,
+ );
+
+ userEvent.click(getByRole('button', { name: 'Save' }));
+
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { phoneNumber: '305-555-1234' },
+ },
+ },
+ ),
+ );
+ });
});
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.tsx
index 564190184..34fe7e2be 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NsoMpdQuestionnaireContext.tsx
@@ -1,10 +1,21 @@
import React, { createContext, useCallback, useMemo, useState } from 'react';
+import { NewStaffQuestionnaireAttributesInput } from 'src/graphql/types.generated';
+import { useAccountListId } from 'src/hooks/useAccountListId';
import { HcmQuery, useHcmQuery } from '../../Shared/HcmData/Hcm.generated';
import { NsoMpdQuestionnaireStepEnum } from '../NsoMpdQuestionnaireHelper';
+import {
+ NewStaffQuestionnaireDocument,
+ NewStaffQuestionnaireQuery,
+ useNewStaffQuestionnaireQuery,
+} from './NewStaffQuestionnaire.generated';
+import { useUpdateNewStaffQuestionnaireMutation } from './UpdateNewStaffQuestionnaire.generated';
import { NsoMpdQuestionnaireStep, useSteps } from './useSteps';
export type HcmPerson = HcmQuery['hcm'][number];
+export type NewStaffQuestionnaire =
+ NewStaffQuestionnaireQuery['newStaffQuestionnaire'];
+
export type NsoMpdQuestionnaireType = {
steps: NsoMpdQuestionnaireStep[];
currentStep: NsoMpdQuestionnaireStep;
@@ -17,6 +28,11 @@ export type NsoMpdQuestionnaireType = {
setDrawerOpen: (open: boolean) => void;
hcmUser: HcmPerson | null;
hcmSpouse: HcmPerson | null;
+ questionnaire: NewStaffQuestionnaire | null;
+ questionnaireLoading: boolean;
+ saveField: (
+ attributes: Partial,
+ ) => Promise;
};
const NsoMpdQuestionnaireContext =
@@ -48,6 +64,48 @@ export const NsoMpdQuestionnaireProvider: React.FC = ({ children }) => {
const hcmUser = hcmData?.hcm[0] ?? null;
const hcmSpouse = hcmData?.hcm[1] ?? null;
+ const accountListId = useAccountListId();
+
+ const { data: questionnaireData, loading: questionnaireLoading } =
+ useNewStaffQuestionnaireQuery({
+ variables: { accountListId: accountListId ?? '' },
+ skip: !accountListId,
+ });
+ const questionnaire = questionnaireData?.newStaffQuestionnaire ?? null;
+
+ const [updateNewStaffQuestionnaire] = useUpdateNewStaffQuestionnaireMutation();
+
+ const saveField = useCallback(
+ async (
+ attributes: Partial,
+ ): Promise => {
+ if (!accountListId) {
+ return;
+ }
+ await updateNewStaffQuestionnaire({
+ variables: {
+ input: { accountListId, attributes },
+ },
+ // The mutation returns the full record, so Apollo merges updates into the cache by id
+ // automatically. The one case that isn't covered is the first save, which creates the
+ // record while the query field is still cached as null — point it at the new record so
+ // every step sees it without an extra refetch round-trip per save.
+ update: (cache, { data }) => {
+ const saved = data?.updateNewStaffQuestionnaire?.newStaffQuestionnaire;
+ if (!saved) {
+ return;
+ }
+ cache.writeQuery({
+ query: NewStaffQuestionnaireDocument,
+ variables: { accountListId },
+ data: { newStaffQuestionnaire: saved },
+ });
+ },
+ });
+ },
+ [accountListId, updateNewStaffQuestionnaire],
+ );
+
const toggleDrawer = useCallback(() => {
setIsDrawerOpen((prev) => !prev);
}, []);
@@ -85,6 +143,9 @@ export const NsoMpdQuestionnaireProvider: React.FC = ({ children }) => {
setDrawerOpen,
hcmUser,
hcmSpouse,
+ questionnaire,
+ questionnaireLoading,
+ saveField,
}),
[
steps,
@@ -98,6 +159,9 @@ export const NsoMpdQuestionnaireProvider: React.FC = ({ children }) => {
setDrawerOpen,
hcmUser,
hcmSpouse,
+ questionnaire,
+ questionnaireLoading,
+ saveField,
],
);
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.test.tsx
index 2cb646ad4..37a2b14f2 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.test.tsx
@@ -1,13 +1,12 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as yup from 'yup';
-import theme from 'src/theme';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { NumberQuestion } from './NumberQuestion';
const schema = yup.object({
- count: yup
+ childcareChildrenCount: yup
.string()
.matches(/^\d+$/, 'Please enter a whole number.')
.required('Please enter a number.'),
@@ -16,15 +15,15 @@ const schema = yup.object({
const TestComponent: React.FC<{ startAdornment?: React.ReactNode }> = ({
startAdornment,
}) => (
-
+
-
+
);
describe('NumberQuestion', () => {
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.tsx
index bdb0fc284..4bc4bf29e 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/NumberQuestion.tsx
@@ -1,10 +1,13 @@
import React from 'react';
import { TextField } from '@mui/material';
import * as yup from 'yup';
-import { useQuestionnaireAutoSave } from './useQuestionnaireAutoSave';
+import {
+ QuestionnaireField,
+ useQuestionnaireAutoSave,
+} from './useQuestionnaireAutoSave';
interface NumberQuestionProps {
- fieldName: string;
+ fieldName: QuestionnaireField;
schema: yup.Schema;
question: string;
helperText: string;
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.test.tsx
index 8cff296be..37de9b32a 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.test.tsx
@@ -1,13 +1,12 @@
import React from 'react';
-import { ThemeProvider } from '@mui/material/styles';
import { render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as yup from 'yup';
-import theme from 'src/theme';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { RadioOption, RadioQuestion } from './RadioQuestion';
const schema = yup.object({
- choice: yup.string().required('Please select an answer.'),
+ geographicLocation: yup.string().required('Please select an answer.'),
});
const options: RadioOption[] = [
{ value: 'A', label: 'Option A' },
@@ -15,15 +14,15 @@ const options: RadioOption[] = [
];
const TestComponent: React.FC<{ row?: boolean }> = ({ row }) => (
-
+
-
+
);
describe('RadioQuestion', () => {
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.tsx
index 4576adbc6..d16770d68 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/RadioQuestion.tsx
@@ -8,7 +8,10 @@ import {
RadioGroup,
} from '@mui/material';
import * as yup from 'yup';
-import { useQuestionnaireAutoSave } from './useQuestionnaireAutoSave';
+import {
+ QuestionnaireField,
+ useQuestionnaireAutoSave,
+} from './useQuestionnaireAutoSave';
export interface RadioOption {
value: string;
@@ -16,7 +19,7 @@ export interface RadioOption {
}
interface RadioQuestionProps {
- fieldName: string;
+ fieldName: QuestionnaireField;
schema: yup.Schema;
label: string;
options: RadioOption[];
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/UpdateNewStaffQuestionnaire.graphql b/src/components/HrTools/NsoMpdQuestionnaire/Shared/UpdateNewStaffQuestionnaire.graphql
new file mode 100644
index 000000000..082501708
--- /dev/null
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/UpdateNewStaffQuestionnaire.graphql
@@ -0,0 +1,9 @@
+mutation UpdateNewStaffQuestionnaire(
+ $input: NewStaffQuestionnaireUpdateMutationInput!
+) {
+ updateNewStaffQuestionnaire(input: $input) {
+ newStaffQuestionnaire {
+ ...NewStaffQuestionnaireFields
+ }
+ }
+}
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.test.ts b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.test.ts
index cad80e521..d7327db45 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.test.ts
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.test.ts
@@ -14,9 +14,9 @@ describe('getAmountSchema', () => {
);
});
- it('accepts a whole-dollar amount and normalizes leading zeros', () => {
- expect(getAmountSchema(i18n.t).validateSync('500')).toBe('500');
- expect(getAmountSchema(i18n.t).validateSync('007')).toBe('7');
- expect(getAmountSchema(i18n.t).validateSync('0')).toBe('0');
+ it('accepts a whole-dollar amount and parses it to a number', () => {
+ expect(getAmountSchema(i18n.t).validateSync('500')).toBe(500);
+ expect(getAmountSchema(i18n.t).validateSync('007')).toBe(7);
+ expect(getAmountSchema(i18n.t).validateSync('0')).toBe(0);
});
});
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.ts b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.ts
index 6e44135a1..c6a682869 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.ts
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getAmountSchema.ts
@@ -6,7 +6,7 @@ import { getWholeNumberSchema } from './getWholeNumberSchema';
* Yup schema for a required, non-negative whole-dollar amount (no cents). Built on
* {@link getWholeNumberSchema} with dollar-specific validation copy.
*/
-export const getAmountSchema = (t: TFunction): yup.StringSchema =>
+export const getAmountSchema = (t: TFunction): yup.NumberSchema =>
getWholeNumberSchema(t, t('Please enter an amount, or 0 if you have none.'), {
positive: t('Please enter a positive amount.'),
whole: t('Please enter a whole dollar amount.'),
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.test.ts b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.test.ts
index 02467d70c..31a41fcad 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.test.ts
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.test.ts
@@ -22,12 +22,12 @@ describe('getWholeNumberSchema', () => {
).toThrow('Please enter a whole number.');
});
- it('accepts a whole number and normalizes leading zeros', () => {
+ it('accepts a whole number and parses it to a number', () => {
const schema = getWholeNumberSchema(i18n.t, requiredMessage);
- expect(schema.validateSync('500')).toBe('500');
- expect(schema.validateSync('007')).toBe('7');
- expect(schema.validateSync('0')).toBe('0');
+ expect(schema.validateSync('500')).toBe(500);
+ expect(schema.validateSync('007')).toBe(7);
+ expect(schema.validateSync('0')).toBe(0);
});
it('uses overridden positive and whole messages', () => {
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.ts b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.ts
index 5a89f1542..eddcd54d5 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.ts
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/helpers/getWholeNumberSchema.ts
@@ -9,21 +9,17 @@ interface WholeNumberMessages {
}
/**
- * yup schema for a required, non-negative whole number. Leading zeros
- * are normalized. Callers supply the required message and may override the
+ * yup schema for a required, non-negative whole number. Callers supply the required message and may override the
* positive/whole validation messages for domain-specific copy.
*/
export const getWholeNumberSchema = (
t: TFunction,
requiredMessage: string,
messages: WholeNumberMessages = {},
-): yup.StringSchema =>
+): yup.NumberSchema =>
yup
- .string()
- // Normalize leading zeros
- .transform((value) =>
- typeof value === 'string' ? value.replace(/^0+(?=\d)/, '') : value,
- )
- .matches(/^[^-]/, messages.positive ?? t('Please enter a positive number.'))
- .matches(/^\d+$/, messages.whole ?? t('Please enter a whole number.'))
+ .number()
+ .typeError(messages.whole ?? t('Please enter a whole number.'))
+ .min(0, messages.positive ?? t('Please enter a positive number.'))
+ .integer(messages.whole ?? t('Please enter a whole number.'))
.required(requiredMessage);
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.test.tsx b/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.test.tsx
index 48596eaa9..8cb6180c5 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.test.tsx
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.test.tsx
@@ -1,39 +1,52 @@
import React from 'react';
import { TextField } from '@mui/material';
-import { render } from '@testing-library/react';
+import { render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import * as yup from 'yup';
+import { NsoMpdQuestionnaireTestWrapper } from '../NsoMpdQuestionnaireTestWrapper';
import { useQuestionnaireAutoSave } from './useQuestionnaireAutoSave';
const TestComponent: React.FC = () => {
const schema = yup.object({
- field: yup.string().min(7, 'Too short'),
+ phoneNumber: yup.string().min(7, 'Too short'),
+ });
+ const props = useQuestionnaireAutoSave({
+ fieldName: 'phoneNumber',
+ schema,
});
-
- const props = useQuestionnaireAutoSave({ fieldName: 'field', schema });
-
return ;
};
describe('useQuestionnaireAutoSave', () => {
- it('starts empty without an error', () => {
- const { getByRole, queryByText } = render();
+ it('starts empty when there is no loaded questionnaire', () => {
+ const { getByRole } = render(
+
+
+ ,
+ );
expect(getByRole('textbox', { name: 'Field' })).toHaveValue('');
- expect(queryByText('Too short')).not.toBeInTheDocument();
});
- it('stores typed values in local state', () => {
- const { getByRole } = render();
+ it('seeds the field from the loaded questionnaire', async () => {
+ const { findByDisplayValue } = render(
+
+
+ ,
+ );
- const input = getByRole('textbox', { name: 'Field' });
- userEvent.type(input, '1234567890');
-
- expect(input).toHaveValue('1234567890');
+ expect(await findByDisplayValue('305-111-2222')).toBeInTheDocument();
});
it('surfaces the schema validation error for an invalid value once touched', () => {
- const { getByRole, getByText } = render();
+ const { getByRole, getByText } = render(
+
+
+ ,
+ );
userEvent.type(getByRole('textbox', { name: 'Field' }), '123');
userEvent.tab();
@@ -41,12 +54,28 @@ describe('useQuestionnaireAutoSave', () => {
expect(getByText('Too short')).toBeInTheDocument();
});
- it('clears the error once the value becomes valid', () => {
- const { getByRole, queryByText } = render();
+ it('saves a valid value through the upsert on blur', async () => {
+ const mutationSpy = jest.fn();
+ const { getByRole } = render(
+
+
+ ,
+ );
const input = getByRole('textbox', { name: 'Field' });
- userEvent.type(input, '1234567');
+ userEvent.type(input, '305-555-1234');
+ userEvent.tab();
- expect(queryByText('Too short')).not.toBeInTheDocument();
+ await waitFor(() =>
+ expect(mutationSpy).toHaveGraphqlOperation(
+ 'UpdateNewStaffQuestionnaire',
+ {
+ input: {
+ accountListId: 'account-list-1',
+ attributes: { phoneNumber: '305-555-1234' },
+ },
+ },
+ ),
+ );
});
});
diff --git a/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.ts b/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.ts
index d4e864f96..3f372e3b7 100644
--- a/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.ts
+++ b/src/components/HrTools/NsoMpdQuestionnaire/Shared/useQuestionnaireAutoSave.ts
@@ -1,29 +1,34 @@
-import { useState } from 'react';
import * as yup from 'yup';
import { useAutoSave } from 'src/components/Shared/Autosave/useAutosave';
+import { NewStaffQuestionnaireAttributesInput } from 'src/graphql/types.generated';
+import {
+ NewStaffQuestionnaire,
+ useNsoMpdQuestionnaire,
+} from './NsoMpdQuestionnaireContext';
+
+export type QuestionnaireField = keyof NewStaffQuestionnaireAttributesInput &
+ keyof NonNullable;
interface UseQuestionnaireAutoSaveOptions {
- fieldName: string;
+ fieldName: QuestionnaireField;
schema: yup.Schema;
saveOnChange?: boolean;
}
/**
- * Bridges questionnaire fields to the shared {@link useAutoSave} hook until the questionnaire has a
- * backing API.
+ * Bridges a single questionnaire field to the shared {@link useAutoSave} hook: seeds the value
+ * from the loaded questionnaire and persists edits through the context's upsert mutation.
*/
export const useQuestionnaireAutoSave = ({
fieldName,
...options
}: UseQuestionnaireAutoSaveOptions) => {
- const [questionnaire, setQuestionnaire] = useState<
- Record
- >({});
+ const { questionnaire, saveField } = useNsoMpdQuestionnaire();
return useAutoSave({
- value: questionnaire[fieldName],
+ value: questionnaire?.[fieldName],
saveValue: async (value) => {
- setQuestionnaire((prev) => ({ ...prev, [fieldName]: value }));
+ await saveField({ [fieldName]: value });
},
fieldName,
...options,