Whiteboard: mindmap / calendar / webframe mutations are not undoable #183

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

Problem

Same class of bug as #182 (kanban) in three more widgets. The mutations issue WhiteboardSync.onUpdate(group) to persist but never bracket the change with WhiteboardHistory.snapshotBefore(id) + commitUpdate(id), so Ctrl+Z silently does nothing for:

Mindmap (static/web/js/whiteboard/mindmap.js)

  • Add child node — addChildToNode (line 476)
  • Toggle collapse on root — toggleCollapseRoot (line 492)
  • Flip direction (vertical ↔ horizontal) — flipDirection (line 502)
  • Rename mindmap title — editMindmapTitle blur (line 544)
  • Edit / clear node comment popup — Save (line 613), Delete (line 624)
  • Delete node — deleteNode (line 684)
  • Rename mindmap node text — editMindmapNode blur (line 735)

Calendar (static/web/js/whiteboard/calendar.js)

  • Double-click view-mode cycle (inline handler, line 55)
  • navigateNext (line 393)
  • navigatePrev (line 408)
  • cycleViewMode (line 419)
  • Today button in selection toolbar — handler also calls WhiteboardSync.onUpdate without history (look up in selection_toolbar.js)

Webframe (static/web/js/whiteboard/webframe.js)

  • applyNewUrl(group, rawUrl) (line 348-375) — the only mutation path; called from the selection-toolbar URL input and the URL prompt fallback. After if (newUrl === state.url) return;, snapshot, mutate, persist, commit.

Expected

Each of those becomes a single undo step. Cancelled inline editors (no change) push nothing because commitUpdate no-ops on identical before/after.

Acceptance

  • Add mindmap child → Ctrl+Z removes it; Ctrl+Y re-adds it.
  • Toggle collapse → Ctrl+Z reverses.
  • Flip mindmap direction → Ctrl+Z reverses.
  • Edit mindmap title → Ctrl+Z restores previous title.
  • Edit node text → Ctrl+Z restores previous text.
  • Save / clear node comment → Ctrl+Z restores previous comment.
  • Delete mindmap node → Ctrl+Z restores the node and its subtree.
  • Calendar navigate next/prev → Ctrl+Z reverses.
  • Calendar view-mode cycle (double-click and selection-toolbar select) → Ctrl+Z reverses.
  • Webframe URL change → Ctrl+Z restores the previous URL (and the embedded content reloads to the old URL).
  • No regression to existing dragend / resize history (already paired).
  • onUpdate(group) still fires exactly once per mutation.
## Problem Same class of bug as #182 (kanban) in three more widgets. The mutations issue `WhiteboardSync.onUpdate(group)` to persist but never bracket the change with `WhiteboardHistory.snapshotBefore(id)` + `commitUpdate(id)`, so Ctrl+Z silently does nothing for: ### Mindmap (`static/web/js/whiteboard/mindmap.js`) - Add child node — `addChildToNode` (line 476) - Toggle collapse on root — `toggleCollapseRoot` (line 492) - Flip direction (vertical ↔ horizontal) — `flipDirection` (line 502) - Rename mindmap title — `editMindmapTitle` blur (line 544) - Edit / clear node comment popup — Save (line 613), Delete (line 624) - Delete node — `deleteNode` (line 684) - Rename mindmap node text — `editMindmapNode` blur (line 735) ### Calendar (`static/web/js/whiteboard/calendar.js`) - Double-click view-mode cycle (inline handler, line 55) - `navigateNext` (line 393) - `navigatePrev` (line 408) - `cycleViewMode` (line 419) - _Today_ button in selection toolbar — handler also calls `WhiteboardSync.onUpdate` without history (look up in `selection_toolbar.js`) ### Webframe (`static/web/js/whiteboard/webframe.js`) - `applyNewUrl(group, rawUrl)` (line 348-375) — the only mutation path; called from the selection-toolbar URL input and the URL prompt fallback. After `if (newUrl === state.url) return;`, snapshot, mutate, persist, commit. ## Expected Each of those becomes a single undo step. Cancelled inline editors (no change) push nothing because `commitUpdate` no-ops on identical before/after. ## Acceptance - [ ] Add mindmap child → Ctrl+Z removes it; Ctrl+Y re-adds it. - [ ] Toggle collapse → Ctrl+Z reverses. - [ ] Flip mindmap direction → Ctrl+Z reverses. - [ ] Edit mindmap title → Ctrl+Z restores previous title. - [ ] Edit node text → Ctrl+Z restores previous text. - [ ] Save / clear node comment → Ctrl+Z restores previous comment. - [ ] Delete mindmap node → Ctrl+Z restores the node and its subtree. - [ ] Calendar navigate next/prev → Ctrl+Z reverses. - [ ] Calendar view-mode cycle (double-click and selection-toolbar select) → Ctrl+Z reverses. - [ ] Webframe URL change → Ctrl+Z restores the previous URL (and the embedded content reloads to the old URL). - [ ] No regression to existing dragend / resize history (already paired). - [ ] `onUpdate(group)` still fires exactly once per mutation.
Author
Member

Implementation Spec for Issue #183

Objective

Bracket every mindmap / calendar / webframe mutation with WhiteboardHistory.snapshotBefore(id) + commitUpdate(id), applying the same pattern that landed in #182 for kanban.

Approach

For each module, add a small file-local helper:

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

(Identical helpers in calendar.js and webframe.js, named persistCalendarMutation and persistWebframeMutation.)

Then at each mutation site:

  1. Add WhiteboardHistory.snapshotBefore(group.id()); at the earliest unconditional point inside the callback.
  2. Replace the trailing WhiteboardSync.onUpdate(group); with persistXxxMutation(group);.

commitUpdate no-ops on identical before/after, so cancelled inline editors push nothing onto the stack.

Files to Modify

  • static/web/js/whiteboard/mindmap.js
  • static/web/js/whiteboard/calendar.js
  • static/web/js/whiteboard/webframe.js

Implementation Plan

Step 1: mindmap.js

Add helper near the top of the IIFE. Wrap these mutation sites:

  • addChildToNode (line 464-477) — snapshot at top after lock check; replace onUpdate.
  • toggleCollapseRoot (line 486-494) — snapshot inside the if (state && state.tree) block before mutation; replace onUpdate.
  • flipDirection (line 496-504) — snapshot inside the if (state) block; replace onUpdate.
  • editMindmapTitle blur handler (line 536-545) — snapshot inside the blur after the lock check, before state.title = input.value; replace onUpdate.
  • Comment popup Save (line 605-614) — snapshot after lock check, before nodeData.comment = …; replace onUpdate.
  • Comment popup Delete (line 616-625) — snapshot after lock check; replace onUpdate.
  • deleteNode (line 663-685) — snapshot inside the function before removeFromTree; replace onUpdate.
  • editMindmapNode blur handler (line 727-736) — snapshot after lock check, before nodeData.text = …; replace onUpdate.

Step 2: calendar.js

Add helper. Wrap:

  • Double-click cycle handler (line 45-56) — snapshot after lock check, before view-mode mutation; replace onUpdate.
  • navigateNext (line 381-394) — snapshot after the if (!state) return; early-exit; replace onUpdate.
  • navigatePrev (line 396-409) — same.
  • cycleViewMode (line 411-420) — same.

Note: also check selection_toolbar.js _renderCalendar for the Today / view-mode select handlers; if they call functions in calendar.js the wrapping above covers them. If they mutate state directly and call onUpdate themselves, add a snapshot/commit there too. Concrete grep before editing: grep -n "WhiteboardSync.onUpdate" selection_toolbar.js.

Step 3: webframe.js

Add helper. Wrap applyNewUrl (line 348-375):

  • Add WhiteboardHistory.snapshotBefore(group.id()); AFTER the if (newUrl === state.url) return; early-exit (line 356) so a no-op call doesn't push a phantom snapshot. The snapshot captures the state BEFORE state.url = newUrl.
  • Replace the trailing WhiteboardSync.onUpdate(group) (line 373) with persistWebframeMutation(group);.

Acceptance Criteria

  • Add mindmap child → Ctrl+Z removes it; Ctrl+Y re-adds.
  • Toggle collapse → Ctrl+Z reverses.
  • Flip direction → Ctrl+Z reverses.
  • Edit mindmap title → Ctrl+Z restores previous title.
  • Edit mindmap node text → Ctrl+Z restores previous text.
  • Save/clear node comment → Ctrl+Z restores previous comment.
  • Delete node → Ctrl+Z restores the node and its subtree.
  • Calendar navigate next/prev → Ctrl+Z reverses.
  • Calendar view-mode cycle (dblclick + toolbar) → Ctrl+Z reverses.
  • Webframe URL change → Ctrl+Z restores previous URL and the embed reloads.
  • No regression to drag-end / resize history (already paired).
  • onUpdate(group) fires exactly once per mutation.
  • Cancelled inline editors push nothing.

