Kanban: cards cannot be moved between columns via drag-and-drop #45

Open
opened 2026-04-22 10:34:37 +00:00 by AhmedHanafy725 · 4 comments
Member

Problem

Kanban cards cannot be moved between columns (or reordered within a column) by dragging. The only way to change a card's column today is via the 3-dot menu -> "Move to X", which is slow and hidden.

Expected behavior (Miro/Trello/Jira-style): grab a card with the mouse and drop it into another column, or reorder it within the same column.

Evidence

In crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js:

  • The kanban group is draggable (line 25) — the whole board moves on the canvas.
  • Individual cards are not draggable. renderCard() (line 218) creates plain Konva.Rect + Konva.Text with no draggable: true.
  • No drop-target detection logic exists for columns.
  • Column change only happens via showCardMenu() at line 315.

Proposed Fix

  • Make each card draggable on its own, with cancelBubble so parent kanban group drag is not triggered.
  • On dragstart, lift the card to the top so it renders above other columns.
  • On dragmove, optionally highlight the target column under the pointer.
  • On dragend:
    • Convert the card's absolute pointer position to local kanban coordinates.
    • Determine the target column by X range.
    • Determine the insertion index by comparing Y against existing cards in that column.
    • Splice the card out of the source column, insert into the target at the computed index.
    • Re-render, refresh properties panel if visible, sync to server.
  • Must not regress: (a) dragging the whole kanban group on the canvas, (b) double-click card to edit, (c) 3-dot menu still works, (d) add/delete/move-up/move-down all still work.

Acceptance

  • Drag a card from column A and drop it on column B -> card moves to column B.
  • Drag a card within its column to reorder -> card moves to the dropped position.
  • Dropping a card outside any column snaps it back to its original position.
  • The whole kanban board can still be dragged on the canvas.
  • Changes persist after reload.
  • Other card operations (edit, delete, duplicate, menu move) still work.
## Problem Kanban cards cannot be moved between columns (or reordered within a column) by dragging. The only way to change a card's column today is via the 3-dot menu -> "Move to X", which is slow and hidden. Expected behavior (Miro/Trello/Jira-style): grab a card with the mouse and drop it into another column, or reorder it within the same column. ## Evidence In `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js`: - The kanban **group** is draggable (line 25) — the whole board moves on the canvas. - Individual cards are **not** draggable. `renderCard()` (line 218) creates plain `Konva.Rect` + `Konva.Text` with no `draggable: true`. - No drop-target detection logic exists for columns. - Column change only happens via `showCardMenu()` at line 315. ## Proposed Fix - Make each card draggable on its own, with `cancelBubble` so parent kanban group drag is not triggered. - On `dragstart`, lift the card to the top so it renders above other columns. - On `dragmove`, optionally highlight the target column under the pointer. - On `dragend`: - Convert the card's absolute pointer position to local kanban coordinates. - Determine the target column by X range. - Determine the insertion index by comparing Y against existing cards in that column. - Splice the card out of the source column, insert into the target at the computed index. - Re-render, refresh properties panel if visible, sync to server. - Must not regress: (a) dragging the whole kanban group on the canvas, (b) double-click card to edit, (c) 3-dot menu still works, (d) add/delete/move-up/move-down all still work. ## Acceptance - [ ] Drag a card from column A and drop it on column B -> card moves to column B. - [ ] Drag a card within its column to reorder -> card moves to the dropped position. - [ ] Dropping a card outside any column snaps it back to its original position. - [ ] The whole kanban board can still be dragged on the canvas. - [ ] Changes persist after reload. - [ ] Other card operations (edit, delete, duplicate, menu move) still work.
Author
Member

Implementation Spec for Issue #45

Objective

Enable Miro/Trello/Jira-style drag-and-drop of kanban cards across and within columns while preserving existing board-drag, inline edit, and 3-dot-menu "Move to X" behavior.

