Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PROD-06/05] [TTAHUB-2942] Fix RTR goal selection #2191

Merged
merged 11 commits into from
Jun 5, 2024
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ parameters:
default: "mb/TTAHUB-2943/remove-models"
type: string
sandbox_git_branch: # change to feature branch to test deployment
default: "mb/TTAHUB-2530/update-RTR-objective-form"
default: "al-ttahub-2942-fix-number-of-goals"
type: string
prod_new_relic_app_id:
default: "877570491"
Expand Down
57 changes: 34 additions & 23 deletions frontend/src/components/GoalCards/GoalCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,14 @@ function GoalCards({
canMergeGoals,
shouldDisplayMergeSuccess,
dismissMergeSuccess,
goalBuckets,
}) {
const history = useHistory();
const [rttapaValidation, setRttapaValidation] = useState(false);

// Goal select check boxes.
const [selectedGoalCheckBoxes, setSelectedGoalCheckBoxes] = useState({});
const [allGoalsChecked, setAllGoalsChecked] = useState(false);
const [printAllGoals, setPrintAllGoals] = useState(false);

// Close/Suspend Reason Modal.
const [closeSuspendGoalIds, setCloseSuspendGoalIds] = useState([]);
Expand Down Expand Up @@ -116,29 +116,35 @@ function GoalCards({
goalsArr.reduce((obj, g) => ({ ...obj, [g.id]: checked }), {})
);

useEffect(() => {
const checkValues = Object.values(selectedGoalCheckBoxes);
if (checkValues.length
&& (checkValues.length === goals.length || checkValues.length === goalsCount)
&& checkValues.every((v) => v === true)) {
setAllGoalsChecked(true);
} else if (printAllGoals === true) {
setPrintAllGoals(false);
}
}, [selectedGoalCheckBoxes, allGoalsChecked, printAllGoals, goalsCount, goals.length]);

const selectAllGoalCheckboxSelect = (event) => {
const { target: { checked = null } = {} } = event;

// Preserve checked goals on other pages.
const thisPagesGoalIds = goals.map((g) => g.id);
const preservedCheckboxes = Object.keys(selectedGoalCheckBoxes).reduce((obj, key) => {
if (!thisPagesGoalIds.includes(parseInt(key, DECIMAL_BASE))) {
return { ...obj, [key]: selectedGoalCheckBoxes[key] };
}
return { ...obj };
}, {});

if (checked === true) {
setSelectedGoalCheckBoxes(makeGoalCheckboxes(goals, true));
setSelectedGoalCheckBoxes({ ...makeGoalCheckboxes(goals, true), ...preservedCheckboxes });
} else {
setSelectedGoalCheckBoxes({ ...makeGoalCheckboxes(goals, false), ...preservedCheckboxes });
}
};

// Check if all goals on the page are checked.
useEffect(() => {
const goalIds = goals.map((g) => g.id);
const countOfCheckedOnThisPage = goalIds.filter((id) => selectedGoalCheckBoxes[id]).length;
if (goals.length === countOfCheckedOnThisPage) {
setAllGoalsChecked(true);
} else {
setSelectedGoalCheckBoxes(makeGoalCheckboxes(goals, false));
setAllGoalsChecked(false);
setPrintAllGoals(false);
}
};
}, [goals, selectedGoalCheckBoxes]);

const handleGoalCheckboxSelect = (event) => {
const { target: { checked = null, value = null } = {} } = event;
Expand All @@ -149,10 +155,9 @@ function GoalCards({
}
};

const checkAllGoals = () => {
const allIdCheckBoxes = allGoalIds.reduce((obj, g) => ({ ...obj, [g]: true }), {});
const checkAllGoals = (isClear) => {
const allIdCheckBoxes = allGoalIds.reduce((obj, g) => ({ ...obj, [g]: !isClear }), {});
setSelectedGoalCheckBoxes(allIdCheckBoxes);
setPrintAllGoals(true);
};

const numberOfSelectedGoals = Object.values(selectedGoalCheckBoxes).filter((g) => g).length;
Expand All @@ -164,14 +169,14 @@ function GoalCards({
const selectedGoalIdsButNumerical = selectedCheckBoxes.map((id) => parseInt(id, DECIMAL_BASE));
const draftSelectedRttapa = goals.filter((g) => selectedGoalIdsButNumerical.includes(g.id) && g.goalStatus === 'Draft').map((g) => g.id);

const allSelectedGoalIds = (() => {
const allSelectedPageGoalIds = (() => {
const selection = goals.filter((g) => selectedGoalCheckBoxes[g.id]);
return selection.map((g) => g.ids).flat();
return selection.map((g) => g.id);
})();

const rttapaLink = (() => {
if (selectedCheckBoxes && selectedCheckBoxes.length) {
const selectedGoalIdsQuery = allSelectedGoalIds.map((id) => `goalId[]=${encodeURIComponent(id)}`).join('&');
const selectedGoalIdsQuery = allSelectedPageGoalIds.map((id) => `goalId[]=${encodeURIComponent(id)}`).join('&');
return `/recipient-tta-records/${recipientId}/region/${regionId}/rttapa/new?${selectedGoalIdsQuery}`;
}

Expand Down Expand Up @@ -232,7 +237,7 @@ function GoalCards({
allGoalsChecked={allGoalsChecked}
selectAllGoalCheckboxSelect={selectAllGoalCheckboxSelect}
selectAllGoals={checkAllGoals}
selectedGoalIds={allSelectedGoalIds}
pageSelectedGoalIds={allSelectedPageGoalIds}
perPageChange={perPageChange}
pageGoalIds={goals.map((g) => g.id)}
showRttapaValidation={showRttapaValidation}
Expand All @@ -241,6 +246,8 @@ function GoalCards({
canMergeGoals={canMergeGoals}
shouldDisplayMergeSuccess={shouldDisplayMergeSuccess}
dismissMergeSuccess={dismissMergeSuccess}
goalBuckets={goalBuckets}
allSelectedGoalIds={selectedGoalCheckBoxes}
/>
<div className="padding-x-3 padding-y-2">
{goals.map((goal, index) => (
Expand Down Expand Up @@ -291,6 +298,10 @@ GoalCards.propTypes = {
canMergeGoals: PropTypes.bool.isRequired,
shouldDisplayMergeSuccess: PropTypes.bool,
dismissMergeSuccess: PropTypes.func.isRequired,
goalBuckets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
goalIds: PropTypes.arrayOf(PropTypes.number),
})).isRequired,
};

GoalCards.defaultProps = {
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/GoalCards/GoalDataController.js
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,9 @@ function GoalDataController({
query,
mergedGoals || [],
);
setData(response);
const rolledUpGoalIds = response.allGoalIds.map((goal) => goal.id);
const goalBuckets = response.allGoalIds;
setData({ ...response, allGoalIds: rolledUpGoalIds, goalBuckets });
setError('');
// display success message if we have merged goals
setShouldDisplayMergedSuccess((mergedGoals && mergedGoals.length > 0));
Expand Down Expand Up @@ -259,6 +261,7 @@ function GoalDataController({
canMergeGoals={canMergeGoals}
shouldDisplayMergeSuccess={shouldDisplayMergeSuccess}
dismissMergeSuccess={dismissMergeSuccess}
goalBuckets={data.goalBuckets}
/>
</FilterContext.Provider>
</div>
Expand Down
44 changes: 36 additions & 8 deletions frontend/src/components/GoalCards/GoalsCardsHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,16 @@ export default function GoalCardsHeader({
allGoalsChecked,
selectAllGoalCheckboxSelect,
selectAllGoals,
selectedGoalIds,
pageSelectedGoalIds,
perPageChange,
pageGoalIds,
showRttapaValidation,
draftSelectedRttapa,
canMergeGoals,
shouldDisplayMergeSuccess,
dismissMergeSuccess,
allSelectedGoalIds,
goalBuckets,
}) {
const [goalMergeGroups, setGoalMergeGroups] = useState([]);
const history = useHistory();
Expand Down Expand Up @@ -71,8 +73,22 @@ export default function GoalCardsHeader({

const showAddNewButton = hasActiveGrants && hasButtonPermissions;
const onPrint = () => {
// See if we have goals selected.
let goalsToPrint = Object.keys(allSelectedGoalIds).filter(
(key) => allSelectedGoalIds[key],
).map((key) => parseInt(key, DECIMAL_BASE));

// If we don't just print the page.
if (!goalsToPrint.length) {
goalsToPrint = pageGoalIds;
}
// Get all the goals and associated goals from the buckets.
goalsToPrint = goalBuckets.filter(
(bucket) => goalsToPrint.includes(bucket.id),
).map((bucket) => bucket.goalIds).flat();

history.push(`/recipient-tta-records/${recipientId}/region/${regionId}/rttapa/print${window.location.search}`, {
sortConfig, selectedGoalIds: !selectedGoalIds.length ? pageGoalIds : selectedGoalIds,
sortConfig, selectedGoalIds: goalsToPrint,
});
};

Expand All @@ -89,6 +105,9 @@ export default function GoalCardsHeader({
return null;
})();

const hasGoalsSelected = pageSelectedGoalIds ? pageSelectedGoalIds.length > 0 : false;
const showClearAllAlert = numberOfSelectedGoals === count;

return (
<div className="padding-x-3 position-relative">
<div className="desktop:display-flex flex-1 desktop:padding-top-0 padding-top-2 bg-white">
Expand Down Expand Up @@ -199,7 +218,7 @@ export default function GoalCardsHeader({
className="display-flex flex-align-center margin-left-3 margin-y-0"
onClick={onPrint}
>
{`Preview and print ${selectedGoalIds.length > 0 ? 'selected' : ''}`}
{`Preview and print ${hasGoalsSelected ? 'selected' : ''}`}
</Button>
</div>
<div>
Expand All @@ -222,16 +241,20 @@ export default function GoalCardsHeader({
</Alert>
)}
{
!showRttapaValidation && allGoalsChecked && (numberOfSelectedGoals !== count)
!showRttapaValidation && allGoalsChecked
? (
<Alert className="margin-top-3" type="info" slim>
{`All ${numberOfSelectedGoals} goals on this page are selected.`}
{showClearAllAlert
? `All ${count} goals are selected.`
: `All ${pageSelectedGoalIds.length} goals on this page are selected.`}
<button
type="button"
className="usa-button usa-button--unstyled margin-left-1"
onClick={selectAllGoals}
onClick={() => selectAllGoals(showClearAllAlert)}
>
{`Select all ${count} goals`}
{showClearAllAlert
? 'Clear selection'
: `Select all ${count} goals`}
</button>
</Alert>
)
Expand Down Expand Up @@ -287,7 +310,7 @@ GoalCardsHeader.propTypes = {
allGoalsChecked: PropTypes.bool,
numberOfSelectedGoals: PropTypes.number,
selectAllGoals: PropTypes.func,
selectedGoalIds: PropTypes.arrayOf(PropTypes.string).isRequired,
pageSelectedGoalIds: PropTypes.arrayOf(PropTypes.number).isRequired,
perPageChange: PropTypes.func.isRequired,
pageGoalIds: PropTypes.oneOfType(
[PropTypes.arrayOf(PropTypes.number), PropTypes.number],
Expand All @@ -297,6 +320,11 @@ GoalCardsHeader.propTypes = {
canMergeGoals: PropTypes.bool.isRequired,
shouldDisplayMergeSuccess: PropTypes.bool,
dismissMergeSuccess: PropTypes.func.isRequired,
allSelectedGoalIds: PropTypes.shape({ id: PropTypes.bool }).isRequired,
goalBuckets: PropTypes.arrayOf(PropTypes.shape({
id: PropTypes.number,
goals: PropTypes.arrayOf(PropTypes.number),
})).isRequired,
};

GoalCardsHeader.defaultProps = {
Expand Down
54 changes: 53 additions & 1 deletion frontend/src/components/GoalCards/__tests__/GoalCards.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ const defaultUser = {

const baseGoals = [{
id: 4598,
ids: [4598],
ids: [4598, 4599],
goalStatus: 'In Progress',
createdOn: '2021-06-15',
goalText: 'This is goal text 1.',
Expand Down Expand Up @@ -220,6 +220,7 @@ const setGoals = jest.fn();
const history = createMemoryHistory();

const renderTable = ({ goals, goalsCount, allGoalIds = null }, user, hasActiveGrants = true) => {
const goalBuckets = !goals ? [] : goals.map((g) => ({ id: g.id, goalIds: g.ids }));
render(
<Router history={history}>
<AriaLiveContext.Provider value={{ announce: mockAnnounce }}>
Expand All @@ -246,6 +247,7 @@ const renderTable = ({ goals, goalsCount, allGoalIds = null }, user, hasActiveGr
allGoalIds={allGoalIds || goals.map((g) => g.id)}
shouldDisplayMergeSuccess={false}
dismissMergeSuccess={jest.fn()}
goalBuckets={goalBuckets}
/>
</UserContext.Provider>
</AriaLiveContext.Provider>
Expand Down Expand Up @@ -489,6 +491,27 @@ describe('Goals Table', () => {
expect(screen.queryByText(/7 selected/i)).toBeNull();
});

it('Shows the clear selection button and clears when clicked', async () => {
const selectAll = await screen.findByRole('checkbox', { name: /deselect all goals/i });
fireEvent.click(selectAll);
expect(await screen.findByText(/6 selected/i)).toBeVisible();

const selectAllPages = await screen.findByRole('button', { name: /select all 7 goals/i });
fireEvent.click(selectAllPages);

expect(screen.queryByText(/7 selected/i)).toBeVisible();

const clearSelection = await screen.findByRole('button', { name: /clear selection/i });
fireEvent.click(clearSelection);

expect(screen.queryByText(/7 selected/i)).toBeNull();
// verify all check boxes are unchecked.
const checkBoxes = screen.queryAllByTestId('selectGoalTestId');
checkBoxes.forEach((checkBox) => {
expect(checkBox.checked).toBe(false);
});
});

it('Deselect via pill', async () => {
const selectAll = await screen.findByRole('checkbox', { name: /deselect all goals/i });
fireEvent.click(selectAll);
Expand Down Expand Up @@ -604,6 +627,35 @@ describe('Goals Table', () => {

expect(history.push).toHaveBeenCalled();
});

it('calls print passing all goal ids on the page', async () => {
// print goals
const printButton = await screen.findByRole('button', { name: /Preview and print/i });
userEvent.click(printButton);
expect(history.push).toHaveBeenCalledWith('/recipient-tta-records/1000/region/1/rttapa/print', {
selectedGoalIds: [4598, 4599, 65479],
sortConfig: {
activePage: 1, direction: 'asc', offset: 0, sortBy: 'goalStatus',
},
});
});

it('calls print passing all selected goal ids', async () => {
// print goals
const printButton = await screen.findByRole('button', { name: /Preview and print/i });

// select the checkbox with the value of 4598.
const checkBox = screen.queryAllByTestId('selectGoalTestId')[0];
fireEvent.click(checkBox);

userEvent.click(printButton);
expect(history.push).toHaveBeenCalledWith('/recipient-tta-records/1000/region/1/rttapa/print', {
selectedGoalIds: [4598, 4599],
sortConfig: {
activePage: 1, direction: 'asc', offset: 0, sortBy: 'goalStatus',
},
});
});
});

describe('Context Menu with Different User Permissions', () => {
Expand Down
Loading