Estimate: 16m · Depends on: 2.2.4, 2.2.5
Resolves finding #48 (Yue chose option b). Today (2.2.5)
deleteStatus REFUSES with StatusInUseError when any work item
references the status. This Subtask makes an in-use status deletable via the Jira-style
delete-with-reassign flow: the admin picks a TARGET status, every
referencing work item is migrated to it, and only then is the status removed — all in one
transaction. Why this shape is forced: work_item.status is a
free-form String with no FK to workflow_status (2.2.1), so a
delete can't cascade; the service MUST reassign the referencing rows or they'd dangle.
Why this Subtask sits in Story 2.3, not Story 2.2: the flow spans two
surfaces — the workflow-settings page (Story 2.2's WorkflowEditor) AND the
work-item mutation pipeline (this Story's domain) — and its E2E only makes sense once a
real issue mutation runs through it. Story 2.2 stays scoped to the workflow primitive;
Story 2.3 owns the issue-mutation flows that consume the primitive.
Service. Extend the delete path with an optional target:
deleteStatus({ userId, workspaceId, statusId, reassignToStatusId? }) (or a
sibling reassignAndDeleteStatus). Admin-gated (same assertProjectAdmin).
In one transaction:
(1) the existing protections still fire FIRST and unconditionally — CANNOT_DELETE_INITIAL_STATUS
and CANNOT_DELETE_LAST_TERMINAL_STATUS (reassignment does NOT let you delete the
initial or the last terminal status);
(2) if the status is in use and reassignToStatusId is absent →
StatusInUseError as today (the UI then prompts for a target);
(3) validate the target: it exists in the SAME project and isn't the status being deleted
(else InvalidReassignTargetError → 422);
(4) migrate every work_item in the project whose status = the
deleted status's key to the target's key, recording a
status-change revision per item (reusing 2.2.4's revision pipeline — diff
{ status: { from, to } }, changeKind: 'updated');
(5) delete the status's transitions + the status (2.2.5's cascade).
UI. In WorkflowEditor, clicking delete on an in-use status
opens a confirm modal that says "N issues use this status — move them to:" with a
status-picker (the project's other statuses); confirming calls the reassign path. Deleting
an unused status keeps the current one-click delete (no target needed). Optimistic + toast.
reassignToStatusId: all referencing work items migrate to the target (status string updated), one status-change revision is written per migrated item, the status + its transitions are removed — all atomically (a forced failure rolls back the whole thing, items included).StatusInUseError (the UI's cue to prompt for a target).InvalidReassignTargetError (422); nothing is migrated or deleted.NotProjectAdminError for a non-owner); workspace-scoped (a cross-workspace status/target 404s).lib/services/workflowsService.ts (2.2.5) — the current deleteStatus + protections + assertProjectAdmin to extendlib/services/workItemsService.ts + workItemRevisionsService (2.2.4) — the status-change + revision pipeline to reuse for the per-item migrationlib/repositories/workItemRepository.ts — countByProjectAndStatusKey (2.2.5) + a new bulk reassignStatusKey(projectId, fromKey, toKey, tx) / per-item find for the revisionsapp/(authed)/settings/project/workflow/_components/WorkflowEditor.tsx + actions.ts (2.2.5) — the delete affordance + Server Action to extend with the reassign modallib/workflows/errors.ts — add InvalidReassignTargetError beside the 2.2.5 errors