Skip to content

feat: enable grant agreement editing on detail page - #6089

Open
josbell wants to merge 38 commits into
mainfrom
OPS-6013/edit-a-grant
Open

feat: enable grant agreement editing on detail page#6089
josbell wants to merge 38 commits into
mainfrom
OPS-6013/edit-a-grant

Conversation

@josbell

@josbell josbell commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

What changed

Enables editing of existing Grant agreements end-to-end. Previously, Grant editing was explicitly blocked in the UI with a "not developed yet" message. This PR lifts that restriction and wires up the full edit flow — metadata, grant numbers, and budget lines.

Backend

  • Full CRUD for GrantNumber with authorization, IDOR protection, cascade/disassociation behavior (deleting a grant number NULLs grant_number_id on its BLIs rather than blocking the delete), uniqueness enforcement, and audit history (GRANT_NUMBER_CREATED/UPDATED/DELETED).
  • GrantBudgetLineItem PUT/PATCH/DELETE with required-field validation and role/authorization checks, mirroring contract BLI coverage.
  • grant_number_id added to ALWAYS_DIRECT_EDIT_FIELDS so reassigning a BLI to a different grant number does not route through the change-request approval flow.
  • Atomic PATCH /agreements/{id}/edit-bundle support for grant numbers (create/update/delete in one request with rollback on failure).
  • Seed fixtures: grant numbers and BLIs on Agreement Set up for spike on Oracle DB #3 for manual testing.

Frontend

  • Removed all "Grant editing not available" guards: AgreementTableRow, AgreementDetailHeader, AgreementBudgetLinesHeader, AgreementBudgetLines.
  • AgreementEditForm renders grant-specific fields (NOFO number, total funding, funding period, ALN) and hides contract-only fields (contract type, procurement shop, vendor) when agreement type is GRANT.
  • AgreementEditorContext fixes an async race where project officer fields rendered empty on edit because the getUser() fetch hadn't resolved at mount time.
  • DELETE_GRANT_NUMBER reducer action reconciles BLIs in state immediately, moving them to the "not associated" group so the accordion is consistent before saving.
  • EditAgreementAndBudgetLines wires up the nav-away blocker (useNavigationBlocker) with an approval variant (when pending changes require DD approval). fireBundleSave now returns true/false so a failed save keeps the blocker modal open rather than silently proceeding.
  • Grant BLI validation is intentionally bypassed on the send-to-approval page — the suite's grant_number_id requirement would block DRAFT→PLANNED transitions for unlinked BLIs; the backend is the authoritative validator per transition.
  • New Cypress E2E suite (editGrantAgreement.cy.js): happy path (metadata edit, grant number edit, BLI edit, delete-grant-number-moves-BLI-to-not-associated), and unauthorized user blocked.

Issue

#6013

How to test