Requirements

  • Cards can be dragged with the mouse from one column to another.
  • Cards can be reordered within the same column by dragging.
  • Dropping outside any column snaps the card back (no state mutation).
  • Parent kanban group remains draggable when grabbing the background (no regression).
  • Double-click inline edit on a card still works.
  • 3-dot menu (edit/move/duplicate/delete) still works.
  • Column add/delete, card add/delete/duplicate, title rename all still work.
  • Drag result persists through sync reload and is undoable via history.
  • No new dependencies; pure Konva + existing WhiteboardHistory / WhiteboardSync / refreshProperties.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js — make cardRect individually draggable inside renderCard(), add dragstart / dragend handlers that compute target (colIdx, insertIdx) from the pointer in the group's local coordinate space, splice the card within group._kanbanState.columns, then re-render / commit history / sync.

Implementation Plan

Step 1: Make kanban cards draggable between columns and within a column

Files: crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js

Subtasks:

  • In renderCard(), set draggable: true on the cardRect Konva.Rect only. cardText, menuHit, menuBtn remain non-draggable siblings; the parent group stays draggable for whole-board movement.
  • cardRect.on('dragstart'): e.cancelBubble = true, WhiteboardHistory.snapshotBefore(group.id()), cardRect.moveToTop(), record source (colIdx, cardIdx).
  • cardRect.on('dragend'):
    • Get pointer via stage.getPointerPosition(), convert to group-local via group.getAbsoluteTransform().copy().invert().point(pointer).
    • Determine target column by X using same formula as renderColumn: padding + colIdx*(colW+padding) .. + colW, require local.y >= headerH.
    • Determine insertion index by Y within target column using slot midlines: cy + 28 + i*(cardH+cardGap) + cardH/2.
    • If no valid target column → renderKanban(group) to snap back; no history/sync.
    • Otherwise splice out of source, adjust index when same-column downward move, splice into target; then renderKanban, refreshProperties, WhiteboardHistory.commitUpdate, WhiteboardSync.onUpdate.
  • Keep dblclick inline edit intact; keep menuHit handlers intact; keep hover effects.

Acceptance Criteria

  • Drag from column A to B → card moves; re-renders immediately.
  • Drop between two cards → card inserts at that index.
  • Drag within same column → reorders correctly (no off-by-one).
  • Drop outside any column → snaps back; no mutation.
  • Whole kanban board still draggable via background.
  • Double-click still edits card text.
  • 3-dot menu still works.
  • + Add card, + Column, rename, delete all still work.
  • Changes persist via sync after reload.
  • Undo reverts the drag.

Notes

  • Geometry constants (lines 12–16): padding=8, headerH=36, cardGap=6, DEFAULT_COL_W=200, DEFAULT_CARD_H=44. Read state.colWidth / state.cardHeight live.
  • group._kanbanState is the single source of truth; every mutation → renderKanban + sync + history.
  • No new dependencies. Change is entirely local to renderCard().
