Skip to content

Commit

Permalink
Merge pull request #3186 from HHS/OPS-310/3098_can_details_form
Browse files Browse the repository at this point in the history
feat: add CAN detail form
  • Loading branch information
fpigeonjr authored Dec 11, 2024
2 parents df0872c + 2d22f15 commit b868ee2
Show file tree
Hide file tree
Showing 13 changed files with 623 additions and 84 deletions.
71 changes: 65 additions & 6 deletions frontend/cypress/e2e/canDetail.cy.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,85 @@
/// <reference types="cypress" />
import { terminalLog, testLogin } from "./utils";
import { getCurrentFiscalYear } from "../../src/helpers/utils.js";

beforeEach(() => {
testLogin("system-owner");
testLogin("budget-team");
});

afterEach(() => {
cy.injectAxe();
cy.checkA11y(null, null, terminalLog);
});

const can502Nickname = "SSRD";
const can502Description = "Social Science Research and Development";

const currentFiscalYear = getCurrentFiscalYear();

describe("CAN detail page", () => {
it("shows relevant CAN data", () => {
it("shows the CAN details page", () => {
cy.visit("/cans/502/");
cy.get("h1").should("contain", "G99PHS9"); // heading
cy.get("p").should("contain", "SSRD - 5 Years"); // sub-heading
cy.get("p").should("contain", can502Nickname); // sub-heading
cy.get("span").should("contain", "Nicole Deterding"); // team member
cy.get("span").should("contain", "Director Derrek"); // division director
cy.get("span").should("contain", "Program Support"); // portfolio
cy.get("span").should("contain", "Division of Data and Improvement"); // division
});
it("CAN Edit form", () => {
cy.visit("/cans/502/");
cy.get("#fiscal-year-select").select("2024");
cy.get("#edit").should("not.exist");
cy.get("#fiscal-year-select").select(currentFiscalYear);
cy.get("#edit").should("exist");
cy.get("#edit").click();
cy.get("h2").should("contain", "Edit CAN Details");
cy.get("#can-nickName").invoke("val").should("equal", can502Nickname);
cy.get("#can-nickName").clear();
cy.get(".usa-error-message").should("exist").contains("This is required information");
cy.get("#save-changes").should("be.disabled");
cy.get("#can-nickName").type("Test Can Nickname");
cy.get("#save-changes").should("not.be.disabled");
cy.get(".usa-error-message").should("not.exist");
cy.get("#description").invoke("val").should("equal", can502Description);
cy.get("#description").clear();
cy.get("#description").type("Test description.");
cy.get("#save-changes").click();
cy.get(".usa-alert__body").should("contain", "The CAN G99PHS9 has been successfully updated.");
cy.get("p").should("contain", "Test Can Nickname");
cy.get("dd").should("contain", "Test description.");
// revert back to original values
cy.get("#edit").click();
cy.get("#can-nickName").clear();
cy.get("#can-nickName").type(can502Nickname);
cy.get("#description").clear();
cy.get("#description").type(can502Description);
cy.get("#save-changes").click();
});
it("handles cancelling from CAN edit form", () => {
cy.visit("/cans/502/");
cy.get("#fiscal-year-select").select(currentFiscalYear);
// Attempt cancel without making any changes
cy.get("#edit").click();
cy.get("[data-cy='cancel-button']").click();
cy.get(".usa-modal__heading").should(
"contain",
"Are you sure you want to cancel editing? Your changes will not be saved."
);
cy.get("[data-cy='cancel-action']").click();
// Exit out of the cancel modal
cy.get("[data-cy='cancel-button']").click();
// Actual cancel event
cy.get("[data-cy='confirm-action']").click();
cy.get("h2").should("contain", "CAN Details");
cy.get("p").should("contain", can502Nickname);
cy.get("dd").should("contain", can502Description);
});
it("shows the CAN Spending page", () => {
cy.visit("/cans/504/spending");
cy.get("#fiscal-year-select").select("2043");
cy.get("h1").should("contain", "G994426"); // heading
cy.get("p").should("contain", "HS - 5 Years"); // sub-heading
cy.get("p").should("contain", "HS"); // sub-heading
// should contain the budget line table
cy.get("table").should("exist");
// table should have more than 1 row
Expand Down Expand Up @@ -75,14 +131,17 @@ describe("CAN detail page", () => {
cy.visit("/cans/504/funding");
cy.get("#fiscal-year-select").select("2024");
cy.get("h1").should("contain", "G994426"); // heading
cy.get("p").should("contain", "HS - 5 Years"); // sub-heading
cy.get("p").should("contain", "HS"); // sub-heading
cy.get("[data-cy=can-funding-info-card]")
.should("exist")
.and("contain", "EEXXXX20215DAD")
.and("contain", "5 Years")
.and("contain", "IDDA")
.and("contain", "09/30/25")
.and("contain", "2021");
.and("contain", "2021")
.and("contain", "Quarterly")
.and("contain", "Direct")
.and("contain", "Discretionary");
cy.get("[data-cy=budget-received-card]")
.should("exist")
.and("contain", "FY 2024 Funding Received YTD")
Expand Down
18 changes: 14 additions & 4 deletions frontend/src/api/opsAPI.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,28 +203,37 @@ export const opsApi = createApi({
query: (id) => `/cans/${id}`,
providesTags: ["Cans"]
}),
updateCan: builder.mutation({
query: ({ id, data }) => ({
url: `/cans/${id}`,
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: data
}),
invalidatesTags: ["Cans"]
}),
getCanFundingSummary: builder.query({
query: ({ ids, fiscalYear, activePeriod, transfer, portfolio, fyBudgets }) => {
const queryParams = [];

if (ids && ids.length > 0) {
ids.forEach(id => queryParams.push(`can_ids=${id}`));
ids.forEach((id) => queryParams.push(`can_ids=${id}`));
}

if (fiscalYear) {
queryParams.push(`fiscal_year=${fiscalYear}`);
}

if (activePeriod && activePeriod.length > 0) {
activePeriod.forEach(period => queryParams.push(`active_period=${period}`));
activePeriod.forEach((period) => queryParams.push(`active_period=${period}`));
}

if (transfer && transfer.length > 0) {
transfer.forEach(t => queryParams.push(`transfer=${t}`));
transfer.forEach((t) => queryParams.push(`transfer=${t}`));
}

if (portfolio && portfolio.length > 0) {
portfolio.forEach(p => queryParams.push(`portfolio=${p}`));
portfolio.forEach((p) => queryParams.push(`portfolio=${p}`));
}

if (fyBudgets && fyBudgets.length === 2) {
Expand Down Expand Up @@ -403,6 +412,7 @@ export const {
useUpdateUserMutation,
useGetCansQuery,
useGetCanByIdQuery,
useUpdateCanMutation,
useGetCanFundingSummaryQuery,
useGetNotificationsByUserIdQuery,
useGetNotificationsByUserIdAndAgreementIdQuery,
Expand Down
97 changes: 97 additions & 0 deletions frontend/src/components/CANs/CANDetailForm/CANDetailForm.hooks.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import React from "react";
import classnames from "vest/classnames";
import { useUpdateCanMutation } from "../../../api/opsAPI";
import useAlert from "../../../hooks/use-alert.hooks";
import suite from "./suite.js";

export default function useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode) {
const [nickName, setNickName] = React.useState(canNickname);
const [description, setDescription] = React.useState(canDescription);
const [showModal, setShowModal] = React.useState(false);
const [modalProps, setModalProps] = React.useState({
heading: "",
actionButtonText: "",
secondaryButtonText: "",
handleConfirm: () => {}
});
const [updateCan] = useUpdateCanMutation();
const { setAlert } = useAlert();

let res = suite.get();

const cn = classnames(suite.get(), {
invalid: "usa-form-group--error",
valid: "success",
warning: "warning"
});

const handleCancel = (e) => {
e.preventDefault();
setShowModal(true);
setModalProps({
heading: "Are you sure you want to cancel editing? Your changes will not be saved.",
actionButtonText: "Cancel Edits",
secondaryButtonText: "Continue Editing",
handleConfirm: () => cleanUp()
});
};

const handleSubmit = (e) => {
e.preventDefault();
const payload = {
number: canNumber,
portfolio_id: portfolioId,
nick_name: nickName,
description: description
};

setAlert({
type: "success",
heading: "CAN Updated",
message: `The CAN ${canNumber} has been successfully updated.`
});

updateCan({
id: canId,
data: payload
});

cleanUp();
};

const cleanUp = () => {
setNickName("");
setDescription("");
setShowModal(false);
setModalProps({
heading: "",
actionButtonText: "",
secondaryButtonText: "",
handleConfirm: () => {}
});
toggleEditMode();
};

const runValidate = (name, value) => {
suite(
{
...{ [name]: value }
},
name
);
};
return {
nickName,
setNickName,
description,
setDescription,
handleCancel,
handleSubmit,
runValidate,
res,
cn,
setShowModal,
showModal,
modalProps
};
}
94 changes: 94 additions & 0 deletions frontend/src/components/CANs/CANDetailForm/CANDetailForm.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import Input from "../../UI/Form/Input";
import TextArea from "../../UI/Form/TextArea";
import ConfirmationModal from "../../UI/Modals/ConfirmationModal";
import useCanDetailForm from "./CANDetailForm.hooks";

/**
* @typedef {Object} CANDetailFormProps
* @property {number} canId - CAN ID
* @property {string} canNumber - CAN number
* @property {string} canNickname - CAN nick name
* @property {string} canDescription - CAN description
* @property {number} portfolioId - Portfolio ID
* @property {Function} toggleEditMode - Function to toggle edit mode
*/

/**
* @component - The CAN Details form
* @param {CANDetailFormProps} props
* @returns {JSX.Element}
*/
const CANDetailForm = ({ canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode }) => {
const {
nickName,
setNickName,
description,
setDescription,
handleCancel,
handleSubmit,
runValidate,
res,
cn,
showModal,
setShowModal,
modalProps
} = useCanDetailForm(canId, canNumber, canNickname, canDescription, portfolioId, toggleEditMode);

return (
<form
onSubmit={(e) => {
handleSubmit(e);
}}
>
{showModal && (
<ConfirmationModal
heading={modalProps.heading}
setShowModal={setShowModal}
actionButtonText={modalProps.actionButtonText}
secondaryButtonText={modalProps.secondaryButtonText}
handleConfirm={modalProps.handleConfirm}
/>
)}
<Input
name="can-nickName"
label="CAN Nickname"
onChange={(name, value) => {
runValidate("can-nickName", value);
setNickName(value);
}}
value={nickName}
isRequired
messages={res.getErrors("can-nickName")}
className={cn("can-nickName")}
/>
<TextArea
maxLength={1000}
name="description"
label="Description"
value={description}
onChange={(name, value) => {
setDescription(value);
}}
/>
<div className="grid-row flex-justify-end margin-top-8">
<button
className="usa-button usa-button--unstyled margin-right-2"
data-cy="cancel-button"
onClick={(e) => handleCancel(e)}
>
Cancel
</button>
<button
id="save-changes"
className="usa-button"
disabled={nickName.length == 0 || res.hasErrors()}
data-cy="save-btn"
>
Save Changes
</button>
</div>
</form>
);
};

export default CANDetailForm;
Loading

0 comments on commit b868ee2

Please sign in to comment.