Pre-requisites

  • Docker stack running (docker compose up --build)
  • Log in as a user who is a team member on the grant (e.g. admin@example.com / superuser)
  • Have a second non-team-member account ready for authorization checks

  1. Edit from agreements table (new — was blocked before)

  2. Go to /agreements

  3. Find a Grant agreement in the list

  4. Hover the row — confirm the edit pencil icon is enabled and clickable

  5. Click it — confirm you land on the edit page (not a blocked/disabled state)


  1. Edit grant metadata

  2. Go to the grant's detail page (/agreements/)

  3. Click Edit

  4. Confirm grant-specific fields are present: NOFO Number, Total Funding, Nickname, Description (no Procurement Shop, no Contract Type, no Vendor)

  5. Change the NOFO number to something new

  6. Click Continue / Save

  7. Confirm success alert appears

  8. Navigate back — confirm the updated NOFO is shown


  1. Add a grant number

  2. Go to /agreements//budget-lines

  3. Click Edit

  4. In the Grant Numbers section, click Add Grant Number

  5. Fill in a number and description

  6. Save — confirm the new accordion appears with the grant number's label


  1. Edit a grant number

  2. Enter edit mode on the budget-lines page

  3. Hover over an existing grant number row — click the edit icon

  4. Change the description

  5. Save — confirm the updated description is shown


  1. Delete a grant number that has BLIs

  2. Enter edit mode on the budget-lines page

  3. Note which BLIs are under a grant number accordion (e.g. "Grant 1")

  4. Delete that grant number

  5. Confirm:

  • The "Grant 1" accordion disappears immediately in the editor
  • Those BLIs now appear under "Budget lines not associated with a grant number"
  1. Save — confirm success alert
  2. Reload — confirm the BLIs are still there under the not-associated group, not lost

  1. Add / edit / delete a budget line

  2. Enter edit mode on the budget-lines page

  3. Add a new BLI under a grant number accordion — fill in CAN, amount, date

  4. Save — confirm it appears in the list and the grant total updates

  5. Re-enter edit mode — edit the amount on an existing BLI

  6. Save — confirm the updated amount and totals

  7. Re-enter edit mode — delete a DRAFT BLI

  8. Save — confirm it's gone


  1. Nav-away blocker — unsaved changes

  2. Enter edit mode, make a change (e.g. edit a BLI amount) — do not save

  3. Click the browser back button or a nav link

  4. Confirm the "Save changes before leaving?" modal appears

  5. Click Leave without saving — confirm you navigate away and the change is discarded

  6. Repeat, this time click Save in the modal — confirm the save succeeds and you land at the


  1. Nav-away blocker — failed save keeps modal open

  2. Enter edit mode, make a change

  3. Navigate away to trigger the blocker modal

  4. With browser devtools, block the PATCH /agreements/.../edit-bundle request (network throttle → offline, or use a request blocker)

  5. Click Save in the modal

  6. Confirm: the error alert appears, the modal stays open, and you remain on the edit page (not navigated away)


  1. Lifecycle lock — in review

  2. Put the grant's BLIs into a state that triggers review (PLANNED BLIs with a pending chang

  3. Navigate to the detail/budget-lines page

  4. Confirm the Edit button is disabled with an appropriate tooltip (not the "not available fgone)


  1. Authorization — unauthorized user

  2. Log in as a user who is not a team member on the grant

  3. Go to the grant's detail page — confirm Edit button is absent or disabled


  1. Authorization — unauthorized user

  2. Log in as a user who is not a team member on the grant

  3. Go to the grant's detail page — confirm Edit button is absent or disabled

  4. Directly navigate to /agreements/edit/ — confirm an error/access-denied state

  5. Go to /agreements — hover the grant row — confirm the edit icon is disabled


  1. Audit history

  2. Make a metadata change, a grant number change, and a BLI change

  3. Navigate to the agreement's history/activity log

  4. Confirm entries for each change show the correct user, field, and timestamp

A11y impact

  • cy.checkA11y() runs on the grant edit flow in the E2E suite

Storybook

  • N/A — changes are page-specific or non-visual

Screenshots

N/A

Definition of Done Checklist

  • UI works as designed (UX team)
  • OESA: Code refactored for clarity
  • OESA: Dependency rules followed
  • Automated unit tests updated and passed
  • Automated integration tests updated and passed
  • Automated quality tests updated and passed
  • Automated load tests updated and passed
  • Automated a11y tests updated and passed
  • Automated security tests updated and passed
  • 90%+ Code coverage achieved
  • Form validations updated

Links

N/A

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Enables grant agreement editing across agreement details, grant numbers, budget lines, lifecycle controls, and navigation safeguards.

Changes:

  • Enables grant metadata and budget-line editing with lifecycle and authorization controls.
  • Adds unsaved-change navigation handling and deleted-association reconciliation.
  • Expands frontend, backend, E2E, and seed-data coverage.

Reviewed changes

Copilot reviewed 25 out of 26 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
frontend/src/types/ProcurementTrackerTypes.d.ts Reformats tracker step types.
frontend/src/pages/agreements/review/ReviewAgreement.hooks.test.js Tests grant validation behavior.
frontend/src/pages/agreements/review/ReviewAgreement.hooks.js Adjusts grant approval validation.
frontend/src/pages/agreements/review/EditAgreementAndBudgetLines.test.jsx Mocks navigation blocking.
frontend/src/pages/agreements/review/EditAgreementAndBudgetLines.jsx Adds navigation-away modals.
frontend/src/pages/agreements/details/AgreementDetails.test.js Tests enabled grant editing.
frontend/src/pages/agreements/details/AgreementDetails.jsx Removes grant edit restriction.
frontend/src/pages/agreements/details/AgreementBudgetLines.test.jsx Tests grant lifecycle locks.
frontend/src/pages/agreements/details/AgreementBudgetLines.jsx Enables grant BLI actions.
frontend/src/hooks/useNavigationBlocker.hooks.js Adds approval-aware destinations.
frontend/src/components/BudgetLineItems/CreateBLIsAndSCs/CreateBLIsAndSCs.jsx Exposes dirty state and reconciliation.
frontend/src/components/BudgetLineItems/CreateBLIsAndSCs/CreateBLIsAndSCs.hooks.test.js Tests deleted-link reconciliation.
frontend/src/components/BudgetLineItems/CreateBLIsAndSCs/CreateBLIsAndSCs.hooks.js Disassociates deleted component links.
frontend/src/components/BudgetLineItems/CreateBLIsAndSCs/CreateBLIAndSCs.test.js Tests lifecycle-based editing visibility.
frontend/src/components/BudgetLineItems/BudgetLinesForm/BudgetLinesForm.test.jsx Covers grant BLI submission.
frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.jsx Reseeds asynchronously loaded officers.
frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.hooks.test.js Tests officer reseeding.
frontend/src/components/Agreements/AgreementEditor/AgreementEditForm.hooks.test.js Tests grant field persistence.
frontend/src/components/Agreements/AgreementDetailHeader.jsx Enables grant detail editing.
frontend/src/components/Agreements/AgreementBudgetLinesHeader.jsx Adds lifecycle-specific locking.
frontend/cypress/e2e/editGrantAgreement.cy.js Adds grant editing E2E coverage.
backend/ops_api/tests/ops/grant_numbers/test_grant_number.py Tests deletion disassociation.
backend/ops_api/tests/ops/budget_line_items/test_grant_budget_line_item.py Covers grant BLI endpoints.
backend/ops_api/tests/ops/agreement/test_agreement.py Covers grant edits and authorization.
backend/data_tools/src/import_static_data/import_data.py Imports grant seed entities.
backend/data_tools/data/agreements_and_blin_data.json5 Adds grant testing data.
Suppressed comments (1)

frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.jsx:103

  • The same late-response race applies to the alternate project officer: a user selection made before the original officer fetch resolves is unconditionally overwritten here. Guard reseeding on untouched initial state or track field edits before applying the fetched value.
    useEffect(() => {
        if (alternateProjectOfficer?.id) {
            dispatch({ type: "SET_STATE", key: "selected_alternate_project_officer", value: alternateProjectOfficer });
        }
    }, [alternateProjectOfficer]);

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +98 to +101
// Bypass budget-line validation for grants (OPS-6013); an empty result set means no BLI
// errors gate the status change.
if (isGrant) {
return [];

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bypass is intentional by design. The suite's grant_number_id > 0 check would block every DRAFT→PLANNED status change for a grant BLI not yet linked to a grant number — but linking isn't a prerequisite for draft status changes. The frontend BLI validator is optimized for contract agreements (SC assignment, procurement-shop constraints) and doesn't have an equivalent grant-specific model. The backend is the authoritative safety net for required-field enforcement per transition. Updated the comment in the code to make this reasoning explicit.

Comment thread frontend/src/pages/agreements/review/EditAgreementAndBudgetLines.jsx Outdated
Comment thread frontend/src/hooks/useNavigationBlocker.hooks.js Outdated
Comment thread frontend/src/hooks/useNavigationBlocker.hooks.js
Comment thread frontend/cypress/e2e/editGrantAgreement.cy.js
Comment on lines +17 to +19
// useBlocker is used by useNavigationBlocker; stub it to return an idle state
// so the nav-away modal never fires in unit tests (integration/E2E covers that).
useBlocker: () => ({ state: "idle", location: null, proceed: vi.fn(), reset: vi.fn() })

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The blocker modal save/proceed flow is covered at the unit level in EditAgreementAndBudgetLines.test.jsx (nav-away blocker modal describe block — 3 tests: save-and-proceed, leave-without-saving, approval-copy variant). E2E coverage of the blocker via Cypress would require real navigation interception which is out of scope for this story slice — the unit tests are the appropriate layer per the project's test-type decision matrix.

Comment on lines +93 to +97
useEffect(() => {
if (projectOfficer?.id) {
dispatch({ type: "SET_STATE", key: "selected_project_officer", value: projectOfficer });
}
}, [projectOfficer]);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged — the race is documented in the existing code comment ("Known race: if the user changes the officer field before getUser() resolves..."). The fetch is fast relative to the time it takes to navigate to and interact with the officer field, making this a low-likelihood scenario in practice. Fixing it properly (tracking whether the field has been edited before dispatching) is out of scope for this story; the comment flags it for a future pass.

@rajohnson90

Copy link
Copy Markdown
Contributor

Suggestions per the AI code review:
Suggestions

  1. Add a unit test in EditAgreementAndBudgetLines.test.jsx that forces the blocker into "blocked" state and asserts navigate/alert-redirect
    fires exactly once — closing the gap left by the "integration/E2E covers that" comment.
  2. Consider a short comment at the ReviewAgreement.hooks.js isGrant bypass clarifying whether this is a deliberate permanent design choice
    (grants have no equivalent required-field model yet) or a placeholder pending grant-specific validation rules.
  3. Leave a note near CreateBLIsAndSCs.hooks.js's internal useBlocker/blockerDisabledForCreateAgreement explaining that the review screen
    relies on currentStep never being passed, so a future caller doesn't reintroduce a live second blocker.

@Santi-3rd

Copy link
Copy Markdown
Contributor

Scope discrepancy — PR body describes a backend change that isn't in the diff

The PR description dedicates a whole section to backend/ops_api/ops/services/budget_line_items.py (inlining_validation_change_status_higher_than_draft, removing _validate_date_within_sc_window). That file is not in the PR's changed-files list, and the methods still exist on main/the branch tip. Either the change was dropped/reverted before pushing, landed in a separate PR, or the description is stale.

This matters because the frontend leans on backend validation as the safety net (see next point). Reviewers should confirm whether that SC-window removal is actually intended to ship here. Recommend either restoring the change or trimming the description.

@josbell josbell self-assigned this Aug 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 34 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.jsx:164

  • The async officer response can overwrite a selection the user makes while the request is in flight, as the preceding comment acknowledges. That silently restores the old officer and can save the wrong value. Track whether each field has been touched (or otherwise ignore stale fetch results) before dispatching the reseed; timing alone is not a safe guard.
    useEffect(() => {
        if (projectOfficer?.id) {
            dispatch({ type: "SET_STATE", key: "selected_project_officer", value: projectOfficer });
        }
    }, [projectOfficer]);

frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.hooks.js:225

  • Changing this collection from IDs to full objects breaks the existing approval-message path in CreateBLIsAndSCs.hooks.js:568-570, which still treats each entry as an ID (budgetLines.find((bl) => bl.id === id)). With the real reducer, every lookup now returns undefined, so deleting a PLANNED/IN_EXECUTION BLI is incorrectly reported as “Agreement Updated” instead of “Changes Sent to Approval.” Update that consumer and its tests to use the object shape (or preserve the ID contract separately).
                // Store the full BLI object so the save path can inspect its status for
                // approval routing (isDeletionRoutedToApproval). The deletion API call
                // uses .id from each object; see handleDeletions in CreateBLIsAndSCs.hooks.js.
                deleted_budget_line_items_ids: action.payload.id
                    ? [...state.deleted_budget_line_items_ids, action.payload]

frontend/src/components/Agreements/AgreementEditor/AgreementEditForm.hooks.test.js:709

  • This test does not cover the real ALN save field: editor state and the API schema use aln_numbers (plural), but the fixture and assertion use aln_number, so the test only verifies that an unrelated property survives the helper. It also uses funding_period_months: 24, while the backend accepts only 12 or 18. Use aln_numbers with the API's integer-array shape and a valid funding period so this test exercises a request the server can actually persist.
                nofo_number: "NOFO-UPDATED",
                aln_number: "10.001",
                funding_period_months: 24,

frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.hooks.js:123

  • This reconciliation only handles persisted BLIs with a services_component_id. A BLI added during the current edit has a temporary ID and only services_component_number; deleting its newly added services component leaves that number/grouping intact, so it remains displayed under a component that no longer exists. Clear the link when either the persisted ID is removed or the BLI's component number matches the deleted component.
                budget_line_items: state.budget_line_items.map((bli) => {
                    if (bli.services_component_id != null && !remainingScIds.has(bli.services_component_id)) {

frontend/src/components/Agreements/AgreementEditor/AgreementEditorContext.hooks.js:193

  • This misses BLIs created during the current edit: those carry grant_number_number but no grant_number_id. Deleting their newly added grant number therefore leaves them grouped under the removed number until save, rather than moving them to “not associated” as intended. Reconcile both persisted ID links and temporary number-based links.
                budget_line_items: state.budget_line_items.map((bli) => {
                    if (bli.grant_number_id != null && !remainingGnIds.has(bli.grant_number_id)) {
                        return { ...bli, grant_number_id: null, grant_number_number: 0 };

Comment thread frontend/src/pages/agreements/review/EditAgreementAndBudgetLines.jsx Outdated
@josbell
josbell marked this pull request as ready for review August 18, 2026 20:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants