Mind map: changing the root node color does not propagate to other viewers and is not persisted on reload #94

Open
opened 2026-04-28 16:56:32 +00:00 by eslamnawara · 3 comments
Member

Summary

Editing the root node color on a mind map has two problems:

  1. Not synced live. The new color is applied only in the originating tab. Other tabs viewing the same board keep rendering the previous color until they reload.
  2. Not persisted. Reloading the originating tab restores the previous color.

Steps to reproduce

  1. Open a board in tab A.
  2. Open the same board in tab B via the shared link.
  3. Confirm both tabs are connected (cursor presence visible).
  4. In tab A, place a mind map with a Central Idea root node.
  5. In tab A, select the root node and change its color via the Properties
    panel (e.g. blue → black).
  6. Observe tab B.
  7. Reload tab A.

Expected

  • Tab A shows the new root color immediately.
  • Tab B receives the color change live and updates the root accordingly. (broken)
  • Reloading tab A keeps the new root color (the change persists). (broken)

Actual

  • Tab A: root re-renders in the new color immediately.
  • Tab B: root keeps rendering the old color indefinitely; only a manual
    reload picks up any change — and even then it picks up the old color,
    because the new value was never persisted.
  • Tab A after reload: root reverts to the previous color, confirming the
    change was never saved.
## Summary Editing the **root node** color on a mind map has two problems: 1. **Not synced live.** The new color is applied only in the originating tab. Other tabs viewing the same board keep rendering the previous color until they reload. 2. **Not persisted.** Reloading the originating tab restores the previous color. ## Steps to reproduce 1. Open a board in tab A. 2. Open the same board in tab B via the shared link. 3. Confirm both tabs are connected (cursor presence visible). 4. In tab A, place a mind map with a `Central Idea` root node. 5. In tab A, select the root node and change its color via the Properties panel (e.g. blue → black). 6. Observe tab B. 7. Reload tab A. ## Expected - Tab A shows the new root color immediately. - Tab B receives the color change live and updates the root accordingly. (broken) - Reloading tab A keeps the new root color (the change persists). (broken) ## Actual - Tab A: root re-renders in the new color immediately. - Tab B: root keeps rendering the old color indefinitely; only a manual reload picks up any change — and even then it picks up the old color, because the new value was never persisted. - Tab A after reload: root reverts to the previous color, confirming the change was never saved.
Member

Implementation Spec — Issue #94: Mind map root color does not sync or persist

Objective

Fix two related bugs in the mind-map Properties panel so that changing the root node's color (and, by symmetry, the root node's text) is both persisted to the server and broadcast live to all other viewers, using the existing realtime/persistence infra. No new RPC methods, no schema changes, smallest possible diff.

Root Cause (one bug, one fix)

In properties.js, the prop-mm-root-color and prop-mm-root-text input listeners mutate currentNode._mmState.tree.color / .text and call WhiteboardMindmap.redraw(currentNode) but never call WhiteboardSync.onUpdate(currentNode).

WhiteboardSync.onUpdate(node) already does BOTH in this codebase:

  • rpcCall('object.update', { ..., data }) — persistence (so reload retains the new color)
  • wsSendThrottled({ type: 'object.updated', data }) — live broadcast (so tab B updates without reload)

serializeForServer already pulls _mmState.tree into data.tree for mindmap objects, and the receiver branch in applySyncUpdate already replaces _mmState wholesale from data.tree and calls WhiteboardMindmap.redraw(node). Color is stored per tree node on tree.color — there is no separate rootColor/rootFill field; the root just renders with fill: node.color while non-root nodes render with fill: '#2b3035' and stroke: node.color. So as soon as the root's tree.color change reaches the server / peers, both rendering and persistence work end-to-end. The receiver path already handles it; only the sender path is broken.

Comparison points that already work and prove the model:

  • prop-mm-title input handler — calls WhiteboardSync.onUpdate(currentNode).
  • prop-mm-direction change handler — calls WhiteboardSync.onUpdate(currentNode).
  • Non-root node text edits via editMindmapNode — calls WhiteboardSync.onUpdate(group).
  • Add child / toggle / flip / drag — all call WhiteboardSync.onUpdate(group).

<input type="color"> fires input continuously while the user drags, but WhiteboardSync.onUpdate already debounces RPC writes via pendingUpdates/scheduleFlush and the WS broadcast goes through wsSendThrottled. No additional throttling needed.