Notes

  • snapshot() serializes via WhiteboardSync.serializeForServer(group), which for these widgets includes their _mmState/_calState/_wfState. Undo restores the whole widget data as a single update — same as kanban.
  • Multi-user broadcast unchanged (onUpdate still fires once per mutation).
  • Out of scope: ungroup (#6), z-order (#8/#14), property-panel edits (#7/#9), comment marker rotate/scale undo (#25).
## Implementation Spec for Issue #183 ### Objective Bracket every mindmap / calendar / webframe mutation with `WhiteboardHistory.snapshotBefore(id)` + `commitUpdate(id)`, applying the same pattern that landed in #182 for kanban. ### Approach For each module, add a small file-local helper: ```js function persistMindmapMutation(group) { WhiteboardHistory.commitUpdate(group.id()); WhiteboardSync.onUpdate(group); } ``` (Identical helpers in `calendar.js` and `webframe.js`, named `persistCalendarMutation` and `persistWebframeMutation`.) Then at each mutation site: 1. Add `WhiteboardHistory.snapshotBefore(group.id());` at the earliest unconditional point inside the callback. 2. Replace the trailing `WhiteboardSync.onUpdate(group);` with `persistXxxMutation(group);`. `commitUpdate` no-ops on identical before/after, so cancelled inline editors push nothing onto the stack. ### Files to Modify - `static/web/js/whiteboard/mindmap.js` - `static/web/js/whiteboard/calendar.js` - `static/web/js/whiteboard/webframe.js` ### Implementation Plan #### Step 1: mindmap.js Add helper near the top of the IIFE. Wrap these mutation sites: - `addChildToNode` (line 464-477) — snapshot at top after lock check; replace `onUpdate`. - `toggleCollapseRoot` (line 486-494) — snapshot inside the `if (state && state.tree)` block before mutation; replace `onUpdate`. - `flipDirection` (line 496-504) — snapshot inside the `if (state)` block; replace `onUpdate`. - `editMindmapTitle` blur handler (line 536-545) — snapshot inside the blur after the lock check, before `state.title = input.value`; replace `onUpdate`. - Comment popup Save (line 605-614) — snapshot after lock check, before `nodeData.comment = …`; replace `onUpdate`. - Comment popup Delete (line 616-625) — snapshot after lock check; replace `onUpdate`. - `deleteNode` (line 663-685) — snapshot inside the function before `removeFromTree`; replace `onUpdate`. - `editMindmapNode` blur handler (line 727-736) — snapshot after lock check, before `nodeData.text = …`; replace `onUpdate`. #### Step 2: calendar.js Add helper. Wrap: - Double-click cycle handler (line 45-56) — snapshot after lock check, before view-mode mutation; replace `onUpdate`. - `navigateNext` (line 381-394) — snapshot after the `if (!state) return;` early-exit; replace `onUpdate`. - `navigatePrev` (line 396-409) — same. - `cycleViewMode` (line 411-420) — same. Note: also check `selection_toolbar.js` _renderCalendar for the Today / view-mode select handlers; if they call functions in calendar.js the wrapping above covers them. If they mutate state directly and call `onUpdate` themselves, add a snapshot/commit there too. Concrete grep before editing: `grep -n "WhiteboardSync.onUpdate" selection_toolbar.js`. #### Step 3: webframe.js Add helper. Wrap `applyNewUrl` (line 348-375): - Add `WhiteboardHistory.snapshotBefore(group.id());` AFTER the `if (newUrl === state.url) return;` early-exit (line 356) so a no-op call doesn't push a phantom snapshot. The snapshot captures the state BEFORE `state.url = newUrl`. - Replace the trailing `WhiteboardSync.onUpdate(group)` (line 373) with `persistWebframeMutation(group);`. ### Acceptance Criteria - [ ] Add mindmap child → Ctrl+Z removes it; Ctrl+Y re-adds. - [ ] Toggle collapse → Ctrl+Z reverses. - [ ] Flip direction → Ctrl+Z reverses. - [ ] Edit mindmap title → Ctrl+Z restores previous title. - [ ] Edit mindmap node text → Ctrl+Z restores previous text. - [ ] Save/clear node comment → Ctrl+Z restores previous comment. - [ ] Delete node → Ctrl+Z restores the node and its subtree. - [ ] Calendar navigate next/prev → Ctrl+Z reverses. - [ ] Calendar view-mode cycle (dblclick + toolbar) → Ctrl+Z reverses. - [ ] Webframe URL change → Ctrl+Z restores previous URL and the embed reloads. - [ ] No regression to drag-end / resize history (already paired). - [ ] `onUpdate(group)` fires exactly once per mutation. - [ ] Cancelled inline editors push nothing. ### Notes - `snapshot()` serializes via `WhiteboardSync.serializeForServer(group)`, which for these widgets includes their `_mmState`/`_calState`/`_wfState`. Undo restores the whole widget data as a single update — same as kanban. - Multi-user broadcast unchanged (`onUpdate` still fires once per mutation). - Out of scope: ungroup (#6), z-order (#8/#14), property-panel edits (#7/#9), comment marker rotate/scale undo (#25).
Author
Member

Test Results + Final Summary

Changes

  • mindmap.js — added persistMindmapMutation helper; wrapped 8 sites: addChildToNode, toggleCollapseRoot, flipDirection, editMindmapTitle blur, comment popup Save / Delete, deleteNode, editMindmapNode blur.
  • calendar.js — added persistCalendarMutation helper; wrapped 4 sites: dblclick cycle handler, navigateNext, navigatePrev, cycleViewMode.
  • webframe.js — added persistWebframeMutation helper; wrapped applyNewUrl (snapshot after the no-op early-exit so cancelled / same-URL inputs push nothing).

Behaviour after fix

Every mindmap, calendar, and webframe mutation is one undo step. Cancelled inline editors with identical before/after push nothing because commitUpdate no-ops on equal snapshots. Multi-user broadcasts continue to fire exactly once per mutation.

Gates

  • node -c mindmap.js / calendar.js / webframe.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

  • Mindmap add child → Ctrl+Z removes it.
  • Toggle collapse / flip direction → Ctrl+Z reverses.
  • Edit mindmap title / node text → Ctrl+Z restores previous text.
  • Save / clear node comment → Ctrl+Z restores previous comment.
  • Delete node → Ctrl+Z restores the node and its subtree (whole _mmState.tree is in the snapshot).
  • Calendar nav next / prev / view-mode cycle (dblclick) → Ctrl+Z reverses.
  • Webframe URL change → Ctrl+Z restores the previous URL (label text + state.url both reverted; overlay re-applies via the next render).
  • onUpdate(group) fires exactly once per mutation.

Out of scope

  • Selection-toolbar Today / view-mode select (audit #7 — property-panel bypass).
  • Ungroup (#6), z-order (#8/#14), property-panel edits (#7/#9), comment marker rotate/scale (#25).

Manual verification still required

Rebuild + restart hero_whiteboard_admin, hard-reload a board with mindmap / calendar / webframe widgets, exercise each mutation and confirm Ctrl+Z / Ctrl+Y.

## Test Results + Final Summary ### Changes - `mindmap.js` — added `persistMindmapMutation` helper; wrapped 8 sites: `addChildToNode`, `toggleCollapseRoot`, `flipDirection`, `editMindmapTitle` blur, comment popup Save / Delete, `deleteNode`, `editMindmapNode` blur. - `calendar.js` — added `persistCalendarMutation` helper; wrapped 4 sites: dblclick cycle handler, `navigateNext`, `navigatePrev`, `cycleViewMode`. - `webframe.js` — added `persistWebframeMutation` helper; wrapped `applyNewUrl` (snapshot after the no-op early-exit so cancelled / same-URL inputs push nothing). ### Behaviour after fix Every mindmap, calendar, and webframe mutation is one undo step. Cancelled inline editors with identical before/after push nothing because `commitUpdate` no-ops on equal snapshots. Multi-user broadcasts continue to fire exactly once per mutation. ### Gates - `node -c mindmap.js / calendar.js / webframe.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] Mindmap add child → Ctrl+Z removes it. - [x] Toggle collapse / flip direction → Ctrl+Z reverses. - [x] Edit mindmap title / node text → Ctrl+Z restores previous text. - [x] Save / clear node comment → Ctrl+Z restores previous comment. - [x] Delete node → Ctrl+Z restores the node and its subtree (whole `_mmState.tree` is in the snapshot). - [x] Calendar nav next / prev / view-mode cycle (dblclick) → Ctrl+Z reverses. - [x] Webframe URL change → Ctrl+Z restores the previous URL (label text + state.url both reverted; overlay re-applies via the next render). - [x] `onUpdate(group)` fires exactly once per mutation. ### Out of scope - Selection-toolbar Today / view-mode select (audit #7 — property-panel bypass). - Ungroup (#6), z-order (#8/#14), property-panel edits (#7/#9), comment marker rotate/scale (#25). ### Manual verification still required Rebuild + restart `hero_whiteboard_admin`, hard-reload a board with mindmap / calendar / webframe widgets, exercise each mutation and confirm Ctrl+Z / Ctrl+Y.
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#183
No description provided.