From 6a3da7170a80764358913cb7c122993dbfd8ee6a Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:09:48 -0500 Subject: [PATCH 1/2] Add validation logic to disable Continue button in PdsGoalCalculator and SetupStep components --- .../PdsGoalCalculator.test.tsx | 54 ++++++++++++++++++- .../PdsGoalCalculator/PdsGoalCalculator.tsx | 41 +++++++++----- .../PdsGoalCalculator/Setup/SetupStep.tsx | 34 +++++++++--- .../DirectionButtons.test.tsx | 21 ++++++++ .../DirectionButtons/DirectionButtons.tsx | 1 + 5 files changed, 131 insertions(+), 20 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.test.tsx b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.test.tsx index 5e5550d2a8..c913e776c1 100644 --- a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.test.tsx +++ b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.test.tsx @@ -1,8 +1,23 @@ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, waitFor } from '@testing-library/react'; +import { + DesignationSupportSalaryType, + DesignationSupportStatus, +} from 'src/graphql/types.generated'; import { PdsGoalCalculator } from './PdsGoalCalculator'; import { PdsGoalCalculatorTestWrapper } from './PdsGoalCalculatorTestWrapper'; +const completeSetupMock = { + id: 'goal-1', + name: 'Test Goal', + status: DesignationSupportStatus.FullTime, + salaryOrHourly: DesignationSupportSalaryType.Salaried, + payRate: 50000, + hoursWorkedPerWeek: null, + benefits: 1500, + geographicLocation: 'Orlando, FL', +}; + describe('PdsGoalCalculator', () => { it('renders the setup step by default', () => { const { getByRole } = render( @@ -15,4 +30,41 @@ describe('PdsGoalCalculator', () => { getByRole('heading', { level: 6, name: 'Calculator Setup' }), ).toBeInTheDocument(); }); + + it('disables Continue on Setup step when a required field is missing', async () => { + const { findByRole } = render( + + + , + ); + + const continueButton = await findByRole('button', { name: /continue/i }); + await waitFor(() => expect(continueButton).toBeDisabled()); + }); + + it('disables Continue on Setup step when geographicLocation is not set', async () => { + const { findByRole } = render( + + + , + ); + + const continueButton = await findByRole('button', { name: /continue/i }); + await waitFor(() => expect(continueButton).toBeDisabled()); + }); + + it('enables Continue on Setup step when all required fields are filled', async () => { + const { findByRole } = render( + + + , + ); + + const continueButton = await findByRole('button', { name: /continue/i }); + await waitFor(() => expect(continueButton).toBeEnabled()); + }); }); diff --git a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx index a61dbf08f3..2c08820a2f 100644 --- a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx +++ b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx @@ -1,6 +1,10 @@ import React from 'react'; import { SectionList } from 'src/components/Reports/GoalCalculator/SharedComponents/SectionList'; import { DirectionButtons } from 'src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons'; +import { + AutosaveForm, + useAutosaveForm, +} from 'src/components/Shared/Autosave/AutosaveForm'; import { PdsGoalCalculatorStepEnum } from './PdsGoalCalculatorHelper'; import { ReimbursableExpensesStep } from './ReimbursableExpenses/ReimbursableExpensesStep'; import { SalaryStep } from './Salary/SalaryStep'; @@ -24,27 +28,38 @@ const CurrentStep: React.FC = () => { } }; -export const PdsGoalCalculator: React.FC = () => { +const StepContent: React.FC = () => { const { currentStep, stepIndex, steps, handleContinue, handlePreviousStep } = usePdsGoalCalculator(); - const sections = currentStep.sections; - + const { allValid } = useAutosaveForm(); const isLastStep = stepIndex === steps.length - 1; + return ( + <> + + {!isLastStep && ( + + )} + + ); +}; + +export const PdsGoalCalculator: React.FC = () => { + const { currentStep } = usePdsGoalCalculator(); + const sections = currentStep.sections; + return ( } mainContent={ - <> - - {!isLastStep && ( - - )} - + + + } /> ); diff --git a/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.tsx b/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.tsx index aeb2851d67..6d40ef1adc 100644 --- a/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.tsx +++ b/src/components/Reports/PdsGoalCalculator/Setup/SetupStep.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; import CalculateIcon from '@mui/icons-material/Calculate'; import { Autocomplete, @@ -21,6 +21,7 @@ import { CurrencyAdornment, PercentageAdornment, } from 'src/components/Reports/GoalCalculator/Shared/Adornments'; +import { useOptionalAutosaveForm } from 'src/components/Shared/Autosave/AutosaveForm'; import { useGetUserQuery } from 'src/components/User/GetUser.generated'; import { DesignationSupportSalaryType, @@ -47,20 +48,27 @@ export const SetupStep: React.FC = () => { () => yup.object({ name: yup.string().required(t('Goal Name is a required field')), - status: yup.string().nullable(), - salaryOrHourly: yup.string().nullable(), + status: yup + .string() + .required(t('Employment Status is a required field')), + salaryOrHourly: yup + .string() + .required(t('Pay Type is a required field')), payRate: yup .number() - .nullable() + .required(t('Pay Rate is a required field')) .min(0, t('Pay Rate must be a positive number')), hoursWorkedPerWeek: yup .number() - .nullable() + .required(t('Hours Worked is a required field')) .min(0, t('Hours Worked must be a positive number')), benefits: yup .number() - .nullable() + .required(t('Benefits is a required field')) .min(0, t('Benefits must be a positive number')), + geographicLocation: yup + .string() + .required(t('Geographic Multiplier is a required field')), }), [t], ); @@ -72,6 +80,20 @@ export const SetupStep: React.FC = () => { [goalGeographicConstantMap], ); + const autosaveForm = useOptionalAutosaveForm(); + const geographicLocationValue = calculation?.geographicLocation; + useEffect(() => { + if (!autosaveForm) { + return; + } + if (geographicLocationValue) { + autosaveForm.markValid('geographicLocation'); + } else { + autosaveForm.markInvalid('geographicLocation'); + } + return () => autosaveForm.markValid('geographicLocation'); + }, [autosaveForm, geographicLocationValue]); + const isSalaried = calculation?.salaryOrHourly === DesignationSupportSalaryType.Salaried; const isPartTime = calculation?.status === DesignationSupportStatus.PartTime; diff --git a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx index fa4860e5e6..6df496049b 100644 --- a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx +++ b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx @@ -25,6 +25,7 @@ interface TestComponentProps { showBackButton?: boolean; buttonTitle?: string; isEdit?: boolean; + isValid?: boolean; } const TestComponent: React.FC = ({ @@ -33,6 +34,7 @@ const TestComponent: React.FC = ({ showBackButton = false, buttonTitle, isEdit, + isValid, }) => ( = ({ showBackButton={showBackButton} buttonTitle={buttonTitle} isEdit={isEdit} + isValid={isValid} /> @@ -114,6 +117,24 @@ describe('DirectionButtons', () => { expect(await findByRole('button', { name: title })).toBeInTheDocument(); }); + it('disables Continue when isValid is false', async () => { + const { findByRole } = render(); + + expect(await findByRole('button', { name: 'Continue' })).toBeDisabled(); + }); + + it('enables Continue when isValid is true', async () => { + const { findByRole } = render(); + + expect(await findByRole('button', { name: 'Continue' })).toBeEnabled(); + }); + + it('leaves Continue enabled when isValid is not provided', async () => { + const { findByRole } = render(); + + expect(await findByRole('button', { name: 'Continue' })).toBeEnabled(); + }); + it('renders Discard button', async () => { const { findByRole } = render(); diff --git a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx index cb110d9525..b3607f7398 100644 --- a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx +++ b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx @@ -141,6 +141,7 @@ export const DirectionButtons: React.FC = ({ variant="contained" color="primary" onClick={overrideNext ?? handleNextStep} + disabled={isValid === false} > {buttonTitle ?? t('Continue')} From df50b1b7f944c1543cb5f8b8a7545ca750e709d5 Mon Sep 17 00:00:00 2001 From: zachery with an e <45150570+zweatshirt@users.noreply.github.com> Date: Thu, 16 Apr 2026 14:20:34 -0500 Subject: [PATCH 2/2] Refactor DirectionButtons to use disableNext prop for button state management --- .../PdsGoalCalculator/PdsGoalCalculator.tsx | 2 +- .../DirectionButtons/DirectionButtons.test.tsx | 16 ++++++++-------- .../DirectionButtons/DirectionButtons.tsx | 4 +++- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx index 2c08820a2f..e259c0f80f 100644 --- a/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx +++ b/src/components/Reports/PdsGoalCalculator/PdsGoalCalculator.tsx @@ -42,7 +42,7 @@ const StepContent: React.FC = () => { formTitle={currentStep.title} handleNextStep={handleContinue} handlePreviousStep={handlePreviousStep} - isValid={allValid} + disableNext={!allValid} /> )} diff --git a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx index 6df496049b..e91f76cb5b 100644 --- a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx +++ b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.test.tsx @@ -25,7 +25,7 @@ interface TestComponentProps { showBackButton?: boolean; buttonTitle?: string; isEdit?: boolean; - isValid?: boolean; + disableNext?: boolean; } const TestComponent: React.FC = ({ @@ -34,7 +34,7 @@ const TestComponent: React.FC = ({ showBackButton = false, buttonTitle, isEdit, - isValid, + disableNext, }) => ( = ({ showBackButton={showBackButton} buttonTitle={buttonTitle} isEdit={isEdit} - isValid={isValid} + disableNext={disableNext} /> @@ -117,19 +117,19 @@ describe('DirectionButtons', () => { expect(await findByRole('button', { name: title })).toBeInTheDocument(); }); - it('disables Continue when isValid is false', async () => { - const { findByRole } = render(); + it('disables Continue when disableNext is true', async () => { + const { findByRole } = render(); expect(await findByRole('button', { name: 'Continue' })).toBeDisabled(); }); - it('enables Continue when isValid is true', async () => { - const { findByRole } = render(); + it('enables Continue when disableNext is false', async () => { + const { findByRole } = render(); expect(await findByRole('button', { name: 'Continue' })).toBeEnabled(); }); - it('leaves Continue enabled when isValid is not provided', async () => { + it('leaves Continue enabled when disableNext is not provided', async () => { const { findByRole } = render(); expect(await findByRole('button', { name: 'Continue' })).toBeEnabled(); diff --git a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx index b3607f7398..d93617893a 100644 --- a/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx +++ b/src/components/Reports/Shared/CalculationReports/DirectionButtons/DirectionButtons.tsx @@ -21,6 +21,7 @@ interface DirectionButtonsProps { additionalApproval?: boolean; splitAsr?: boolean; disableSubmit?: boolean; + disableNext?: boolean; //Formik validation for submit modal isSubmission?: boolean; submitForm?: () => Promise; @@ -51,6 +52,7 @@ export const DirectionButtons: React.FC = ({ additionalApproval, splitAsr, disableSubmit, + disableNext, }) => { const { t } = useTranslation(); @@ -141,7 +143,7 @@ export const DirectionButtons: React.FC = ({ variant="contained" color="primary" onClick={overrideNext ?? handleNextStep} - disabled={isValid === false} + disabled={disableNext} > {buttonTitle ?? t('Continue')}