Frame drag does not move external widgets (calendar, kanban, mindmap, webframe) inside it #105

Open
opened 2026-04-29 10:33:04 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

After issue #96 introduced explicit parent_frame_id for frame membership, dragging a frame correctly moves the seven core object types (sticky, text, shape, document, image, emoji, drawing) that are members of it. External widgets — calendar, kanban, mindmap, webframe — do not move with their containing frame, even when they are visually inside it.

This was called out as a follow-up in the #96 summary; this ticket tracks doing the work.

Steps to reproduce

  1. Open a board.
  2. Place a frame.
  3. Drop a kanban (or calendar / mindmap / webframe) inside the frame's bounds.
  4. Drag the frame to a new position on the canvas.

Expected

The widget moves with the frame, the same way a sticky note does.

Actual

The frame moves; the widget stays where it was. The widget effectively detaches from its visual container.

Root cause

The four external widget modules (calendar.js, kanban.js, mindmap.js, webframe.js) own their own registry construction:

  • objStore[id] = { group: group, type: 'webframe', url: url }; (webframe.js:93)
  • objStore[id] = { group: group, type: 'mindmap' }; (mindmap.js:63)
  • objStore[id] = { group: group, type: 'kanban' }; (kanban.js:84)
  • objStore[id] = { group: group, type: 'calendar', viewMode: viewMode }; (calendar.js:59)

None of them carry a parent_frame_id field. Issue #96 only extended the seven core types' registry entries (in objects.js) and only called recomputeParentFrame(group) in those types' dragend handlers. As a result, external widgets never get a parent_frame_id set, and createFrame's id-based capture loop in dragstart never picks them up.

Suggested fix

For each of calendar.js, kanban.js, mindmap.js, webframe.js:

  1. Accept opts.parent_frame_id in the create function and add it to the registry entry: parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null.
  2. After the registry write, when no opts.id is present (i.e. local user creation, not sync hydration), call WhiteboardObjects.recomputeParentFrame(group) (which would need to be exposed as a public export) so a widget dropped inside a frame's bounds gets its membership set.
  3. In each widget's dragend handler, after the existing WhiteboardSync.onUpdate(group) call, call if (WhiteboardObjects.recomputeParentFrame(group)) WhiteboardSync.onUpdate(group); so dragging the widget into / out of a frame transitions membership.

Then export recomputeParentFrame on the WhiteboardObjects public API. The existing _fromSync semantics in applySyncUpdate already update the registry's parent_frame_id from server-broadcast payloads; that path is shared with the seven core types and needs no change.

app.js::createObjectFromData already passes parent_frame_id: obj.parent_frame_id to every external widget's create* call (added in #96), so once each module accepts the opt and stores it, server-driven hydration on initial load and on object.created broadcasts will round-trip correctly.

