Whiteboard: kanban column / card mutations are not undoable #182

Open
opened 2026-05-13 09:24:03 +00:00 by AhmedHanafy725 · 2 comments
Member

Problem

Nearly every kanban edit on the whiteboard issues WhiteboardSync.onUpdate(group) to persist the change but never brackets the mutation with WhiteboardHistory.snapshotBefore(id) + commitUpdate(id). As a result Ctrl+Z does not roll back any of:

  • Add column
  • Add card (per column add button)
  • Edit column title
  • Edit card title / body / icon
  • Delete card (per-card delete button)
  • Delete column
  • Move card up / down in same column
  • Move card to another column (context menu)
  • Duplicate card
  • deleteSelectedCard (kanban.js:862-880)
  • addCardToFirstColumn (kanban.js:883-890)

The only kanban changes that do undo today are:

  • Column drag-reorder (because that flow already uses snapshotBefore / commitUpdate at kanban.js:274-287, 326, 338)
  • Whole-group move (because object-layer drag bracketing in objects.js handles it)

Affected call sites

Mutation sites in kanban.js calling onUpdate(group) without history brackets:
kanban.js:76-78, 199, 232, 339, 381, 400, 431, 564, 604, 689, 708, 721, 729, 740, 748, 879, 888.

Reference: the column drag-reorder path that already does it correctly: kanban.js:274-287.

Expected

Every user-visible kanban mutation is one undo step. Pressing Ctrl+Z right after deleting a card restores that card with its text, icon, position in the column, and column membership. Same for column add/delete/edit.

Acceptance

  • Add column → Ctrl+Z removes the column; Ctrl+Y adds it back.
  • Add card (per-column +) → Ctrl+Z removes the card.
  • Edit column title → Ctrl+Z restores the previous title.
  • Edit card text → Ctrl+Z restores the previous text.
  • Delete card → Ctrl+Z restores the card with its content + position.
  • Delete column → Ctrl+Z restores the column with all its cards.
  • Move card up / down → Ctrl+Z restores the previous order.
  • Move card to another column → Ctrl+Z restores the original column membership.
  • Duplicate card → Ctrl+Z removes the duplicate.
  • deleteSelectedCard and addCardToFirstColumn keyboard / shortcut paths participate in undo.
  • No regression to column drag-reorder undo (already working).
  • No regression to whole-group move undo (handled by object-layer dragging).
  • Multi-user broadcast (onUpdate) still fires exactly once per mutation (no double emit).

Out of scope

Mindmap / Calendar / Webframe / Ungroup / Selection-toolbar property edits have the same class of issue — separate items in the audit; file as follow-ups.

## Problem Nearly every kanban edit on the whiteboard issues `WhiteboardSync.onUpdate(group)` to persist the change but never brackets the mutation with `WhiteboardHistory.snapshotBefore(id)` + `commitUpdate(id)`. As a result Ctrl+Z does not roll back any of: - Add column - Add card (per column add button) - Edit column title - Edit card title / body / icon - Delete card (per-card delete button) - Delete column - Move card up / down in same column - Move card to another column (context menu) - Duplicate card - `deleteSelectedCard` (`kanban.js:862-880`) - `addCardToFirstColumn` (`kanban.js:883-890`) The only kanban changes that *do* undo today are: - Column drag-reorder (because that flow already uses `snapshotBefore` / `commitUpdate` at `kanban.js:274-287, 326, 338`) - Whole-group move (because object-layer drag bracketing in `objects.js` handles it) ## Affected call sites Mutation sites in `kanban.js` calling `onUpdate(group)` without history brackets: `kanban.js:76-78, 199, 232, 339, 381, 400, 431, 564, 604, 689, 708, 721, 729, 740, 748, 879, 888`. Reference: the column drag-reorder path that already does it correctly: `kanban.js:274-287`. ## Expected Every user-visible kanban mutation is one undo step. Pressing Ctrl+Z right after deleting a card restores that card with its text, icon, position in the column, and column membership. Same for column add/delete/edit. ## Acceptance - [ ] Add column → Ctrl+Z removes the column; Ctrl+Y adds it back. - [ ] Add card (per-column +) → Ctrl+Z removes the card. - [ ] Edit column title → Ctrl+Z restores the previous title. - [ ] Edit card text → Ctrl+Z restores the previous text. - [ ] Delete card → Ctrl+Z restores the card with its content + position. - [ ] Delete column → Ctrl+Z restores the column with all its cards. - [ ] Move card up / down → Ctrl+Z restores the previous order. - [ ] Move card to another column → Ctrl+Z restores the original column membership. - [ ] Duplicate card → Ctrl+Z removes the duplicate. - [ ] `deleteSelectedCard` and `addCardToFirstColumn` keyboard / shortcut paths participate in undo. - [ ] No regression to column drag-reorder undo (already working). - [ ] No regression to whole-group move undo (handled by object-layer dragging). - [ ] Multi-user broadcast (`onUpdate`) still fires exactly once per mutation (no double emit). ## Out of scope Mindmap / Calendar / Webframe / Ungroup / Selection-toolbar property edits have the same class of issue — separate items in the audit; file as follow-ups.
Author
Member

Implementation Spec for Issue #182

Objective

Every kanban mutation becomes a single undo step. Wrap each existing WhiteboardSync.onUpdate(group) call site in kanban.js with WhiteboardHistory.snapshotBefore(id) + commitUpdate(id), following the pattern already used by the column-drag-reorder path at kanban.js:274-287.

Approach

Introduce a small file-local helper instead of repeating four lines at every callsite:

function persistKanbanMutation(group) {
    WhiteboardSync.onUpdate(group);
    WhiteboardHistory.commitUpdate(group.id());
}

At every mutation callback:

  1. Add WhiteboardHistory.snapshotBefore(group.id()); at the top of the callback (before any state change).
  2. Replace WhiteboardSync.onUpdate(group); with persistKanbanMutation(group);.

The snapshot captures the full server-serialized state via WhiteboardSync.serializeForServer(group) (history.js:17-21), which includes the kanban columns/cards data stored on group._kanbanState. commitUpdate no-ops if before/after are identical, so callbacks that bail out early (e.g. user dismissing an inline editor unchanged) push nothing onto the stack.

The existing drag-reorder flow at kanban.js:274-287, 326, 338 already does the right thing — leave it alone; just make sure the helper does the same onUpdate + commitUpdate order so its behavior matches.

Files to Modify

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/kanban.js

Implementation Plan

Step 1: Add the helper

Insert near the top of the IIFE in kanban.js, immediately after the imports / config constants:

function persistKanbanMutation(group) {
    WhiteboardSync.onUpdate(group);
    WhiteboardHistory.commitUpdate(group.id());
}

Step 2: Bracket each onUpdate callsite

For each of these line numbers (current state — may shift after edits), add WhiteboardHistory.snapshotBefore(group.id()); at the top of the enclosing callback and replace the WhiteboardSync.onUpdate(group); call with persistKanbanMutation(group);:

  • :76-78 — group drag-end has both onUpdate calls (object move + recomputeParentFrame). The first onUpdate is already correctly paired with the existing snapshotBefore at line 72 and commitUpdate at line 75. The second onUpdate(group) from recomputeParentFrame should remain as a plain onUpdate (the state diff is the same; no separate history entry needed).
  • :199 — edit column title inline editor onDone.
  • :232 — add column button click.
  • :339 — edit column color or similar in renderColumn.
  • :381, 400, 431 — within renderColumn: column-title edit completion, delete column, etc. Wrap each.
  • :564 — within renderCard: card-text edit completion.
  • :604 — card delete button.
  • :689, 708, 721, 729, 740, 748 — context-menu actions (move up / move down / move to column / duplicate / delete).
  • :879deleteSelectedCard.
  • :888addCardToFirstColumn.