## Implementation Spec for Issue #45 ### Objective Enable Miro/Trello/Jira-style drag-and-drop of kanban cards across and within columns while preserving existing board-drag, inline edit, and 3-dot-menu "Move to X" behavior. ### Requirements - Cards can be dragged with the mouse from one column to another. - Cards can be reordered within the same column by dragging. - Dropping outside any column snaps the card back (no state mutation). - Parent kanban group remains draggable when grabbing the background (no regression). - Double-click inline edit on a card still works. - 3-dot menu (edit/move/duplicate/delete) still works. - Column add/delete, card add/delete/duplicate, title rename all still work. - Drag result persists through sync reload and is undoable via history. - No new dependencies; pure Konva + existing `WhiteboardHistory` / `WhiteboardSync` / `refreshProperties`. ### Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` — make `cardRect` individually draggable inside `renderCard()`, add `dragstart` / `dragend` handlers that compute target `(colIdx, insertIdx)` from the pointer in the group's local coordinate space, splice the card within `group._kanbanState.columns`, then re-render / commit history / sync. ### Implementation Plan #### Step 1: Make kanban cards draggable between columns and within a column Files: `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` Subtasks: - In `renderCard()`, set `draggable: true` on the `cardRect` Konva.Rect only. `cardText`, `menuHit`, `menuBtn` remain non-draggable siblings; the parent `group` stays draggable for whole-board movement. - `cardRect.on('dragstart')`: `e.cancelBubble = true`, `WhiteboardHistory.snapshotBefore(group.id())`, `cardRect.moveToTop()`, record source `(colIdx, cardIdx)`. - `cardRect.on('dragend')`: - Get pointer via `stage.getPointerPosition()`, convert to group-local via `group.getAbsoluteTransform().copy().invert().point(pointer)`. - Determine target column by X using same formula as `renderColumn`: `padding + colIdx*(colW+padding)` .. `+ colW`, require `local.y >= headerH`. - Determine insertion index by Y within target column using slot midlines: `cy + 28 + i*(cardH+cardGap) + cardH/2`. - If no valid target column → `renderKanban(group)` to snap back; no history/sync. - Otherwise splice out of source, adjust index when same-column downward move, splice into target; then `renderKanban`, `refreshProperties`, `WhiteboardHistory.commitUpdate`, `WhiteboardSync.onUpdate`. - Keep `dblclick` inline edit intact; keep menuHit handlers intact; keep hover effects. ### Acceptance Criteria - [ ] Drag from column A to B → card moves; re-renders immediately. - [ ] Drop between two cards → card inserts at that index. - [ ] Drag within same column → reorders correctly (no off-by-one). - [ ] Drop outside any column → snaps back; no mutation. - [ ] Whole kanban board still draggable via background. - [ ] Double-click still edits card text. - [ ] 3-dot menu still works. - [ ] + Add card, + Column, rename, delete all still work. - [ ] Changes persist via sync after reload. - [ ] Undo reverts the drag. ### Notes - Geometry constants (lines 12–16): `padding=8`, `headerH=36`, `cardGap=6`, `DEFAULT_COL_W=200`, `DEFAULT_CARD_H=44`. Read `state.colWidth` / `state.cardHeight` live. - `group._kanbanState` is the single source of truth; every mutation → renderKanban + sync + history. - No new dependencies. Change is entirely local to `renderCard()`.
Author
Member

Test Results

  • cargo check --workspace: PASS
  • cargo clippy --workspace -- -D warnings: PASS (0 warnings)
  • cargo fmt --all -- --check: PASS
  • cargo test --workspace --lib: PASS (0 passed; 0 failed — no crate defines library unit tests)

No regressions introduced. Change is JavaScript-only (crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js), so the Rust CI gates act purely as a no-regression guard here. Interactive behavior needs manual smoke testing in the browser.

## Test Results - `cargo check --workspace`: PASS - `cargo clippy --workspace -- -D warnings`: PASS (0 warnings) - `cargo fmt --all -- --check`: PASS - `cargo test --workspace --lib`: PASS (0 passed; 0 failed — no crate defines library unit tests) No regressions introduced. Change is JavaScript-only (`crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js`), so the Rust CI gates act purely as a no-regression guard here. Interactive behavior needs manual smoke testing in the browser.
Author
Member

Implementation Summary

Changes

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js — in renderCard():
    • Set draggable: true on the card's Konva.Rect (card background only — text, menu-hit overlay, and menu dots stay non-draggable; the parent kanban group remains draggable for whole-board movement).
    • Added dragstart handler: cancels event bubbling, takes a history snapshot via WhiteboardHistory.snapshotBefore, and calls moveToTop() so the grabbed card floats over its neighbors during the gesture.
    • Added dragend handler:
      • Converts the stage pointer position into group-local coordinates.
      • Determines the target column by X range (same formula as renderColumn) and requires the pointer to be below the header band.
      • Determines the insertion index by comparing Y against each existing card slot's midline.
      • If no valid target, snaps back via renderKanban without mutating state; clears the pending snapshot with a no-op commitUpdate.
      • Otherwise splices the card out of the source column, adjusts the insertion index for the same-column downward-move off-by-one, splices into the target column, then re-renders, refreshes the properties panel, commits the history update, and syncs via WhiteboardSync.onUpdate.