Acceptance criteria

  • Drop a kanban inside a frame, drag the frame — the kanban moves with it.
  • Same for calendar, mindmap, webframe.
  • Dragging an external widget out of a frame clears its parent_frame_id (next frame drag doesn't pick it up).
  • Reloading the board preserves membership for external widgets.
  • Two-window sync: changes to an external widget's parent_frame_id propagate via object.updated and the receiver's local registry is updated.
  • No regression to the seven core types fixed in #96.
  • Webframe sync from #101 still works (position/size/url tracking on remote updates).
## Summary After issue #96 introduced explicit `parent_frame_id` for frame membership, dragging a frame correctly moves the seven core object types (sticky, text, shape, document, image, emoji, drawing) that are members of it. **External widgets — calendar, kanban, mindmap, webframe — do not move with their containing frame**, even when they are visually inside it. This was called out as a follow-up in the #96 summary; this ticket tracks doing the work. ## Steps to reproduce 1. Open a board. 2. Place a frame. 3. Drop a kanban (or calendar / mindmap / webframe) inside the frame's bounds. 4. Drag the frame to a new position on the canvas. ## Expected The widget moves with the frame, the same way a sticky note does. ## Actual The frame moves; the widget stays where it was. The widget effectively detaches from its visual container. ## Root cause The four external widget modules (`calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js`) own their own registry construction: - `objStore[id] = { group: group, type: 'webframe', url: url };` (webframe.js:93) - `objStore[id] = { group: group, type: 'mindmap' };` (mindmap.js:63) - `objStore[id] = { group: group, type: 'kanban' };` (kanban.js:84) - `objStore[id] = { group: group, type: 'calendar', viewMode: viewMode };` (calendar.js:59) None of them carry a `parent_frame_id` field. Issue #96 only extended the seven core types' registry entries (in `objects.js`) and only called `recomputeParentFrame(group)` in those types' `dragend` handlers. As a result, external widgets never get a `parent_frame_id` set, and `createFrame`'s id-based capture loop in `dragstart` never picks them up. ## Suggested fix For each of `calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js`: 1. Accept `opts.parent_frame_id` in the create function and add it to the registry entry: `parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null`. 2. After the registry write, when no `opts.id` is present (i.e. local user creation, not sync hydration), call `WhiteboardObjects.recomputeParentFrame(group)` (which would need to be exposed as a public export) so a widget dropped inside a frame's bounds gets its membership set. 3. In each widget's `dragend` handler, after the existing `WhiteboardSync.onUpdate(group)` call, call `if (WhiteboardObjects.recomputeParentFrame(group)) WhiteboardSync.onUpdate(group);` so dragging the widget into / out of a frame transitions membership. Then export `recomputeParentFrame` on the `WhiteboardObjects` public API. The existing `_fromSync` semantics in `applySyncUpdate` already update the registry's `parent_frame_id` from server-broadcast payloads; that path is shared with the seven core types and needs no change. `app.js::createObjectFromData` already passes `parent_frame_id: obj.parent_frame_id` to every external widget's `create*` call (added in #96), so once each module accepts the opt and stores it, server-driven hydration on initial load and on `object.created` broadcasts will round-trip correctly. ## Acceptance criteria - [ ] Drop a kanban inside a frame, drag the frame — the kanban moves with it. - [ ] Same for calendar, mindmap, webframe. - [ ] Dragging an external widget out of a frame clears its `parent_frame_id` (next frame drag doesn't pick it up). - [ ] Reloading the board preserves membership for external widgets. - [ ] Two-window sync: changes to an external widget's `parent_frame_id` propagate via `object.updated` and the receiver's local registry is updated. - [ ] No regression to the seven core types fixed in #96. - [ ] Webframe sync from #101 still works (position/size/url tracking on remote updates).
Author
Member

Spec — Issue #105: Frame drag must move external widgets inside it

Objective

Make the four external widget modules (calendar.js, kanban.js, mindmap.js, webframe.js) participate in id-based frame capture the same way the seven core types in objects.js already do (post-#96). After this change, dragging a frame carries calendar/kanban/mindmap/webframe children with it, and dragging one of these widgets into or out of a frame updates its parent_frame_id and broadcasts it.

Requirements

  • Each external widget's registry entry must carry a parent_frame_id field, initialized from opts.parent_frame_id (so server-loaded widgets keep their server-side membership) or null for fresh creates.
  • Each external widget's outer-group dragend handler must call WhiteboardObjects.recomputeParentFrame(group) so dropping inside/outside a frame updates membership and triggers a sync.
  • Each external widget's create function must, on local-only creation (!opts.id), run recomputeParentFrame(group) BEFORE WhiteboardSync.onCreate(group) so the broadcasted create payload includes the freshly-set parent.
  • recomputeParentFrame must be exported on the WhiteboardObjects public API.
  • No changes to transports, RPC, schema, or server code — the field already round-trips end-to-end (verified in sync.js::serializeForServer and app.js::createObjectFromData).

Files to Modify

  • crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js — export recomputeParentFrame.
  • crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js, kanban.js, mindmap.js, webframe.js — two surgical edits each (dragend tail + registry-entry line + conditional recompute on local create).

Files to Leave Alone

  • sync.js, app.js, server crates — already wired post-#96.
  • Inner kanban.js handlers (column dragend, card dragend) — intra-board moves, kanban group itself doesn't move.
  • webframe.js stage-scoped dragend.wf_<id> listeners — handle iframe overlay during canvas pan/zoom, unrelated to membership.

Step-by-Step Plan

Step 1 — Export recomputeParentFrame from WhiteboardObjects

Add recomputeParentFrame: recomputeParentFrame, to the public API return block in objects.js (e.g. after redrawAllFrameBadges). Blocks Steps 2–5.

Step 2 — calendar.js

In createCalendar:

  • After WhiteboardSync.onUpdate(group); in the outer-group dragend, append:
    if (WhiteboardObjects.recomputeParentFrame(group)) {
        WhiteboardSync.onUpdate(group);
    }
    
  • Replace the registry-entry write with:
    objStore[id] = { group: group, type: 'calendar', viewMode: viewMode, parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null };
    if (!opts.id) WhiteboardObjects.recomputeParentFrame(group);
    
    Order: registry write → recompute → existing WhiteboardSync.onCreate(group).

Step 3 — kanban.js

Same pattern in createKanban's outer-group dragend and registry-entry write. Do NOT touch colTitle/cardGroup dragend handlers.

Step 4 — mindmap.js

Same pattern in createMindmap.

Step 5 — webframe.js

Same pattern in createWebframe. Keep the existing dragend ordering: showOverlaycommitUpdateonUpdate, then append the recompute branch. Do NOT touch the stage-scoped dragend.wf_<id> listeners.

Parallelism

  • Step 1 blocks Steps 2-5.
  • Steps 2-5 are independent; runtime IIFE order already loads objects.js before the widget files.

Acceptance Criteria

  • Drop a calendar over a frame → its parent_frame_id becomes that frame's id; subsequently dragging the frame moves the calendar. Same for kanban, mindmap, webframe.
  • Drag any of the four widgets out of a frame → its parent_frame_id becomes null and a sync update is sent.
  • Local create over an existing frame produces a registry entry with the correct parent_frame_id, and the broadcast object.create payload carries it (verified via network or another client).
  • Loading a board where any of the four widgets has a server-side parent_frame_id keeps the membership through reload — frame drag still captures them.
  • The seven core types and the frame type itself behave exactly as before — no regressions.
  • No new console warnings or undefined-reference errors.

Notes

  • Each widget file gets two surgical edits; objects.js gets one new line in the export block.
  • recomputeParentFrame early-returns for type === 'frame', so frames stay out of recursive parenting.
  • No new transports/RPC/DB/migrations — parent_frame_id already round-trips end-to-end.
# Spec — Issue #105: Frame drag must move external widgets inside it ## Objective Make the four external widget modules (`calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js`) participate in id-based frame capture the same way the seven core types in `objects.js` already do (post-#96). After this change, dragging a frame carries calendar/kanban/mindmap/webframe children with it, and dragging one of these widgets into or out of a frame updates its `parent_frame_id` and broadcasts it. ## Requirements - Each external widget's registry entry must carry a `parent_frame_id` field, initialized from `opts.parent_frame_id` (so server-loaded widgets keep their server-side membership) or `null` for fresh creates. - Each external widget's outer-group `dragend` handler must call `WhiteboardObjects.recomputeParentFrame(group)` so dropping inside/outside a frame updates membership and triggers a sync. - Each external widget's create function must, on local-only creation (`!opts.id`), run `recomputeParentFrame(group)` BEFORE `WhiteboardSync.onCreate(group)` so the broadcasted create payload includes the freshly-set parent. - `recomputeParentFrame` must be exported on the `WhiteboardObjects` public API. - No changes to transports, RPC, schema, or server code — the field already round-trips end-to-end (verified in `sync.js::serializeForServer` and `app.js::createObjectFromData`). ## Files to Modify - `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` — export `recomputeParentFrame`. - `crates/hero_whiteboard_ui/static/web/js/whiteboard/calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js` — two surgical edits each (dragend tail + registry-entry line + conditional recompute on local create). ## Files to Leave Alone - `sync.js`, `app.js`, server crates — already wired post-#96. - Inner `kanban.js` handlers (column dragend, card dragend) — intra-board moves, kanban group itself doesn't move. - `webframe.js` stage-scoped `dragend.wf_<id>` listeners — handle iframe overlay during canvas pan/zoom, unrelated to membership. ## Step-by-Step Plan ### Step 1 — Export `recomputeParentFrame` from `WhiteboardObjects` Add `recomputeParentFrame: recomputeParentFrame,` to the public API return block in `objects.js` (e.g. after `redrawAllFrameBadges`). Blocks Steps 2–5. ### Step 2 — `calendar.js` In `createCalendar`: - After `WhiteboardSync.onUpdate(group);` in the outer-group `dragend`, append: ```js if (WhiteboardObjects.recomputeParentFrame(group)) { WhiteboardSync.onUpdate(group); } ``` - Replace the registry-entry write with: ```js objStore[id] = { group: group, type: 'calendar', viewMode: viewMode, parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null }; if (!opts.id) WhiteboardObjects.recomputeParentFrame(group); ``` Order: registry write → recompute → existing `WhiteboardSync.onCreate(group)`. ### Step 3 — `kanban.js` Same pattern in `createKanban`'s outer-group `dragend` and registry-entry write. Do NOT touch `colTitle`/`cardGroup` dragend handlers. ### Step 4 — `mindmap.js` Same pattern in `createMindmap`. ### Step 5 — `webframe.js` Same pattern in `createWebframe`. Keep the existing dragend ordering: `showOverlay` → `commitUpdate` → `onUpdate`, then append the recompute branch. Do NOT touch the stage-scoped `dragend.wf_<id>` listeners. ### Parallelism - Step 1 blocks Steps 2-5. - Steps 2-5 are independent; runtime IIFE order already loads `objects.js` before the widget files. ## Acceptance Criteria - [ ] Drop a calendar over a frame → its `parent_frame_id` becomes that frame's id; subsequently dragging the frame moves the calendar. Same for kanban, mindmap, webframe. - [ ] Drag any of the four widgets out of a frame → its `parent_frame_id` becomes `null` and a sync update is sent. - [ ] Local create over an existing frame produces a registry entry with the correct `parent_frame_id`, and the broadcast `object.create` payload carries it (verified via network or another client). - [ ] Loading a board where any of the four widgets has a server-side `parent_frame_id` keeps the membership through reload — frame drag still captures them. - [ ] The seven core types and the `frame` type itself behave exactly as before — no regressions. - [ ] No new console warnings or undefined-reference errors. ## Notes - Each widget file gets two surgical edits; `objects.js` gets one new line in the export block. - `recomputeParentFrame` early-returns for `type === 'frame'`, so frames stay out of recursive parenting. - No new transports/RPC/DB/migrations — `parent_frame_id` already round-trips end-to-end.
Author
Member

Test Results

  • cargo fmt --all -- --check: pass
  • cargo check --workspace: pass
  • cargo clippy --workspace -- -D warnings: pass
  • cargo test -p hero_whiteboard_server: pass (3/3)
  • node --check on all 5 modified JS files: pass

Integration tests in hero_whiteboard_examples require a running hero_proc (per project CLAUDE.md) and were not run in this sandbox — unrelated to this JS-only change.

## Test Results - cargo fmt --all -- --check: pass - cargo check --workspace: pass - cargo clippy --workspace -- -D warnings: pass - cargo test -p hero_whiteboard_server: pass (3/3) - node --check on all 5 modified JS files: pass Integration tests in hero_whiteboard_examples require a running hero_proc (per project CLAUDE.md) and were not run in this sandbox — unrelated to this JS-only change.
Author
Member

Implementation Summary

Five files changed, +21/-5. The seven core types in objects.js already participate in id-based frame capture (post-#96); this change extends the same plumbing to the four external widget modules.

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

Added recomputeParentFrame: recomputeParentFrame, to the public API export block so the widget modules can reach the helper.

calendar.js, kanban.js, mindmap.js, webframe.js

Same two surgical edits in each, inside their respective create function (createCalendar, createKanban, createMindmap, createWebframe):

  1. After WhiteboardSync.onUpdate(group); in the outer-group dragend, append:

    if (WhiteboardObjects.recomputeParentFrame(group)) {
        WhiteboardSync.onUpdate(group);
    }
    

    Persists and broadcasts a membership change when the user drags the widget into or out of a frame.

  2. Extend the registry-entry write to include parent_frame_id, then run recomputeParentFrame on local creates before WhiteboardSync.onCreate(group):

    objStore[id] = { ...existing fields..., parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null };
    if (!opts.id) WhiteboardObjects.recomputeParentFrame(group);
    

    Sync-hydration paths (which always pass opts.id) skip the recompute and trust the server-supplied parent_frame_id. Fresh local creates inherit membership from whichever frame contains the new widget's bounding rect.

webframe.js's outer-group dragend keeps its existing order (showOverlay → commitUpdate → onUpdate) and appends the recompute branch last. The stage-scoped dragend.wf_<id> listeners (used for the iframe overlay during canvas pan/zoom) are intentionally untouched.

kanban.js's inner colTitle and cardGroup dragend handlers (intra-board moves) are intentionally untouched.

Verification

  • cargo fmt --all -- --check: clean
  • cargo check --workspace: clean
  • cargo clippy --workspace -- -D warnings: clean
  • cargo test -p hero_whiteboard_server: 3 passed, 0 failed
  • node --check on objects.js, calendar.js, kanban.js, mindmap.js, webframe.js: all clean

The two integration tests in hero_whiteboard_examples require a running hero_proc (per project CLAUDE.md) and were not run in this sandbox — they are unrelated to this JS-only change.

Manual smoke test

  1. Place a frame. Drop a calendar inside it. Drag the frame — the calendar follows.
  2. Same with kanban, mindmap, webframe.
  3. Drag any of the four widgets out of a frame, then drag the frame — the widget no longer follows.
  4. Reload the board — membership persists for all four widget types.
  5. Two-window: changes to membership in tab A are reflected in tab B via object.updated.
  6. Verify the seven core types (sticky/text/shape/document/image/emoji/drawing) still behave as before. Frame drag still works for the frame and its core-type children.

Notes / scope

  • recomputeParentFrame early-returns for type === 'frame', preserving the no-recursive-parenting invariant.
  • No new transports/RPC/DB/migrations — parent_frame_id already round-trips end-to-end through serializeForServer, applySyncUpdate, and app.js::createObjectFromData.
  • webframe.js's recent #101 (sync), #102 (presentation), #103 (rotation), and #104 (delete cleanup) fixes are unaffected.
## Implementation Summary Five files changed, +21/-5. The seven core types in `objects.js` already participate in id-based frame capture (post-#96); this change extends the same plumbing to the four external widget modules. ### `crates/hero_whiteboard_ui/static/web/js/whiteboard/objects.js` Added `recomputeParentFrame: recomputeParentFrame,` to the public API export block so the widget modules can reach the helper. ### `calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js` Same two surgical edits in each, inside their respective create function (`createCalendar`, `createKanban`, `createMindmap`, `createWebframe`): 1. After `WhiteboardSync.onUpdate(group);` in the outer-group `dragend`, append: ```js if (WhiteboardObjects.recomputeParentFrame(group)) { WhiteboardSync.onUpdate(group); } ``` Persists and broadcasts a membership change when the user drags the widget into or out of a frame. 2. Extend the registry-entry write to include `parent_frame_id`, then run `recomputeParentFrame` on local creates before `WhiteboardSync.onCreate(group)`: ```js objStore[id] = { ...existing fields..., parent_frame_id: opts.parent_frame_id != null ? Number(opts.parent_frame_id) : null }; if (!opts.id) WhiteboardObjects.recomputeParentFrame(group); ``` Sync-hydration paths (which always pass `opts.id`) skip the recompute and trust the server-supplied `parent_frame_id`. Fresh local creates inherit membership from whichever frame contains the new widget's bounding rect. `webframe.js`'s outer-group dragend keeps its existing order (`showOverlay → commitUpdate → onUpdate`) and appends the recompute branch last. The stage-scoped `dragend.wf_<id>` listeners (used for the iframe overlay during canvas pan/zoom) are intentionally untouched. `kanban.js`'s inner `colTitle` and `cardGroup` dragend handlers (intra-board moves) are intentionally untouched. ### Verification - `cargo fmt --all -- --check`: clean - `cargo check --workspace`: clean - `cargo clippy --workspace -- -D warnings`: clean - `cargo test -p hero_whiteboard_server`: 3 passed, 0 failed - `node --check` on `objects.js`, `calendar.js`, `kanban.js`, `mindmap.js`, `webframe.js`: all clean The two integration tests in `hero_whiteboard_examples` require a running `hero_proc` (per project CLAUDE.md) and were not run in this sandbox — they are unrelated to this JS-only change. ### Manual smoke test 1. Place a frame. Drop a calendar inside it. Drag the frame — the calendar follows. 2. Same with kanban, mindmap, webframe. 3. Drag any of the four widgets out of a frame, then drag the frame — the widget no longer follows. 4. Reload the board — membership persists for all four widget types. 5. Two-window: changes to membership in tab A are reflected in tab B via `object.updated`. 6. Verify the seven core types (sticky/text/shape/document/image/emoji/drawing) still behave as before. Frame drag still works for the frame and its core-type children. ### Notes / scope - `recomputeParentFrame` early-returns for `type === 'frame'`, preserving the no-recursive-parenting invariant. - No new transports/RPC/DB/migrations — `parent_frame_id` already round-trips end-to-end through `serializeForServer`, `applySyncUpdate`, and `app.js::createObjectFromData`. - `webframe.js`'s recent #101 (sync), #102 (presentation), #103 (rotation), and #104 (delete cleanup) fixes are unaffected.
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#105
No description provided.