Skip to content

[7/7] Reorder board columns by dragging - #279

Open
alex-clickhouse wants to merge 2 commits into
alex-clickhouse/task-eventsfrom
alex-clickhouse/task-board-columns
Open

[7/7] Reorder board columns by dragging#279
alex-clickhouse wants to merge 2 commits into
alex-clickhouse/task-eventsfrom
alex-clickhouse/task-board-columns

Conversation

@alex-clickhouse

@alex-clickhouse alex-clickhouse commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #277#276#275#274#273#272main.

From live testing feedback. 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.

Two details make the drag work

Columns are sortable in the same DndContext as the cards, branching on what was picked up rather than on what it landed on.

  1. Drag listeners live on the header only, not the whole column. The body has to stay a drop target for cards; listeners on the wrapper would make every card drag also pick up its column.
  2. 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 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 PATCH storm 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/ -q3113 passed
  • npm test84 passed (11 for the column geometry, 3 for the column announcements)
  • tsc -b + npm run build clean · eslint 146

The reorder route test includes a declaration-order guard — /reorder has 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 a nerve restart on the instance to deploy the endpoint before I can check it.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/reorder and corresponding DB/store/client support for idempotent, whole-sequence status ordering.
  • Update agent tool schema text to clarify that source reflects 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 one DndContext, the announcements should branch on active.data.current?.type and (for columns) resolve the target status via statusFromDropTarget so 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.

Comment thread nerve/db/task_statuses.py
@alex-clickhouse
alex-clickhouse force-pushed the alex-clickhouse/task-board-columns branch from 026f04b to bac0df9 Compare August 5, 2026 12:07
@alex-clickhouse

Copy link
Copy Markdown
Collaborator Author

Addressed the suppressed review comment (the one folded into the review body about drag announcements) in bac0df9.

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 active.

Fixed across the two PRs rather than twice in one place:

  • [4/7] Add a Kanban board view for tasks #275 replaced the inline ternaries with a pure boardAnnouncements(labelFor) in announcements.ts, handling task and lane — the two types that exist at that commit. Lanes now resolve to the configured label, and the raw lane: prefix no longer leaks (onDragEnd had no replace at all, so it announced Task dropped on lane:pending.).
  • This PR adds the column branch, which is the right place for it since this is the PR that introduces column dragging. Without it a column drag announced Picked up task col:pending.

It reads from active.data.current.type as suggested; resolving through statusFromDropTarget wasn't needed because the column droppable already carries { type: 'column', status } in its payload, same as lanes do.

Three new cases in announcements.test.ts cover column-picked-up, column-over-column, and column-dropped-on-a-card — that last one because collision detection returns the nearest droppable of any kind, so a column drag routinely finishes with the cursor over a card.

npm test 84 passed · pytest 3113 passed · tsc -b + build clean · eslint 146 (down from 148 on this branch before the review pass).

alex-clickhouse and others added 2 commits August 5, 2026 12:25
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>
@alex-clickhouse
alex-clickhouse marked this pull request as ready for review August 5, 2026 12:27
@alex-clickhouse
alex-clickhouse force-pushed the alex-clickhouse/task-board-columns branch from bac0df9 to 7b810c4 Compare August 5, 2026 12:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants