[7/7] Reorder board columns by dragging - #279
Conversation
There was a problem hiding this comment.
Pull request overview
Adds draggable reordering of task board columns (statuses) with a server-authoritative reorder API, plus an updated agent tool schema description clarifying the meaning of source.
Changes:
- Implement column drag-and-drop within the existing board
DndContext(columns + cards), including namespaced column IDs and drop-target resolution across columns/lane bodies/cards. - Add
POST /api/task-statuses/reorderand corresponding DB/store/client support for idempotent, whole-sequence status ordering. - Update agent tool schema text to clarify that
sourcereflects ingestion origin (not chat channel).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| web/src/stores/taskStatusStore.ts | Adds optimistic reorder(names) action to persist column order and reload on failure. |
| web/src/components/Tasks/Board/TaskBoard.tsx | Wraps columns in a horizontal SortableContext, branches drag behavior for columns vs cards, and adds a column drag overlay. |
| web/src/components/Tasks/Board/dropIntent.ts | Adds column ID namespacing, drop-target → status resolution, and status list reordering helper. |
| web/src/components/Tasks/Board/dropIntent.test.tsx | Adds unit tests for column drop-target resolution and status reordering behavior. |
| web/src/components/Tasks/Board/BoardColumn.tsx | Makes columns sortable via a header-only drag handle while preserving lane-body droppable behavior for cards. |
| web/src/api/client.ts | Adds reorderTaskStatuses client call to /task-statuses/reorder. |
| tests/test_task_statuses.py | Adds DB-level tests for reorder semantics (idempotence, omissions, contiguity). |
| tests/test_task_board_api.py | Adds HTTP-level tests ensuring reorder affects board lane order and route ordering avoids capture. |
| nerve/gateway/routes/tasks.py | Adds authenticated reorder route and request model. |
| nerve/db/task_statuses.py | Implements reorder_task_statuses to rewrite sort_order from a desired name sequence. |
| nerve/agent/tools/schemas.py | Clarifies meaning of source in the tool schema description. |
Suppressed comments (1)
web/src/components/Tasks/Board/TaskBoard.tsx:162
- The drag-and-drop accessibility announcements are hard-coded for tasks, so when a column is dragged they announce "Picked up task col:" and include raw
col:/card IDs. Since columns and cards share oneDndContext, the announcements should branch onactive.data.current?.typeand (for columns) resolve the target status viastatusFromDropTargetso screen reader output matches what’s being dragged.
accessibility={{
announcements: {
onDragStart: ({ active }) => `Picked up task ${active.id}.`,
onDragOver: ({ over }) =>
over ? `Task is over ${String(over.id).replace('lane:', 'the ')} lane.` : '',
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
026f04b to
bac0df9
Compare
|
Addressed the suppressed review comment (the one folded into the review body about drag announcements) in It was right, and it was the same defect Copilot raised on #275 — the announcements block was byte-identical at both commits, so #279 inherited it and then made it worse by putting a non-task into Fixed across the two PRs rather than twice in one place:
It reads from Three new cases in
|
Column order was fixed at whatever sort_order the statuses happened to
have, with no way to change it from the UI — and lane order is the part of
a board people arrange to match how they actually work.
Columns are now sortable in the same DndContext as the cards, branching on
what was picked up rather than on what it landed on. Two details make
that work:
Drag listeners live on the header only, not the whole column. The column
body has to stay a drop target for cards, and listeners on the wrapper
would make every card drag also pick up its column.
Column ids are namespaced ("col:<status>") so they can't be confused with
a task id inside the shared context, and a drop resolves through any kind
of target — another column, a lane body, or a card. Collision detection
returns the nearest droppable of any kind, so a column drag routinely
finishes with the cursor over a card; treating that as "no target" would
make the gesture feel broken.
The API takes the whole desired sequence rather than one status's new
index. A drag shifts several statuses at once, so a full-sequence write is
idempotent and avoids the half-applied states a per-row PATCH storm would
leave behind if one failed. Statuses omitted from the request keep their
relative order after the named ones, so a status can't be dropped from the
board by an incomplete call.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cards and columns share one DndContext, so `active` is no longer always a task — but the announcements still assumed it was, and a column drag came out as "Picked up task col:pending." Both ends now come from the payload, which columns already carry. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
bac0df9 to
7b810c4
Compare
From live testing feedback. Column order was fixed at whatever
sort_orderthe statuses happened to have, with no way to change it from the UI — and lane order is the part of a board people arrange to match how they actually work.Two details make the drag work
Columns are sortable in the same
DndContextas the cards, branching on what was picked up rather than on what it landed on.col:<status>) so they can't be confused with a task id inside the shared context — and a drop resolves through any kind of target: another column, a lane body, or a card. Collision detection returns the nearest droppable of any kind, so a column drag routinely finishes with the cursor over a card. Treating that as "no target" would make the gesture feel broken for no reason.The API takes a whole sequence, not an index
POST /api/task-statuses/reorder {names: [...]}.A drag shifts several statuses at once, so a full-sequence write is idempotent and avoids the half-applied states a per-row
PATCHstorm would leave behind if one of them failed. Statuses omitted from the request keep their relative order after the named ones, so an incomplete call can't drop a column off the board.Applied optimistically in the store so the dragged column doesn't snap back mid-request; the server's echo is authoritative and a failure reloads rather than guessing.
Testing
pytest tests/ -q— 3113 passednpm test— 84 passed (11 for the column geometry, 3 for the column announcements)tsc -b+npm run buildclean ·eslint146The reorder route test includes a declaration-order guard —
/reorderhas to stay above/task-statuses/{name}or it gets captured as a status name.Not verified: the drag gesture itself in a browser. Geometry and the reorder reducer are tested as pure functions, but dnd-kit's sensor behaviour with two nested
SortableContexts (horizontal columns, vertical cards) isn't. Needs anerve restarton the instance to deploy the endpoint before I can check it.🤖 Generated with Claude Code