4.2.4 Drag — reorder (rank) + drag-into-sprint + drag-between-sprints over the virtualized list, reusing the Story-3.2 dnd-kit move contract (optimistic + snap-back)
Estimate: 32m · Depends on: 4.2.1, 4.2.3, 4.2.2
Make the backlog groomable by drag — reorder within a region, drag an issue into a sprint, and drag between sprints / back to the backlog — built on the SAME @dnd-kit move contract the board (Story 3.2) already ships, over the 4.2.3 virtualized list. Drawn per the 4.2.1 drag states. No new drag mechanism (reuse), no new card vocabulary.
Reuse the 3.2 contract. Render the rows inside a @dnd-kit/core DndContext + @dnd-kit/sortable exactly as the board does, reusing the boardMove.ts move-resolution + the drag overlay + the keyboard sensor (drag is keyboard-operable). The 3.2.5 useRowWindow virtualization keeps the dragged node attached when it scrolls out of the window (the same "render the active drag node + its neighbours" guard the board uses).
The three moves → 4.1.4 / 4.2.2 writes:
- Reorder within a region → 4.1.4
rankIssue(itemId, { beforeId?, afterId? }): compute the newbacklog_rankbetween the drop neighbours (a single-rowkeyBetweenwrite — never an N-row renumber), handling the append/prepend edges. - Drag backlog → sprint (or sprint → sprint) → 4.1.4
assignToSprint(single) with an optional rank placement within the target; the same-project guard backstops the write. - Drag sprint → backlog → 4.1.4
moveToBacklog(the issue reappears inbacklog_rankorder).
Optimistic + snap-back (the 3.2 board contract). Apply the move locally on drop, fire the write, and snap back + surface an error on failure — never leave the UI out of sync with the server. Counts (the sprint issue count, the backlog count header) update with the move.
Scale (finding #57). Drag composes with the virtualized + lazy-loaded list (4.2.3): a drop target may be a not-yet-loaded region (drop at the end of a sprint / backlog resolves to a rank-append, no need to have every row loaded), and the rank write stays O(1). The drag never forces a load-all.
Out of scope here: multi-select drag of N rows + the bulk bar (4.2.5 — single-row drag here); inline create + the ⋯ menu (4.2.5); the points/velocity numbers (4.3 / 4.6).
Acceptance criteria
- Rows render inside a
@dnd-kitDndContext/SortableContextreusing the Story-3.2boardMove.tsmove-resolution + drag overlay + keyboard sensor; dragging out of theuseRowWindow-virtualized list keeps the active node attached. - Reorder within a region writes a single
backlog_rankvia 4.1.4rankIssue(keyBetween, append/prepend edges handled) — strictly between the drop neighbours, no other row's rank changed; drag backlog→sprint / sprint→sprint callsassignToSprint(same-project guarded); drag sprint→backlog callsmoveToBacklog. - Moves are optimistic and SNAP BACK with a surfaced error on write failure; the sprint issue count + backlog count header update with the move; drag is keyboard-operable.
- Drag composes with the virtualized/lazy-loaded list (finding #57): a drop into a not-yet-fully-loaded region resolves correctly (append/edge) and never forces a load-all; rank writes stay O(1).
- Component tests assert reorder (single-row rank write), drag-into-sprint (assign + counts), drag-to-backlog (move + restore), and snap-back on failure.
Context refs
app/(authed)/boards/_components/boardMove.ts+BoardColumn.tsx(DndContext+useRowWindow+ the active-node-attached guard) +BoardCard.tsx(Story 3.2.3–3.2.5) — the dnd-kit move contract + virtualized-drag pattern to reuse- Story 4.1.4 (
rankIssue/assignToSprint/moveToBacklog) — the single-row writes the drops call; 4.2.2 — the bulk path 4.2.5 layers on (single here) design/backlog/backlog.mock.htmldrag-state panels (4.2.1) — the overlay + drop-highlight spec@dnd-kit/core/@dnd-kit/sortable/@dnd-kit/utilities(already inpackage.json); finding #57 (drag over a bounded list);motir-core/CLAUDE.md