Requirements

  • Changing the root color in the Properties panel persists to the server (reload of the same tab keeps the new color).
  • Changing the root color in tab A is reflected live in tab B without reload.
  • Same behavior for the root text input (prop-mm-root-text), since it has the identical bug.
  • No regressions to non-root color/text edits.
  • No new RPC methods, no new server schema, no new transports.
  • Match existing JS module style.

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js — add one line to each of two existing handlers.

Files to Leave Alone

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js — render and data model are correct; root color is correctly read from tree.color; _mmState.tree is the source of truth.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js — both serializeForServer (sender) and applySyncUpdate (receiver) for mindmap already handle full tree replacement correctly.
  • Any server-side Rust/RPC code — object.update already accepts the data blob.

Step-by-Step Plan

Step 1 — Make root color change sync + persist

File: crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js

In the prop-mm-root-color input handler, append WhiteboardSync.onUpdate(currentNode); after WhiteboardMindmap.redraw(currentNode);. The handler should match the shape of the prop-mm-title handler.

After change, the body reads:

mmRootColor.addEventListener('input', function() {
    if (!currentNode || !currentNode._mmState) return;
    currentNode._mmState.tree.color = mmRootColor.value;
    WhiteboardMindmap.redraw(currentNode);
    WhiteboardSync.onUpdate(currentNode);
});

Dependencies: none.

Step 2 — Make root text change sync + persist (same bug, same fix)

File: crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js

In the prop-mm-root-text input handler, append WhiteboardSync.onUpdate(currentNode); after WhiteboardMindmap.redraw(currentNode);. Body becomes:

mmRootText.addEventListener('input', function() {
    if (!currentNode || !currentNode._mmState) return;
    currentNode._mmState.tree.text = mmRootText.value;
    WhiteboardMindmap.redraw(currentNode);
    WhiteboardSync.onUpdate(currentNode);
});

Dependencies: none.

Acceptance Criteria

  • Changing the root color in tab A's Properties panel updates the root fill in tab B without reload.
  • After changing the root color in tab A and reloading tab A, the new color is shown.
  • Changing the root text in tab A's Properties panel updates the root label in tab B without reload.
  • After changing the root text in tab A and reloading tab A, the new text is shown.
  • Non-root node color/text edits (via in-canvas double-click) continue to sync and persist as before.
  • Title, direction, add-child, collapse-toggle, flip, drag all continue to sync and persist as before.
  • No new console errors during rapid color picker dragging.
  • Diff is exactly two added lines in properties.js; no other files changed.

Notes

  • Both bugs share a single fix per handler: call the existing WhiteboardSync.onUpdate(currentNode). Persistence and live sync are inseparable here — onUpdate performs both in flushUpdates.
  • Color is stored on tree.color per node; there is no rootColor/rootFill field. The root simply renders with fill: node.color where non-roots use fill: '#2b3035' and stroke: node.color.
  • The receiver in applySyncUpdate replaces _mmState wholesale from data.tree, then calls WhiteboardMindmap.redraw(node), so the new root color renders correctly on remote tabs without further changes.
  • Color-picker input events fire many times per second; pendingUpdates collapses them to one node entry and scheduleFlush debounces the actual RPC. Final state is what gets persisted.
  • Future cleanup (out of scope): the four prop-mm-* handlers could share a tiny helper that mutates state, redraws, and syncs.