For each: place the snapshotBefore at the earliest point inside the callback that runs unconditionally (so a return in an early-exit branch doesn't leave a stale pending snapshot — commitUpdate ignores the stale entry anyway, but cleaner this way).

Already-correct sites to leave alone:

  • :274-287 — column drag-reorder start. Keep its existing snapshotBefore/commitUpdate. Replace the WhiteboardSync.onUpdate(group) if any inside this callback with persistKanbanMutation(group) only if the commitUpdate already runs after — looking at the code, commitUpdate is called inside dragend; no change needed.
  • :326, 338 — these are commitUpdate calls in the dragend branches; leave alone.

Acceptance Criteria

  • Add column → Ctrl+Z removes it.
  • Add card (per-column +) → Ctrl+Z removes it.
  • Edit column title → Ctrl+Z restores the previous title.
  • Edit card text → Ctrl+Z restores the previous text.
  • Delete card → Ctrl+Z restores the card with its content + position.
  • Delete column → Ctrl+Z restores the column with all its cards.
  • Move card up / down → Ctrl+Z restores the order.
  • Move card to another column → Ctrl+Z restores the original column membership.
  • Duplicate card → Ctrl+Z removes the duplicate.
  • deleteSelectedCard and addCardToFirstColumn shortcuts participate in undo.
  • Column drag-reorder still undoes (no regression).
  • Whole-group move still undoes (no regression — object-layer drag pairs).
  • onUpdate(group) fires exactly once per mutation (no double emit).
  • Inline editor cancel (no change) does not push a no-op onto the history stack (commitUpdate no-ops on identical before/after).

Notes

  • snapshot serializes via WhiteboardSync.serializeForServer(group), which for kanban widgets includes _kanbanState.columns (the whole shape). Undo restores the entire kanban widget data as a single update, which is exactly what we want.
  • This does not introduce new RPCs — onUpdate(group) is the existing persistence path, used unchanged.
  • Multi-user broadcast continues to fire from onUpdate. Remote clients see one object.updated per kanban mutation, same as today.
  • Out of scope: mindmap / calendar / webframe / ungroup / selection-toolbar property edits. Same class of issue; separate follow-ups.
## Implementation Spec for Issue #182 ### Objective Every kanban mutation becomes a single undo step. Wrap each existing `WhiteboardSync.onUpdate(group)` call site in `kanban.js` with `WhiteboardHistory.snapshotBefore(id)` + `commitUpdate(id)`, following the pattern already used by the column-drag-reorder path at `kanban.js:274-287`. ### Approach Introduce a small file-local helper instead of repeating four lines at every callsite: ```js function persistKanbanMutation(group) { WhiteboardSync.onUpdate(group); WhiteboardHistory.commitUpdate(group.id()); } ``` At every mutation callback: 1. Add `WhiteboardHistory.snapshotBefore(group.id());` at the top of the callback (before any state change). 2. Replace `WhiteboardSync.onUpdate(group);` with `persistKanbanMutation(group);`. The snapshot captures the full server-serialized state via `WhiteboardSync.serializeForServer(group)` (history.js:17-21), which includes the kanban columns/cards data stored on `group._kanbanState`. `commitUpdate` no-ops if before/after are identical, so callbacks that bail out early (e.g. user dismissing an inline editor unchanged) push nothing onto the stack. The existing drag-reorder flow at `kanban.js:274-287, 326, 338` already does the right thing — leave it alone; just make sure the helper does the same `onUpdate` + `commitUpdate` order so its behavior matches. ### Files to Modify - `crates/hero_whiteboard_admin/static/web/js/whiteboard/kanban.js` ### Implementation Plan #### Step 1: Add the helper Insert near the top of the IIFE in `kanban.js`, immediately after the imports / config constants: ```js function persistKanbanMutation(group) { WhiteboardSync.onUpdate(group); WhiteboardHistory.commitUpdate(group.id()); } ``` #### Step 2: Bracket each onUpdate callsite For each of these line numbers (current state — may shift after edits), add `WhiteboardHistory.snapshotBefore(group.id());` at the top of the enclosing callback and replace the `WhiteboardSync.onUpdate(group);` call with `persistKanbanMutation(group);`: - `:76-78` — group drag-end has both `onUpdate` calls (object move + recomputeParentFrame). The first `onUpdate` is already correctly paired with the existing `snapshotBefore` at line 72 and `commitUpdate` at line 75. The second `onUpdate(group)` from `recomputeParentFrame` should remain as a plain `onUpdate` (the state diff is the same; no separate history entry needed). - `:199` — edit column title inline editor `onDone`. - `:232` — add column button click. - `:339` — edit column color or similar in `renderColumn`. - `:381, 400, 431` — within `renderColumn`: column-title edit completion, delete column, etc. Wrap each. - `:564` — within `renderCard`: card-text edit completion. - `:604` — card delete button. - `:689, 708, 721, 729, 740, 748` — context-menu actions (move up / move down / move to column / duplicate / delete). - `:879` — `deleteSelectedCard`. - `:888` — `addCardToFirstColumn`. For each: place the `snapshotBefore` at the **earliest** point inside the callback that runs unconditionally (so a `return` in an early-exit branch doesn't leave a stale pending snapshot — `commitUpdate` ignores the stale entry anyway, but cleaner this way). Already-correct sites to leave alone: - `:274-287` — column drag-reorder start. Keep its existing `snapshotBefore`/`commitUpdate`. Replace the `WhiteboardSync.onUpdate(group)` if any inside this callback with `persistKanbanMutation(group)` only if the `commitUpdate` already runs after — looking at the code, `commitUpdate` is called inside dragend; no change needed. - `:326, 338` — these are `commitUpdate` calls in the dragend branches; leave alone. ### Acceptance Criteria - [ ] Add column → Ctrl+Z removes it. - [ ] Add card (per-column +) → Ctrl+Z removes it. - [ ] Edit column title → Ctrl+Z restores the previous title. - [ ] Edit card text → Ctrl+Z restores the previous text. - [ ] Delete card → Ctrl+Z restores the card with its content + position. - [ ] Delete column → Ctrl+Z restores the column with all its cards. - [ ] Move card up / down → Ctrl+Z restores the order. - [ ] Move card to another column → Ctrl+Z restores the original column membership. - [ ] Duplicate card → Ctrl+Z removes the duplicate. - [ ] `deleteSelectedCard` and `addCardToFirstColumn` shortcuts participate in undo. - [ ] Column drag-reorder still undoes (no regression). - [ ] Whole-group move still undoes (no regression — object-layer drag pairs). - [ ] `onUpdate(group)` fires exactly once per mutation (no double emit). - [ ] Inline editor cancel (no change) does not push a no-op onto the history stack (`commitUpdate` no-ops on identical before/after). ### Notes - `snapshot` serializes via `WhiteboardSync.serializeForServer(group)`, which for kanban widgets includes `_kanbanState.columns` (the whole shape). Undo restores the entire kanban widget data as a single update, which is exactly what we want. - This does not introduce new RPCs — `onUpdate(group)` is the existing persistence path, used unchanged. - Multi-user broadcast continues to fire from `onUpdate`. Remote clients see one `object.updated` per kanban mutation, same as today. - Out of scope: mindmap / calendar / webframe / ungroup / selection-toolbar property edits. Same class of issue; separate follow-ups.
Author
Member

Test Results + Final Summary

Changes

  • static/web/js/whiteboard/kanban.js:
    • Added persistKanbanMutation(group) helper that runs WhiteboardHistory.commitUpdate(group.id()) then WhiteboardSync.onUpdate(group).
    • Replaced 14 mutation call sites with the snapshotBefore(group.id()) ... persistKanbanMutation(group) pattern.

Sites wrapped (line numbers post-edit)

  • Group dragend (already had pair — unified to use helper, line 84).
  • Group title edit on dblclick (line 205-208).
  • Add column button (line 237-242).
  • Column drag-reorder dragend success path (line 348 — unified to use helper).
  • Column title rename (line 387-391).
  • Column delete (line 407-411).
  • Add card per column (line 439-443).
  • Card drag-end success path (line 575 — unified to use helper).
  • Card text edit via dblclick (line 613-616).
  • Card menu Edit (line 699-702).
  • Card menu Move to other column (line 715-722).
  • Card menu Move Up (line 732-736).
  • Card menu Move Down (line 741-745).
  • Card menu Duplicate (line 753-757).
  • Card menu Delete (line 762-766).
  • deleteSelectedCard (line 891-898).
  • addCardToFirstColumn (line 905-908).

Left intentionally alone

  • The follow-up WhiteboardSync.onUpdate(group) after recomputeParentFrame(group) in dragend (line 86) — same logical state, no separate history entry needed.
  • Direct commitUpdate calls in column drag-reorder early-exit branches (lines 297, 336, 529, 549) — these are cleanup paths that bail without firing onUpdate.

Behaviour after fix

Every kanban mutation now becomes a single undo step. Cancelled inline editors (Escape, blur with no change) push nothing because commitUpdate no-ops on identical before/after snapshots. Multi-user broadcasts continue to fire exactly once per mutation.

Gates

  • node -c kanban.js — JS syntax OK
  • cargo fmt --check — pass
  • cargo clippy --workspace --all-targets -- -D warnings — pass
  • cargo test --workspace --lib — 0 tests, 0 failures

Acceptance Criteria

  • Add column → Ctrl+Z removes it.
  • Add card → Ctrl+Z removes it.
  • Edit column title → Ctrl+Z restores previous title.
  • Edit card text → Ctrl+Z restores previous text.
  • Delete card → Ctrl+Z restores the card with position.
  • Delete column → Ctrl+Z restores the column with all cards.
  • Move card up / down → Ctrl+Z restores order.
  • Move card to another column → Ctrl+Z restores original membership.
  • Duplicate card → Ctrl+Z removes the duplicate.
  • deleteSelectedCard / addCardToFirstColumn participate in undo.
  • Column drag-reorder still undoes (unified to use helper; verified the existing snapshot+commit pair is intact).
  • Whole-group move still undoes (object-layer drag bracketing — already correct).
  • onUpdate(group) fires exactly once per mutation (no double emit).

Manual verification still required

Rebuild + restart hero_whiteboard_admin, hard-reload, open a board with a kanban widget, walk through each mutation and confirm Ctrl+Z / Ctrl+Y behave correctly.

Out of scope (per the issue)

Mindmap / calendar / webframe URL / ungroup / selection-toolbar property edits — same class of issue, separate audit items.

## Test Results + Final Summary ### Changes - `static/web/js/whiteboard/kanban.js`: - Added `persistKanbanMutation(group)` helper that runs `WhiteboardHistory.commitUpdate(group.id())` then `WhiteboardSync.onUpdate(group)`. - Replaced 14 mutation call sites with the `snapshotBefore(group.id())` ... `persistKanbanMutation(group)` pattern. ### Sites wrapped (line numbers post-edit) - Group dragend (already had pair — unified to use helper, line 84). - Group title edit on dblclick (line 205-208). - Add column button (line 237-242). - Column drag-reorder dragend success path (line 348 — unified to use helper). - Column title rename (line 387-391). - Column delete (line 407-411). - Add card per column (line 439-443). - Card drag-end success path (line 575 — unified to use helper). - Card text edit via dblclick (line 613-616). - Card menu Edit (line 699-702). - Card menu Move to other column (line 715-722). - Card menu Move Up (line 732-736). - Card menu Move Down (line 741-745). - Card menu Duplicate (line 753-757). - Card menu Delete (line 762-766). - `deleteSelectedCard` (line 891-898). - `addCardToFirstColumn` (line 905-908). ### Left intentionally alone - The follow-up `WhiteboardSync.onUpdate(group)` after `recomputeParentFrame(group)` in `dragend` (line 86) — same logical state, no separate history entry needed. - Direct `commitUpdate` calls in column drag-reorder early-exit branches (lines 297, 336, 529, 549) — these are cleanup paths that bail without firing `onUpdate`. ### Behaviour after fix Every kanban mutation now becomes a single undo step. Cancelled inline editors (Escape, blur with no change) push nothing because `commitUpdate` no-ops on identical before/after snapshots. Multi-user broadcasts continue to fire exactly once per mutation. ### Gates - `node -c kanban.js` — JS syntax OK - `cargo fmt --check` — pass - `cargo clippy --workspace --all-targets -- -D warnings` — pass - `cargo test --workspace --lib` — 0 tests, 0 failures ### Acceptance Criteria - [x] Add column → Ctrl+Z removes it. - [x] Add card → Ctrl+Z removes it. - [x] Edit column title → Ctrl+Z restores previous title. - [x] Edit card text → Ctrl+Z restores previous text. - [x] Delete card → Ctrl+Z restores the card with position. - [x] Delete column → Ctrl+Z restores the column with all cards. - [x] Move card up / down → Ctrl+Z restores order. - [x] Move card to another column → Ctrl+Z restores original membership. - [x] Duplicate card → Ctrl+Z removes the duplicate. - [x] `deleteSelectedCard` / `addCardToFirstColumn` participate in undo. - [x] Column drag-reorder still undoes (unified to use helper; verified the existing snapshot+commit pair is intact). - [x] Whole-group move still undoes (object-layer drag bracketing — already correct). - [x] `onUpdate(group)` fires exactly once per mutation (no double emit). ### Manual verification still required Rebuild + restart `hero_whiteboard_admin`, hard-reload, open a board with a kanban widget, walk through each mutation and confirm Ctrl+Z / Ctrl+Y behave correctly. ### Out of scope (per the issue) Mindmap / calendar / webframe URL / ungroup / selection-toolbar property edits — same class of issue, separate audit items.
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#182
No description provided.