Skip to content

[CSM Portal] closed cases are terminal; add Create related case as the reopen replacement - #1090

Merged
cloby99 merged 8 commits into
wso2-open-operations:mainfrom
rksk:csm-fix-reopen-closed-terminal
Jul 8, 2026
Merged

[CSM Portal] closed cases are terminal; add Create related case as the reopen replacement#1090
cloby99 merged 8 commits into
wso2-open-operations:mainfrom
rksk:csm-fix-reopen-closed-terminal

Conversation

@rksk

@rksk rksk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Purpose

Describe the problems, issues, or needs driving this feature/fix and include links to related issues in the following format: Resolves issue1, issue2, etc.

The CSM portal's case detail view was showing a "Reopen" action on every closed case. The backend's nextStates derivation advertised closed → reopened as 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

Describe the solutions that this feature/fix will introduce to resolve the problems described above

  1. Make closed a 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.
  2. Replace it with "Create related case": a new case can be filed as related to a recently-closed one, for when the underlying issue needs more follow-up.

Approach

Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email documentation@wso2.com to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here.

Backend (apps/csm-portal/backend):

  • nextStates(closed) in internal/handler/state.go returns an empty slice by default — a real reopen is never valid, so it's no longer advertised.
  • For a case still within its 60-day related-case window (standard case type, closed, closedOn within 60 days — the backing data source's own eligibility rule), nextStates instead returns ["reopened"]. This reuses the existing nextStates array as the single source of truth for available actions rather than adding a separate flag; reopened here is never a real transition, only a signal.
  • openapi.yaml's nextStates field doc explains the reuse.

Frontend (apps/csm-portal/webapp):

  • CaseActionBar renders the reopened entry 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.
  • Clicking it does not PATCH the case — onAction intercepts create_related_case before the generic state-transition logic and instead navigates to the existing new-case form pre-filled with relatedCaseId (and locked to the same project), via a new ?relatedCaseId=&relatedCaseNumber= query-param pattern matching the existing ?projectId= lock.
  • The case detail page shows a clickable "Related: CASE-XXXX" chip when the backend returns a relatedCase reference, linking straight to that case.
  • Added reopened to the frontend's CaseState union (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).
  • Fixed a pre-existing FE/BE field-name mismatch (closedAtclosedOn) discovered while wiring this up; it was unused, so no behavior changed.

No API contract change beyond the additive relatedCase/relatedCaseId fields and the nextStates reuse — nothing existing was renamed or removed except the now-terminal closed → reopened transition, which never succeeded anyway.

User stories

Summary of user stories addressed by this change

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

Brief description of the new feature or bug fix as it will appear in the release notes

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

Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter "N/A" plus brief explanation of why there's no doc impact

N/A — internal state machine and case-detail UI behavior, not separately documented.

Automation tests

  • Unit tests

    Code coverage information

Backend: table-driven tests in cases_test.go cover nextStates returning ["reopened"] vs [] across state/type/closedOn-window combinations. make test (vet + race-enabled unit tests) passes.

Frontend: CaseActionBar tests cover the button being absent when the backend hasn't flagged eligibility, rendering as a single primary button and dispatching create_related_case (never a raw "Reopened" transition) when eligible. pnpm build, pnpm test, and pnpm lint all pass (two pre-existing, unrelated test failures on origin/main were confirmed via a clean baseline checkout before this PR).

  • Integration tests

    Details about the test cases and coverage

N/A — no integration test suite for this component.

Security checks

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), frontend pnpm build + pnpm test + pnpm lint, on macOS.

Learning

N/A

Summary by CodeRabbit

  • New Features
    • Added “Create related case” flow for certain recently closed cases (based on closure date), including a related-case link on the case details page and pre-filled context on the create form.
  • Bug Fixes
    • Closed cases no longer show “reopen” as a standard state transition.
    • Related-case action now routes to related-case creation instead of attempting a reopen transition.
  • Documentation
    • Updated backend state-machine docs and API/UI schema descriptions for reopened/next-state behavior.
  • Tests
    • Expanded test coverage for related-case eligibility and UI action rendering/navigation.

…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.
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 242af541-ee5f-4072-a84a-580d50ae9839

📥 Commits

Reviewing files that changed from the base of the PR and between e71e4ed and 5d625b3.

📒 Files selected for processing (5)
  • apps/csm-portal/backend/internal/handler/cases_test.go
  • apps/csm-portal/backend/internal/handler/state.go
  • apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx
  • apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts
✅ Files skipped from review due to trivial changes (1)
  • apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
  • apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts
  • apps/csm-portal/backend/internal/handler/cases_test.go
  • apps/csm-portal/backend/internal/handler/state.go

📝 Walkthrough

Walkthrough

Closed cases now expose reopened as a related-case creation signal instead of a transition target. The change propagates through backend eligibility checks, API and frontend types, case detail/create navigation, and dashboard state rendering.

Changes

Related-case reopen flow

Layer / File(s) Summary
Backend closed-case handling
apps/csm-portal/backend/internal/handler/state.go, apps/csm-portal/backend/internal/handler/cases_test.go, apps/csm-portal/backend/openapi.yaml, apps/csm-portal/backend/README.md
caseStateClosed now returns no next states, reopened is emitted only for recently closed standard cases, and the behavior is documented in tests, OpenAPI, and project notes.
Case contracts
apps/csm-portal/webapp/src/api/backend/types.ts, apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
The backend and frontend case types rename closedAt to closedOn, add related-case fields, and document reopened as a special next-state value for related-case creation.
Case detail and create flow
apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts, apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx, apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx, apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx, apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseCreatePage.tsx
The detail page maps and renders related cases, create_related_case navigates to the create page, the action bar labels reopened as “Create related case,” and the create page submits relatedCaseId from the URL.
Dashboard reopened state
apps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.ts, apps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts, apps/csm-portal/webapp/src/features/csm-dashboard/components/CaseCompositionCharts.tsx
The dashboard state union and display mappings add reopened, and the composition chart assigns it a slice color.

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
Loading

Possibly related PRs

Suggested labels: Type/Bug, Type/New Feature

Suggested reviewers: cloby99

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: closed cases are terminal and reopen is replaced with Create related case.
Description check ✅ Passed The description follows the required template and covers purpose, goals, approach, testing, security, and release notes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…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.
@rksk rksk changed the title [CSM Portal] make closed case state terminal, drop stale reopen transition [CSM Portal] closed cases are terminal; add Create related case as the reopen replacement Jul 8, 2026
rksk added 2 commits July 8, 2026 16:02
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

@coderabbitai coderabbitai Bot 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.

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 value

Prefer a local variable over non-null assertion for c.relatedCase.

The c.relatedCase!.id non-null assertion is safe today (the chip only renders when c.relatedCase is 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 value

Use a distinct icon for “Create related case”. GitBranch is 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

📥 Commits

Reviewing files that changed from the base of the PR and between c9fbbef and e71e4ed.

📒 Files selected for processing (14)
  • apps/csm-portal/backend/README.md
  • apps/csm-portal/backend/internal/handler/cases_test.go
  • apps/csm-portal/backend/internal/handler/state.go
  • apps/csm-portal/backend/openapi.yaml
  • apps/csm-portal/webapp/src/api/backend/types.ts
  • apps/csm-portal/webapp/src/features/csm-cases/api/useGetCsmCaseDetail.ts
  • apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.test.tsx
  • apps/csm-portal/webapp/src/features/csm-cases/components/CaseActionBar.tsx
  • apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseCreatePage.tsx
  • apps/csm-portal/webapp/src/features/csm-cases/pages/CsmCaseDetailPage.tsx
  • apps/csm-portal/webapp/src/features/csm-cases/types/csmCases.ts
  • apps/csm-portal/webapp/src/features/csm-dashboard/components/CaseCompositionCharts.tsx
  • apps/csm-portal/webapp/src/features/csm-dashboard/types/abtDashboard.ts
  • apps/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

Comment thread apps/csm-portal/webapp/src/features/csm-dashboard/utils/abtDashboard.ts Outdated
rksk added 2 commits July 8, 2026 16:42
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.
@rksk

rksk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

rksk added 2 commits July 8, 2026 18:26
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.
@rksk

rksk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Addressed both nitpicks from the review in 5d625b3:

  • CaseActionBar.tsx: reopened/create_related_case now uses LinkIcon instead of GitBranch (which was already used for the unrelated "Raise internal Git issue" action).
  • CsmCaseDetailPage.tsx: narrowed c.relatedCase into a local relatedCase before the JSX instead of the c.relatedCase! non-null assertion inside the onClick closure.

@rksk

rksk commented Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@cloby99
cloby99 merged commit 9bc44b0 into wso2-open-operations:main Jul 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants