MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-86

2.3.1 Delete a status that's in use — reassign referencing work items (finding #48 · option b)

Done
Description

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 (NotProjectAdminError for 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 current deleteStatus + protections + assertProjectAdmin to extend
  • lib/services/workItemsService.ts + workItemRevisionsService (2.2.4) — the status-change + revision pipeline to reuse for the per-item migration
  • lib/repositories/workItemRepository.tscountByProjectAndStatusKey (2.2.5) + a new bulk reassignStatusKey(projectId, fromKey, toKey, tx) / per-item find for the revisions
  • app/(authed)/settings/project/workflow/_components/WorkflowEditor.tsx + actions.ts (2.2.5) — the delete affordance + Server Action to extend with the reassign modal
  • lib/workflows/errors.ts — add InvalidReassignTargetError beside the 2.2.5 errors