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')}