# Implementation Spec — Issue #94: Mind map root color does not sync or persist ## Objective Fix two related bugs in the mind-map Properties panel so that changing the root node's color (and, by symmetry, the root node's text) is both persisted to the server and broadcast live to all other viewers, using the existing realtime/persistence infra. No new RPC methods, no schema changes, smallest possible diff. ## Root Cause (one bug, one fix) In `properties.js`, the `prop-mm-root-color` and `prop-mm-root-text` `input` listeners mutate `currentNode._mmState.tree.color` / `.text` and call `WhiteboardMindmap.redraw(currentNode)` but never call `WhiteboardSync.onUpdate(currentNode)`. `WhiteboardSync.onUpdate(node)` already does BOTH in this codebase: - `rpcCall('object.update', { ..., data })` — persistence (so reload retains the new color) - `wsSendThrottled({ type: 'object.updated', data })` — live broadcast (so tab B updates without reload) `serializeForServer` already pulls `_mmState.tree` into `data.tree` for `mindmap` objects, and the receiver branch in `applySyncUpdate` already replaces `_mmState` wholesale from `data.tree` and calls `WhiteboardMindmap.redraw(node)`. Color is stored per tree node on `tree.color` — there is no separate `rootColor`/`rootFill` field; the root just renders with `fill: node.color` while non-root nodes render with `fill: '#2b3035'` and `stroke: node.color`. So as soon as the root's `tree.color` change reaches the server / peers, both rendering and persistence work end-to-end. The receiver path already handles it; only the sender path is broken. Comparison points that already work and prove the model: - `prop-mm-title` input handler — calls `WhiteboardSync.onUpdate(currentNode)`. - `prop-mm-direction` change handler — calls `WhiteboardSync.onUpdate(currentNode)`. - Non-root node text edits via `editMindmapNode` — calls `WhiteboardSync.onUpdate(group)`. - Add child / toggle / flip / drag — all call `WhiteboardSync.onUpdate(group)`. `<input type="color">` fires `input` continuously while the user drags, but `WhiteboardSync.onUpdate` already debounces RPC writes via `pendingUpdates`/`scheduleFlush` and the WS broadcast goes through `wsSendThrottled`. No additional throttling needed. ## Requirements - Changing the root color in the Properties panel persists to the server (reload of the same tab keeps the new color). - Changing the root color in tab A is reflected live in tab B without reload. - Same behavior for the root text input (`prop-mm-root-text`), since it has the identical bug. - No regressions to non-root color/text edits. - No new RPC methods, no new server schema, no new transports. - Match existing JS module style. ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` — add one line to each of two existing handlers. ## Files to Leave Alone - `crates/hero_whiteboard_ui/static/web/js/whiteboard/mindmap.js` — render and data model are correct; root color is correctly read from `tree.color`; `_mmState.tree` is the source of truth. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/sync.js` — both `serializeForServer` (sender) and `applySyncUpdate` (receiver) for `mindmap` already handle full tree replacement correctly. - Any server-side Rust/RPC code — `object.update` already accepts the `data` blob. ## Step-by-Step Plan ### Step 1 — Make root color change sync + persist File: `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` In the `prop-mm-root-color` `input` handler, append `WhiteboardSync.onUpdate(currentNode);` after `WhiteboardMindmap.redraw(currentNode);`. The handler should match the shape of the `prop-mm-title` handler. After change, the body reads: ``` mmRootColor.addEventListener('input', function() { if (!currentNode || !currentNode._mmState) return; currentNode._mmState.tree.color = mmRootColor.value; WhiteboardMindmap.redraw(currentNode); WhiteboardSync.onUpdate(currentNode); }); ``` Dependencies: none. ### Step 2 — Make root text change sync + persist (same bug, same fix) File: `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` In the `prop-mm-root-text` `input` handler, append `WhiteboardSync.onUpdate(currentNode);` after `WhiteboardMindmap.redraw(currentNode);`. Body becomes: ``` mmRootText.addEventListener('input', function() { if (!currentNode || !currentNode._mmState) return; currentNode._mmState.tree.text = mmRootText.value; WhiteboardMindmap.redraw(currentNode); WhiteboardSync.onUpdate(currentNode); }); ``` Dependencies: none. ## Acceptance Criteria - [ ] Changing the root color in tab A's Properties panel updates the root fill in tab B without reload. - [ ] After changing the root color in tab A and reloading tab A, the new color is shown. - [ ] Changing the root text in tab A's Properties panel updates the root label in tab B without reload. - [ ] After changing the root text in tab A and reloading tab A, the new text is shown. - [ ] Non-root node color/text edits (via in-canvas double-click) continue to sync and persist as before. - [ ] Title, direction, add-child, collapse-toggle, flip, drag all continue to sync and persist as before. - [ ] No new console errors during rapid color picker dragging. - [ ] Diff is exactly two added lines in `properties.js`; no other files changed. ## Notes - Both bugs share a single fix per handler: call the existing `WhiteboardSync.onUpdate(currentNode)`. Persistence and live sync are inseparable here — `onUpdate` performs both in `flushUpdates`. - Color is stored on `tree.color` per node; there is no `rootColor`/`rootFill` field. The root simply renders with `fill: node.color` where non-roots use `fill: '#2b3035'` and `stroke: node.color`. - The receiver in `applySyncUpdate` replaces `_mmState` wholesale from `data.tree`, then calls `WhiteboardMindmap.redraw(node)`, so the new root color renders correctly on remote tabs without further changes. - Color-picker `input` events fire many times per second; `pendingUpdates` collapses them to one node entry and `scheduleFlush` debounces the actual RPC. Final state is what gets persisted. - Future cleanup (out of scope): the four `prop-mm-*` handlers could share a tiny helper that mutates state, redraws, and syncs.
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo test --workspace --lib: pass
  • node --check properties.js: pass
## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - cargo clippy --workspace -- -D warnings: pass - cargo test --workspace --lib: pass - node --check properties.js: pass
Member

Implementation Summary

One file changed, two added lines (+2/-0). No server / SDK / OpenRPC / DB changes.

Root cause

The prop-mm-root-color and prop-mm-root-text input listeners in properties.js mutated currentNode._mmState.tree.color / .text and re-rendered the mind map locally, but they never called WhiteboardSync.onUpdate(currentNode). That single function is what handles both:

  • rpcCall('object.update', { ..., data }) — persistence
  • wsSendThrottled({ type: 'object.updated', data }) — live broadcast

So the same gap caused both observed bugs: changes weren't persisted (so reload reverted them) and weren't broadcast (so other tabs never saw them).

crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js

  • Added WhiteboardSync.onUpdate(currentNode); in the prop-mm-root-text input handler, after WhiteboardMindmap.redraw(currentNode);.
  • Added the same call in the prop-mm-root-color input handler.

Both handlers now match the shape of the working prop-mm-title handler. The receiver path in sync.js applySyncUpdate already replaces _mmState wholesale from data.tree and calls WhiteboardMindmap.redraw(node), so no additional changes are needed for remote rendering.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • cargo clippy --workspace -- -D warnings: clean
  • cargo test --workspace --lib: pass
  • node --check properties.js: clean

Manual smoke test

  1. Open the same board in two tabs (A and B). Place a mind map with a root node.
  2. In tab A select the root and drag the color picker — tab B updates within ~200-500 ms (sync debounce + WS throttle).
  3. Reload tab A — the new color persists.
  4. Repeat with the root text input.
  5. Non-root edits, title, direction, add-child, collapse-toggle, flip, drag continue to behave as before (no regression).

Notes

  • Color picker input events fire many times per second during dragging; pendingUpdates collapses them to one node entry and scheduleFlush debounces the actual RPC, so only the final state is persisted.
  • Color is stored per tree node on tree.color. There is no separate rootColor/rootFill field — the root simply renders with fill: node.color while non-roots use fill: '#2b3035' and stroke: node.color.
## Implementation Summary One file changed, two added lines (`+2/-0`). No server / SDK / OpenRPC / DB changes. ### Root cause The `prop-mm-root-color` and `prop-mm-root-text` `input` listeners in `properties.js` mutated `currentNode._mmState.tree.color` / `.text` and re-rendered the mind map locally, but they never called `WhiteboardSync.onUpdate(currentNode)`. That single function is what handles both: - `rpcCall('object.update', { ..., data })` — persistence - `wsSendThrottled({ type: 'object.updated', data })` — live broadcast So the same gap caused both observed bugs: changes weren't persisted (so reload reverted them) and weren't broadcast (so other tabs never saw them). ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/properties.js` - Added `WhiteboardSync.onUpdate(currentNode);` in the `prop-mm-root-text` input handler, after `WhiteboardMindmap.redraw(currentNode);`. - Added the same call in the `prop-mm-root-color` input handler. Both handlers now match the shape of the working `prop-mm-title` handler. The receiver path in `sync.js` `applySyncUpdate` already replaces `_mmState` wholesale from `data.tree` and calls `WhiteboardMindmap.redraw(node)`, so no additional changes are needed for remote rendering. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `cargo clippy --workspace -- -D warnings`: clean - `cargo test --workspace --lib`: pass - `node --check properties.js`: clean ### Manual smoke test 1. Open the same board in two tabs (A and B). Place a mind map with a root node. 2. In tab A select the root and drag the color picker — tab B updates within ~200-500 ms (sync debounce + WS throttle). 3. Reload tab A — the new color persists. 4. Repeat with the root text input. 5. Non-root edits, title, direction, add-child, collapse-toggle, flip, drag continue to behave as before (no regression). ### Notes - Color picker `input` events fire many times per second during dragging; `pendingUpdates` collapses them to one node entry and `scheduleFlush` debounces the actual RPC, so only the final state is persisted. - Color is stored per tree node on `tree.color`. There is no separate `rootColor`/`rootFill` field — the root simply renders with `fill: node.color` while non-roots use `fill: '#2b3035'` and `stroke: node.color`.
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#94
No description provided.