2.3.1 Delete a status that's in use — reassign referencing work items (finding #48 · option b)
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.
Acceptance criteria
- Deleting an in-use status WITH a valid
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). - Deleting an in-use status WITHOUT a target still throws
StatusInUseError(the UI's cue to prompt for a target). - Target validation: a target in another project, a non-existent target, or the status-being-deleted as its own target →
InvalidReassignTargetError(422); nothing is migrated or deleted. - The initial-status and last-terminal-status protections STILL fire even when a target is supplied — you can't reassign your way past them.
- Admin-gated (
NotProjectAdminErrorfor a non-owner); workspace-scoped (a cross-workspace status/target 404s). - UI: in-use delete opens the reassign modal with a status picker + the affected-count; unused delete stays one-click; success toast + revalidate.
- Vitest (real Postgres): migrate-N-items-then-delete happy path (items + revisions + removal); StatusInUse without target; invalid-target rejection; initial/last-terminal still blocked with a target; idempotent/rollback check. Quality gates green.
Context refs
lib/services/workflowsService.ts(2.2.5) — the currentdeleteStatus+ protections +assertProjectAdminto 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 bulkreassignStatusKey(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— addInvalidReassignTargetErrorbeside the 2.2.5 errors