2.2.5 Workflow management API + project-settings UI
Estimate: 22m · Depends on: 2.2.3
Ship the write surface so a project admin can edit their workflow: add/rename/recolor/reorder statuses, add/remove transitions, and flip workflow_policy_mode. Built as Server Actions following the 4-layer rule (Action → Service → Repository → Prisma) and mounted under the existing project-settings route shell from Story 1.5.
Service write methods:
createStatus({ projectId, workspaceId, key, label, category, color, position })— inserts, validateskeyis project-unique, position via fractional-indexing.updateStatus({ statusId, workspaceId, label?, category?, color?, position?, isInitial? })— partial update; flippingisInitialto true inside one tx unsets the previous initial-status row (atomic to satisfy the partial unique index from 2.2.1).deleteStatus({ statusId, workspaceId })— refuses (throwsStatusInUseError→ 422) if anywork_item.statusstill references this status key OR if it's the initial status OR if removing it would leave the project with zerocategory = 'done'statuses (the last terminal-status invariant). Same-tx cleanup deletes allworkflow_transitionrows pointing to or from this status.addTransition({ projectId, workspaceId, fromStatusId, toStatusId })— inserts; the unique constraint from 2.2.1 makes duplicate inserts idempotent (catch P2002 → return existing).removeTransition({ transitionId, workspaceId }).setPolicyMode({ projectId, workspaceId, mode })— flips the project'sworkflow_policy_modecolumn.
Permission gate: every write method calls workspacesService.assertProjectAdmin(userId, projectId, workspaceId). v1 routes "project admin" to the workspace owner role added in Story 1.6.5 (finding #36) — full per-project RBAC is Epic 6 work. This is intentionally narrow: the durable shape is a permission check that exists; the durable scope expansion (to a real "project admin" role) is the Epic-6 add. Owner gate today, admin gate later — same gate function, swapped implementation. Not a "shortcut now, real later" — the gate is the durable shape; the role-set behind it widens in Epic 6.
UI: /settings/project/[projectKey]/workflow route under the existing project-settings shell. Two tabs: Statuses (drag-to-reorder list, inline edit, add/delete) and Transitions (a matrix grid: rows = from-status, columns = to-status, click cell to toggle). A header toggle controls policy mode (restricted vs open); a banner under the toggle explains the consequence ("Open mode: any status can transition to any other"). Uses the Story 1.5 shell primitives (Modal, Toast, Pill); no new design-system components needed beyond a color-swatch picker that composes from the existing color tokens. All actions are optimistic with toast confirmation; failures revert + toast the typed-error message.
Acceptance criteria
- Six service write methods shipped with their typed errors; every one calls
assertProjectAdminfirst and surfaces the typed-error message on failure. - Initial-status flip is atomic (the previous initial-status row is unset in the same tx); the partial unique index never sees two true rows.
- Delete protections: deleting a status referenced by any
work_item→ 422STATUS_IN_USE; deleting the initial status → 422CANNOT_DELETE_INITIAL_STATUS; deleting the lastcategory = 'done'status → 422CANNOT_DELETE_LAST_TERMINAL_STATUS. /settings/project/[projectKey]/workflowroute renders the two tabs, drives all six service writes through Server Actions, optimistic UI + toast confirmation, A11y (axe) sweep on the page passes.- Vitest covers the matrix + the three delete-protections + the atomic initial-status flip.
- Playwright spec covers: an owner edits the default workflow (rename "In Review" → "QA"), adds a "Cancelled" terminal status (
category: done), removes the back-transitionin_review→in_progress, flips policy mode → restricted re-applied.
Context refs
- Story 1.5's
AppLayout+ settings-route shell - Story 1.6.5's
JobsDashboard— closest precedent for a project-settings-style admin surface with a Server Action + permission gate - Story 1.2.5's typed-error catalog +
WorkspaceMembershipErrorshape - Story 1.6.5's owner gate + finding #36's
workspaceRolesService— the temporary "owner == admin" implementation seam - Design system tokens for category colors (semantic Pill tones, post-finding-#35 fix)