Kanban: columns cannot be reordered #69

Open
opened 2026-04-23 11:49:05 +00:00 by eslamnawara · 4 comments
Member

Summary

On a Kanban board, columns cannot be rearranged — dragging a column header
does not reorder it relative to the other columns. The column order remains
fixed at whatever was set at creation time.

Steps to reproduce

  1. Place a Kanban board on the whiteboard with at least 2 columns.
  2. Click and hold a column header.
  3. Drag the column past an adjacent column.
  4. Release.

Expected

The dragged column is reordered to the drop position and the new column
order is persisted (survives reload).

Actual

The column does not move.

## Summary On a Kanban board, columns cannot be rearranged — dragging a column header does not reorder it relative to the other columns. The column order remains fixed at whatever was set at creation time. ## Steps to reproduce 1. Place a Kanban board on the whiteboard with at least 2 columns. 2. Click and hold a column header. 3. Drag the column past an adjacent column. 4. Release. ## Expected The dragged column is reordered to the drop position and the new column order is persisted (survives reload). ## Actual The column does not move.
Member

Implementation Spec for Issue #69

Objective

Enable Kanban column reordering via drag-and-drop of the column title handle, with the new order persisted across reloads, undoable/redoable, and synced to collaborators — without regressing existing column/card interactions.

Requirements

  1. Dragging a column's title to a new position reorders that column relative to the others.
  2. New order persists (survives reload) because it flows through WhiteboardSync.onUpdate(group), which already serializes state.columns in array order.
  3. Drag produces an undo/redo entry via WhiteboardHistory.snapshotBefore / commitUpdate.
  4. Existing behaviors must not regress:
    • Double-click on the column title still opens the rename editor.
    • Column color indicator still displays.
    • Column delete × still works.
    • Card drag-and-drop unaffected.
    • Whole-kanban drag (grab background) still works.
    • Selection + Delete key, transformer live-redraw, font scaling — all unchanged.
  5. Edge cases:
    • Only one column: drag is a no-op (snap back).
    • Drop outside the kanban: clamp to nearest slot.
    • Drop on own slot: no-op (no history / sync traffic).

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js — the only file.

No backend, schema, or serialization changes: column order is implicit in state.columns array order, already round-tripped.

Implementation Plan

Step 1 — Capture column layout at render time

Files: kanban.js, function renderKanban

  • Before the columns.forEach(renderColumn ...) loop, stash per-column horizontal bounds on the group:
    group._kbColumnLayout = columns.map(function(_, i) {
        var x = padding + i * (colW + padding);
        return { x: x, w: colW, center: x + colW / 2 };
    });
    
  • Replaced fresh every render so it stays in sync with state.columns.

Dependencies: none.

Step 2 — Make colTitle the drag handle

Files: kanban.js, function renderColumn

  • Add draggable: true to the colTitle Konva.Text construction.
  • Keep the title node as the ONLY drag surface. Background, color indicator, ×, + Add card, cards remain independently interactive. Konva's dblclick threshold still triggers rename on no-drag clicks.

Dependencies: none.

Step 3 — colTitle.on('dragstart')

Files: kanban.js

  • Attach immediately after group.add(colTitle) (and before the existing dblclick handler):
    colTitle.on('dragstart', function(e) {
        e.cancelBubble = true;
        group._selectedColIdx = -1;
        group._selectedCardIdx = -1;
        group._selectedCardRect = null;
        WhiteboardHistory.snapshotBefore(group.id());
        colTitle.moveToTop();
        group._kbColDragSrc = colIdx;
    });
    
  • e.cancelBubble = true prevents the kanban group's existing dragstart from running simultaneously (otherwise both title and whole group would drag).
  • moveToTop() keeps the dragged title above neighboring column backgrounds (same trick cardGroup uses).

Dependencies: Step 2.

Step 4 — colTitle.on('dragend') with reorder math