What still works

  • Whole-kanban drag on the canvas (grab the dark background).
  • Double-click a card to edit its text inline.
  • 3-dot card menu: Edit / Move to X / Move Up / Move Down / Duplicate / Delete.
    • Add card, + Column, rename column, delete column (x).
  • Column color picker and sliders in the properties panel.
  • Undo/redo — the drag is captured via snapshotBefore/commitUpdate which use serializeForServer, so the full column state is restored.
  • Sync and reload — _kanbanState.columns is what the server stores, so reordering persists.

Test Results

  • cargo check --workspace: PASS
  • cargo clippy --workspace -- -D warnings: PASS
  • cargo fmt --all -- --check: PASS
  • cargo test --workspace --lib: PASS

Notes

  • Pure Konva + existing whiteboard modules. No new dependencies, no Rust-side changes.
  • Konva auto-suppresses parent group drag when a draggable child begins dragging, so card drag and board drag don't conflict. The defensive e.cancelBubble = true on dragstart is a belt-and-braces.
## Implementation Summary ### Changes - `crates/hero_whiteboard_ui/static/web/js/whiteboard/kanban.js` — in `renderCard()`: - Set `draggable: true` on the card's Konva.Rect (card background only — text, menu-hit overlay, and menu dots stay non-draggable; the parent kanban group remains draggable for whole-board movement). - Added `dragstart` handler: cancels event bubbling, takes a history snapshot via `WhiteboardHistory.snapshotBefore`, and calls `moveToTop()` so the grabbed card floats over its neighbors during the gesture. - Added `dragend` handler: - Converts the stage pointer position into group-local coordinates. - Determines the target column by X range (same formula as `renderColumn`) and requires the pointer to be below the header band. - Determines the insertion index by comparing Y against each existing card slot's midline. - If no valid target, snaps back via `renderKanban` without mutating state; clears the pending snapshot with a no-op `commitUpdate`. - Otherwise splices the card out of the source column, adjusts the insertion index for the same-column downward-move off-by-one, splices into the target column, then re-renders, refreshes the properties panel, commits the history update, and syncs via `WhiteboardSync.onUpdate`. ### What still works - Whole-kanban drag on the canvas (grab the dark background). - Double-click a card to edit its text inline. - 3-dot card menu: Edit / Move to X / Move Up / Move Down / Duplicate / Delete. - + Add card, + Column, rename column, delete column (x). - Column color picker and sliders in the properties panel. - Undo/redo — the drag is captured via `snapshotBefore`/`commitUpdate` which use `serializeForServer`, so the full column state is restored. - Sync and reload — `_kanbanState.columns` is what the server stores, so reordering persists. ### Test Results - `cargo check --workspace`: PASS - `cargo clippy --workspace -- -D warnings`: PASS - `cargo fmt --all -- --check`: PASS - `cargo test --workspace --lib`: PASS ### Notes - Pure Konva + existing whiteboard modules. No new dependencies, no Rust-side changes. - Konva auto-suppresses parent group drag when a draggable child begins dragging, so card drag and board drag don't conflict. The defensive `e.cancelBubble = true` on `dragstart` is a belt-and-braces.
Author
Member

Pull request opened: #46

This PR implements the changes discussed in this issue.

Pull request opened: https://forge.ourworld.tf/lhumina_code/hero_whiteboard/pulls/46 This PR implements the changes discussed in this issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
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#45
No description provided.