[CSM Portal] closed cases are terminal; add Create related case as the reopen replacement - #1090
Conversation
…ition nextStates(closed) advertised "reopened" as a valid transition, so the portal rendered a Reopen action on every closed case. The backing data source has no transition out of closed, so the action always failed at the data source. Closed is terminal end-to-end now; nextStates(closed) returns no further transitions.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (4)
📝 WalkthroughWalkthroughClosed cases now expose ChangesRelated-case reopen flow
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant CaseActionBar
participant CsmCaseDetailPage
participant CsmCaseCreatePage
participant BackendAPI
CaseActionBar->>CsmCaseDetailPage: onAction("create_related_case", "reopened")
CsmCaseDetailPage->>CsmCaseCreatePage: navigate("/cases/new?projectId&relatedCaseId&relatedCaseNumber")
CsmCaseCreatePage->>BackendAPI: postCase.mutateAsync({ relatedCaseId, ... })
BackendAPI-->>CsmCaseCreatePage: created related case
Possibly related PRs
Suggested labels: Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 golangci-lint (2.12.2)level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
…ement Closed cases can no longer be reopened (previous commit), so this adds the replacement flow: a backend-computed canCreateRelatedCase flag (closed, within its 60-day related-case window, standard case type only) drives a "Create related case" action on the case detail page. Picking it opens the new-case form pre-filled with relatedCaseId; the resulting case is linked, and the original case's detail page shows a link back to it via the backend-supplied relatedCase reference.
Replaces the separate canCreateRelatedCase field with a reuse of the existing nextStates array: a closed case within its 60-day related-case window now returns nextStates: ["reopened"] instead of a boolean flag, since the frontend already treats nextStates as the single source of truth for available case actions. `reopened` here is never a real transition (the data source has none) — it's a signal the action bar renders as "Create related case" and dispatches by navigating to the new-case form, never by patching the case state. Also adds `reopened` to the frontend's CaseState union (it was already a real backend case state that had no FE label/color mapping) and fills in the resulting exhaustive-map gaps.
…-terminal # Conflicts: # apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx # apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx (1)
1012-1022: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valuePrefer a local variable over non-null assertion for
c.relatedCase.The
c.relatedCase!.idnon-null assertion is safe today (the chip only renders whenc.relatedCaseis truthy), but it's fragile — a future refactor of the conditional could silently break the assertion. Extracting a local variable lets TypeScript narrow naturally.♻️ Proposed refactor
{c.relatedCase && (() => { const rc = c.relatedCase; return ( <Chip size="small" variant="outlined" clickable icon={<LinkIcon size={14} />} label={`Related: ${rc.caseNumber ?? rc.id}`} onClick={() => navigate(`/cases/${rc.id}`)} sx={{ fontWeight: 600 }} /> ); })()}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx` around lines 1012 - 1022, The click handler in CsmCaseDetailPage should avoid the non-null assertion on c.relatedCase and instead rely on a locally narrowed variable. Update the related-case rendering block by assigning c.relatedCase to a local variable before the conditional logic, then use that variable for both the label and navigate call so TypeScript can narrow naturally and the Chip remains safe if the conditional is refactored later.apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx (1)
127-135: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse a distinct icon for “Create related case”.
GitBranchis already used for “Raise internal Git issue”, so reusing it here makes two different actions look the same; a link/plus-style icon would read more clearly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx` around lines 127 - 135, The “reopened” action in CaseActionBar.tsx is using the same GitBranch icon as “Raise internal Git issue,” which makes two different actions visually identical. Update the reopened/create_related_case entry in the action config to use a distinct link/plus-style icon, and keep the change localized to the action definition used by CaseActionBar and onAction so the two actions are clearly differentiated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts`:
- Around line 73-74: The inline comment for the `reopened` entry in
`makeAbtStateMap` conflicts with the `CaseState` semantics defined in this
module. Update the comment near the `reopened: "info"` mapping so it describes
`reopened` as a closed-case follow-up signal that appears only in `nextStates`
and enables “Create related case,” not as an active case state like
open/work_in_progress. Use the `CaseState` type and `makeAbtStateMap` as the
reference points when rewriting the wording.
---
Nitpick comments:
In `@apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx`:
- Around line 127-135: The “reopened” action in CaseActionBar.tsx is using the
same GitBranch icon as “Raise internal Git issue,” which makes two different
actions visually identical. Update the reopened/create_related_case entry in the
action config to use a distinct link/plus-style icon, and keep the change
localized to the action definition used by CaseActionBar and onAction so the two
actions are clearly differentiated.
In `@apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx`:
- Around line 1012-1022: The click handler in CsmCaseDetailPage should avoid the
non-null assertion on c.relatedCase and instead rely on a locally narrowed
variable. Update the related-case rendering block by assigning c.relatedCase to
a local variable before the conditional logic, then use that variable for both
the label and navigate call so TypeScript can narrow naturally and the Chip
remains safe if the conditional is refactored later.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 929264e7-95fd-43f1-a07c-8bd7726fdb4c
📒 Files selected for processing (14)
apps/csm-portal/backend/README.mdapps/csm-portal/backend/internal/handler/cases_test.goapps/csm-portal/backend/internal/handler/state.goapps/csm-portal/backend/openapi.yamlapps/csm-portal/webapp/src/api/backend/types.tsapps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.tsapps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsxapps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsxapps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseCreatePage.tsxapps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsxapps/csm-portal/webapp/src/features/csm-cases/types/csmCases.tsapps/csm-portal/webapp/src/features/csm-dashboard/components/CaseCompositionCharts.tsxapps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.tsapps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts
✅ Files skipped from review due to trivial changes (2)
- apps/csm-portal/backend/README.md
- apps/csm-portal/backend/openapi.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
- apps/csm-portal/backend/internal/handler/cases_test.go
Per CodeRabbit review on PR wso2-open-operations#1090: the comment read as if reopened were a real active case state, contradicting the CaseState doc that it only ever appears as a nextStates signal on closed cases.
…dCase Self-review catch: canCreateRelatedCase parsed "closedOn" with time.Parse(time.RFC3339, ...) only. The frontend already has to tolerate a zoneless "YYYY-MM-DD HH:MM:SS" shape from this same field (src/utils/dateTime.ts normalizeBackendTimestamp) for some upstream data, so the strict RFC 3339 parse would silently return false — hiding "Create related case" on an otherwise-eligible case — whenever the entity service passes that shape through. Added a same-shape fallback parser on the backend side, treated as UTC to match the frontend's handling.
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
Verified against real ServiceNow DEV: the entity service's SN case adapter never populates "closedOn" at all, so canCreateRelatedCase could never actually fire against live SN data despite being logically correct. updatedOn is always present and, for a closed case (normally terminal), is a reasonable stand-in for the close time. This is a best-effort UI signal only — the data source's own closed+60-day rule still runs server-side when the related case is actually created, so a stale fallback here fails cleanly there rather than causing incorrect behavior.
…l assertion - reopened/create_related_case now uses LinkIcon instead of GitBranch, which was already used for the unrelated "Raise internal Git issue" action. - Narrow c.relatedCase once into a local before the JSX instead of a c.relatedCase! assertion inside the onClick closure.
|
Addressed both nitpicks from the review in 5d625b3:
|
|
@coderabbitai resume |
✅ Action performedReviews resumed. |
Purpose
The CSM portal's case detail view was showing a "Reopen" action on every closed case. The backend's
nextStatesderivation advertisedclosed → reopenedas a valid transition, but the backing data source has no outbound transition defined for a closed case at all, so attempting the reopen always failed server-side. The action was surfaced to users with no way for it to ever succeed, and closed cases had no alternative way to continue the conversation.Goals
closeda true terminal state in the portal's case state machine, matching the backing data source's actual transition rules, so the Reopen action is no longer offered.Approach
Backend (
apps/csm-portal/backend):nextStates(closed)ininternal/handler/state.goreturns an empty slice by default — a real reopen is never valid, so it's no longer advertised.casetype, closed,closedOnwithin 60 days — the backing data source's own eligibility rule),nextStatesinstead returns["reopened"]. This reuses the existingnextStatesarray as the single source of truth for available actions rather than adding a separate flag;reopenedhere is never a real transition, only a signal.openapi.yaml'snextStatesfield doc explains the reuse.Frontend (
apps/csm-portal/webapp):CaseActionBarrenders thereopenedentry as a "Create related case" primary button (via the existing per-target-state config, same as every other lifecycle button) instead of a generic "Reopened" transition button.onActioninterceptscreate_related_casebefore the generic state-transition logic and instead navigates to the existing new-case form pre-filled withrelatedCaseId(and locked to the same project), via a new?relatedCaseId=&relatedCaseNumber=query-param pattern matching the existing?projectId=lock.relatedCasereference, linking straight to that case.reopenedto the frontend'sCaseStateunion (it was already a real backend case state with no FE label/color mapping — a pre-existing gap) and filled in the resulting map entries (state label, chip color, dashboard chart slice color).closedAt→closedOn) discovered while wiring this up; it was unused, so no behavior changed.No API contract change beyond the additive
relatedCase/relatedCaseIdfields and thenextStatesreuse — nothing existing was renamed or removed except the now-terminalclosed → reopenedtransition, which never succeeded anyway.User stories
As a CS engineer, I don't see a Reopen action on a closed case that can never actually be reopened, so I don't attempt an action guaranteed to fail. When a customer's issue resurfaces shortly after closing, I can file a new case linked back to the original instead.
Release note
Fixed: the Reopen action was shown on closed cases even though reopening was never possible. Closed cases now offer "Create related case" instead, available for 60 days after closing.
Documentation
N/A — internal state machine and case-detail UI behavior, not separately documented.
Automation tests
Backend: table-driven tests in
cases_test.gocovernextStatesreturning["reopened"]vs[]across state/type/closedOn-window combinations.make test(vet + race-enabled unit tests) passes.Frontend:
CaseActionBartests cover the button being absent when the backend hasn't flagged eligibility, rendering as a single primary button and dispatchingcreate_related_case(never a raw "Reopened" transition) when eligible.pnpm build,pnpm test, andpnpm lintall pass (two pre-existing, unrelated test failures onorigin/mainwere confirmed via a clean baseline checkout before this PR).N/A — no integration test suite for this component.
Security checks
go vet ./...andeslintboth run clean)Samples
N/A
Related PRs
None
Migrations (if applicable)
N/A — no schema or data migration involved.
Test environment
Local: backend
make test(go vet + go test -race), frontendpnpm build+pnpm test+pnpm lint, on macOS.Learning
N/A
Summary by CodeRabbit