Files: kanban.js

  • Attach just after dragstart:
    colTitle.on('dragend', function(e) {
        e.cancelBubble = true;
        var stage = WhiteboardCanvas.getStage();
        var pointer = stage.getPointerPosition();
        var srcIdx = group._kbColDragSrc;
        group._kbColDragSrc = -1;
    
        var cols = group._kanbanState.columns;
        if (!pointer || cols.length < 2) {
            renderKanban(group);
            WhiteboardHistory.commitUpdate(group.id());
            return;
        }
    
        var local = group.getAbsoluteTransform().copy().invert().point(pointer);
    
        var layout = group._kbColumnLayout || [];
        var targetIdx = -1;
        for (var i = 0; i < layout.length; i++) {
            if (local.x >= layout[i].x && local.x <= layout[i].x + layout[i].w) { targetIdx = i; break; }
        }
        if (targetIdx < 0) {
            if (local.x < (layout[0] ? layout[0].x : 0)) targetIdx = 0;
            else targetIdx = cols.length - 1;
        }
    
        if (targetIdx === srcIdx) {
            renderKanban(group);
            WhiteboardHistory.commitUpdate(group.id());
            return;
        }
    
        var moved = cols.splice(srcIdx, 1)[0];
        var insertIdx = (targetIdx > srcIdx) ? targetIdx - 1 : targetIdx;
        if (insertIdx < 0) insertIdx = 0;
        if (insertIdx > cols.length) insertIdx = cols.length;
        cols.splice(insertIdx, 0, moved);
    
        renderKanban(group);
        refreshProperties(group);
        WhiteboardHistory.commitUpdate(group.id());
        WhiteboardSync.onUpdate(group);
    });
    
  • WhiteboardSync.onUpdate fires only after a real mutation (not on snap-back / same-slot), avoiding needless traffic.
  • The same splice → renderKanban → commit → onUpdate pattern used by card reorder.

Dependencies: Steps 1, 2, 3.

Acceptance Criteria

  • Drag column 1's title past column 2 → columns swap on release.
  • Drag a column's title far right / far left → becomes rightmost / leftmost.
  • Reload → new order persists.
  • Ctrl/Cmd+Z undoes the reorder; redo re-applies.
  • Double-click a column title (no movement) → rename editor opens.
  • × delete, color indicator, + Add card, + Column all still work.
  • Card drag/reorder, card selection, Delete-key deletion, whole-kanban drag unchanged.
  • Only one column present → drag is a no-op (snap back, no error).
  • Drop outside kanban bounds → clamps to first/last slot.
  • Drop on own slot → no history entry, no sync traffic.
  • Transformer handles track the new bounding box after reorder (already handled by existing tr.forceUpdate() tail in renderKanban).
  • Collaborator in another session sees the new order within their sync tick.

Notes

  • Drag affordance scope: only colTitle moves with the cursor during drag. Column bg, color indicator, ×, + Add card, and cards stay put. Standard "grab the handle" affordance. A future enhancement could wrap the whole header band in a cardGroup-equivalent for richer feedback; out of scope here.
  • No schema changes: column order is implicit in state.columns array order.
  • No refactor: cardGroup.dragstart/dragend and group.dragstart/dragend are untouched. New handlers are additive.
  • Why not the whole header band?: a draggable Rect covering the strip would intercept events meant for the color indicator, ×, and future header UI. Narrow handle avoids coupling.
  • dblclick still works because Konva's dblclick fires when pointer didn't move beyond the drag threshold. Cards already combine draggable: true with dblclick dbltap successfully; same pattern here.
## Implementation Spec for Issue #69 ### Objective Enable Kanban column reordering via drag-and-drop of the column title handle, with the new order persisted across reloads, undoable/redoable, and synced to collaborators — without regressing existing column/card interactions. ### Requirements 1. Dragging a column's title to a new position reorders that column relative to the others. 2. New order persists (survives reload) because it flows through `WhiteboardSync.onUpdate(group)`, which already serializes `state.columns` in array order. 3. Drag produces an undo/redo entry via `WhiteboardHistory.snapshotBefore` / `commitUpdate`. 4. Existing behaviors must not regress: - Double-click on the column title still opens the rename editor. - Column color indicator still displays. - Column delete `×` still works. - Card drag-and-drop unaffected. - Whole-kanban drag (grab background) still works. - Selection + Delete key, transformer live-redraw, font scaling — all unchanged. 5. Edge cases: - Only one column: drag is a no-op (snap back). - Drop outside the kanban: clamp to nearest slot. - Drop on own slot: no-op (no history / sync traffic). ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` — the only file. No backend, schema, or serialization changes: column order is implicit in `state.columns` array order, already round-tripped. ### Implementation Plan #### Step 1 — Capture column layout at render time Files: `kanban.js`, function `renderKanban` - Before the `columns.forEach(renderColumn ...)` loop, stash per-column horizontal bounds on the group: ``` group._kbColumnLayout = columns.map(function(_, i) { var x = padding + i * (colW + padding); return { x: x, w: colW, center: x + colW / 2 }; }); ``` - Replaced fresh every render so it stays in sync with `state.columns`. Dependencies: none. #### Step 2 — Make `colTitle` the drag handle Files: `kanban.js`, function `renderColumn` - Add `draggable: true` to the `colTitle` Konva.Text construction. - Keep the title node as the ONLY drag surface. Background, color indicator, `×`, `+ Add card`, cards remain independently interactive. Konva's dblclick threshold still triggers rename on no-drag clicks. Dependencies: none. #### Step 3 — `colTitle.on('dragstart')` Files: `kanban.js` - Attach immediately after `group.add(colTitle)` (and before the existing dblclick handler): ``` colTitle.on('dragstart', function(e) { e.cancelBubble = true; group._selectedColIdx = -1; group._selectedCardIdx = -1; group._selectedCardRect = null; WhiteboardHistory.snapshotBefore(group.id()); colTitle.moveToTop(); group._kbColDragSrc = colIdx; }); ``` - `e.cancelBubble = true` prevents the kanban group's existing `dragstart` from running simultaneously (otherwise both title and whole group would drag). - `moveToTop()` keeps the dragged title above neighboring column backgrounds (same trick `cardGroup` uses). Dependencies: Step 2. #### Step 4 — `colTitle.on('dragend')` with reorder math Files: `kanban.js` - Attach just after `dragstart`: ``` colTitle.on('dragend', function(e) { e.cancelBubble = true; var stage = WhiteboardCanvas.getStage(); var pointer = stage.getPointerPosition(); var srcIdx = group._kbColDragSrc; group._kbColDragSrc = -1; var cols = group._kanbanState.columns; if (!pointer || cols.length < 2) { renderKanban(group); WhiteboardHistory.commitUpdate(group.id()); return; } var local = group.getAbsoluteTransform().copy().invert().point(pointer); var layout = group._kbColumnLayout || []; var targetIdx = -1; for (var i = 0; i < layout.length; i++) { if (local.x >= layout[i].x && local.x <= layout[i].x + layout[i].w) { targetIdx = i; break; } } if (targetIdx < 0) { if (local.x < (layout[0] ? layout[0].x : 0)) targetIdx = 0; else targetIdx = cols.length - 1; } if (targetIdx === srcIdx) { renderKanban(group); WhiteboardHistory.commitUpdate(group.id()); return; } var moved = cols.splice(srcIdx, 1)[0]; var insertIdx = (targetIdx > srcIdx) ? targetIdx - 1 : targetIdx; if (insertIdx < 0) insertIdx = 0; if (insertIdx > cols.length) insertIdx = cols.length; cols.splice(insertIdx, 0, moved); renderKanban(group); refreshProperties(group); WhiteboardHistory.commitUpdate(group.id()); WhiteboardSync.onUpdate(group); }); ``` - `WhiteboardSync.onUpdate` fires only after a real mutation (not on snap-back / same-slot), avoiding needless traffic. - The same `splice → renderKanban → commit → onUpdate` pattern used by card reorder. Dependencies: Steps 1, 2, 3. ### Acceptance Criteria - [ ] Drag column 1's title past column 2 → columns swap on release. - [ ] Drag a column's title far right / far left → becomes rightmost / leftmost. - [ ] Reload → new order persists. - [ ] Ctrl/Cmd+Z undoes the reorder; redo re-applies. - [ ] Double-click a column title (no movement) → rename editor opens. - [ ] `×` delete, color indicator, `+ Add card`, `+ Column` all still work. - [ ] Card drag/reorder, card selection, Delete-key deletion, whole-kanban drag unchanged. - [ ] Only one column present → drag is a no-op (snap back, no error). - [ ] Drop outside kanban bounds → clamps to first/last slot. - [ ] Drop on own slot → no history entry, no sync traffic. - [ ] Transformer handles track the new bounding box after reorder (already handled by existing `tr.forceUpdate()` tail in `renderKanban`). - [ ] Collaborator in another session sees the new order within their sync tick. ### Notes - **Drag affordance scope**: only `colTitle` moves with the cursor during drag. Column bg, color indicator, `×`, `+ Add card`, and cards stay put. Standard "grab the handle" affordance. A future enhancement could wrap the whole header band in a cardGroup-equivalent for richer feedback; out of scope here. - **No schema changes**: column order is implicit in `state.columns` array order. - **No refactor**: `cardGroup.dragstart/dragend` and `group.dragstart/dragend` are untouched. New handlers are additive. - **Why not the whole header band?**: a draggable Rect covering the strip would intercept events meant for the color indicator, `×`, and future header UI. Narrow handle avoids coupling. - **dblclick still works** because Konva's dblclick fires when pointer didn't move beyond the drag threshold. Cards already combine `draggable: true` with `dblclick dbltap` successfully; same pattern here.
Member

Test Results

JavaScript-only change in kanban.js; Rust workspace validated for regressions.

Check Result
cargo check --workspace pass
cargo clippy --workspace -- -D warnings pass
cargo fmt --check pass

Manual verification required: drag a column title past an adjacent column, release, confirm the columns reorder and that the new order persists across reload. Verify dblclick-to-rename, color indicator, × delete, and card drag-and-drop still work.

## Test Results JavaScript-only change in `kanban.js`; Rust workspace validated for regressions. | Check | Result | |---|---| | `cargo check --workspace` | pass | | `cargo clippy --workspace -- -D warnings` | pass | | `cargo fmt --check` | pass | Manual verification required: drag a column title past an adjacent column, release, confirm the columns reorder and that the new order persists across reload. Verify dblclick-to-rename, color indicator, `×` delete, and card drag-and-drop still work.
Member

Implementation Summary

Kanban columns can now be reordered by dragging the column title. The new order persists across reload and supports undo/redo.

Files changed

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js (+66)

Changes

  • renderKanban now populates group._kbColumnLayout — a per-render array of {x, w, center} per column, consumed by the dragend slot math.
  • colTitle is marked draggable: true in renderColumn.
  • New colTitle.on('dragstart'): cancels bubble (so the whole-kanban drag does not also fire), clears card selection, snapshots history, calls moveToTop(), and records the source column index on the group.
  • New colTitle.on('dragend'): converts pointer to group-local coords, finds the target slot via the stored layout (with clamping for out-of-bounds drops), splices the column to its new position, re-renders, commits history, and calls WhiteboardSync.onUpdate. Snap-back / same-slot / single-column drops are no-ops and do not send sync traffic.

Preserved

  • Double-click the title still renames the column (Konva's dblclick fires when pointer does not move beyond the drag threshold).
  • Color indicator, × delete button, + Add card, + Column.
  • Card drag-and-drop between/within columns, card selection + Delete key, transformer live-redraw, font scaling.
  • Whole-kanban drag (grab background).

Scope notes

  • No schema / sync changes: column order is implicit in the state.columns array order and already round-trips.
  • During drag only the colTitle Konva.Text moves with the cursor; the column background, color indicator, ×, + Add card, and cards stay in place. Standard "grab the handle" affordance — a future enhancement could wrap the whole header band into a drag sub-group.

Test results

  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo fmt --check: pass

Manual QA required: reorder via drag, reload to verify persistence, Ctrl/Cmd+Z to verify undo, and regression-check the preserved interactions.

## Implementation Summary Kanban columns can now be reordered by dragging the column title. The new order persists across reload and supports undo/redo. ### Files changed - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` (+66) ### Changes - `renderKanban` now populates `group._kbColumnLayout` — a per-render array of `{x, w, center}` per column, consumed by the dragend slot math. - `colTitle` is marked `draggable: true` in `renderColumn`. - New `colTitle.on('dragstart')`: cancels bubble (so the whole-kanban drag does not also fire), clears card selection, snapshots history, calls `moveToTop()`, and records the source column index on the group. - New `colTitle.on('dragend')`: converts pointer to group-local coords, finds the target slot via the stored layout (with clamping for out-of-bounds drops), splices the column to its new position, re-renders, commits history, and calls `WhiteboardSync.onUpdate`. Snap-back / same-slot / single-column drops are no-ops and do not send sync traffic. ### Preserved - Double-click the title still renames the column (Konva's dblclick fires when pointer does not move beyond the drag threshold). - Color indicator, `×` delete button, `+ Add card`, `+ Column`. - Card drag-and-drop between/within columns, card selection + Delete key, transformer live-redraw, font scaling. - Whole-kanban drag (grab background). ### Scope notes - No schema / sync changes: column order is implicit in the `state.columns` array order and already round-trips. - During drag only the `colTitle` Konva.Text moves with the cursor; the column background, color indicator, `×`, `+ Add card`, and cards stay in place. Standard "grab the handle" affordance — a future enhancement could wrap the whole header band into a drag sub-group. ### Test results - `cargo check --workspace`: pass - `cargo clippy --workspace -- -D warnings`: pass - `cargo fmt --check`: pass Manual QA required: reorder via drag, reload to verify persistence, Ctrl/Cmd+Z to verify undo, and regression-check the preserved interactions.
Member

Pull request opened: #71

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/71 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#69
No description provided.