Connector delete is not undoable and corrupts the undo stack #193
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#193
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Deleting a connector on the whiteboard cannot be undone, and the undo stack is left in a corrupt state.
Details
createConnectorincrates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.jspushes a history action when a connector is created:But
deleteConnectorin the same file destroys the arrow, removes it from theconnectorsmap, persists the deletion to the server, and broadcasts over the WebSocket — without pushing any history action.Consequences:
deleteaction on the stack to invert).createaction resurrects a broken connector. The originalcreateaction from when the connector was added is still on the undo stack. Undoing back past it tries to recreate a connector whosefrom/toendpoints may no longer correspond to anything valid, producing either a no-op or a dangling connector.Expected
_fromSync) must not push history (mirrors the existing guard on the persistence/broadcast path).Affected
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.js—deleteConnector(and the history-replay path that handles connector actions)Implementation Spec for Issue #193
Objective
Make connector deletion fully undoable/redoable, and stop the existing connector
createhistory action from resurrecting a broken connector. Replace the connector-incompatible{ type: 'create', id }push with a connector-aware{ type: 'custom' }pair (mirroring the comments.js pattern), and add a{ type: 'custom' }history entry on delete that captures enough state to fully recreate the connector with its original endpoints and style.Requirements
id,fromId,toId,lineStyle,stroke,strokeWidth.type: 'create'._fromSync/_fromServer) must NOT push history.cycleLineStyle(which does destroy +createConnectorwith_fromServer: true) must NOT generate any history entries.isNaN(numId) && numId > 0guards).connector.create/connector.delete/connector.updateRPCs already exist and are exercised by the current code. No Rust server change is required.Files to Modify/Create
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.js— replace the brokencreatehistory push increateConnector; add acustomhistory push indeleteConnector; suppress history on sync/server/cycle paths.(No other files require changes.
history.jsalready supportstype: 'custom'withundo/redoclosures.app.js renderConnectorsalready passes_fromServer: true, so it inherits the suppression automatically.)Implementation Plan
Step 1: Add shared internal connector recreate/delete helpers used by history closures
Files:
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.jsrecreateConnectorFromState(state): resolvesfromGroup/toGroupviaWhiteboardCanvas.getObjectLayer().findOne('#' + state.fromId / state.toId)(same pattern asloadFromSync), and if both exist callscreateConnector(fromGroup, toGroup, { id: state.id, lineStyle: state.lineStyle, stroke: state.stroke, strokeWidth: state.strokeWidth })WITHOUT_fromServer(so an undo-of-delete re-persists + re-broadcasts). Returns the effective id (after any temp→server remap). No-op gracefully if either endpoint group is missing.removeConnectorNoHistory(id)that performs the currentdeleteConnectorteardown + server delete + WS broadcast as the canonical implementation (avoids recursion when redo deletes again).Dependencies: none
Step 2: Replace the broken
createhistory push increateConnectorFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.jsWhiteboardHistory.push({ type: 'create', id: id });.history.jsonly handlestype: 'create'for Konva objects inWhiteboardObjects; connectors are Arrows in the module-localconnectorsmap, so this action is broken and is the "stale create resurrects broken connector" bug.custompush that fires only for genuine user-created connectors: skip whenopts._fromServeris true (coversloadFromSync,renderConnectors, andcycleLineStyle's internal recreate) or whenWhiteboardHistory.isEnabled()is false (replay).state = { id: <effective id>, fromId: fromGroup.id(), toId: toGroup.id(), lineStyle, stroke, strokeWidth }. Use a mutablestate.idupdated in theconnector.create.then(next to the existingremapConnectorIdcall) so closures keep targeting the live connector after temp→server id remap.{ type: 'custom', undo: () => removeConnectorNoHistory(state.id), redo: () => { state.id = recreateConnectorFromState(state); } }.Dependencies: Step 1
Step 3: Add an undoable
customhistory push indeleteConnectorFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.jsstate = { id, fromId: c.fromId, toId: c.toId, lineStyle: c.lineStyle, stroke: c.baseStroke || c.arrow.stroke(), strokeWidth: c.baseStrokeWidth || c.arrow.strokeWidth() }.removeConnectorNoHistory(id)(Step 1) and call it here.!o._fromSync && WhiteboardHistory.isEnabled(). Push:{ type: 'custom', undo: () => { state.id = recreateConnectorFromState(state); }, redo: () => removeConnectorNoHistory(state.id) }.app.jssync delete passes{ _fromSync: true }→ no history (correct).deleteSelectedConnectorand context menu call with no opts → history pushed (correct).Dependencies: Step 1
Step 4: Verify
cycleLineStyleand remote style updates produce no historyFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.jscycleLineStyledestroys directly (NOT viadeleteConnector) and recreates viacreateConnector(..., { _fromServer: true })so both new guards suppress history. Correctness check; no code change expected.Dependencies: Step 2, Step 3
Acceptance Criteria
connector.create) and re-broadcast so a second client sees it return.connector.deleted).{ type: 'create', id }connector entries on the undo stack; deep undo never resurrects a broken connector._fromSync: true) push NO history.cycleLineStyle/ remote style updates generate NO history entries.connector.*RPC contract unchanged.Notes
deleteConnectorpushes no history, and thecreatepush usestype: 'create'whichhistory.jsonly knows how to replay for Konva objects inWhiteboardObjects. Connectors are Arrows in the module-localconnectorsmap, so thecreateaction both fails and resurrects a broken connector. The fix replaces, not augments, thecreatepush.type: 'custom'model exactly likecomments.js: capturedstate+undo/redoclosures, withstate.idreassigned after server remap.conn_-1) becomes a numeric server id viaremapConnectorIdafterconnector.createresolves. Closures must resolve the current id at call time (read/update mutablestate.idin the.then).WhiteboardHistory.remapIddoes NOT reach intocustomclosures — manage the id yourself.removeConnectorNoHistory/recreateConnectorFromState, never back through publicdeleteConnector/createConnectorhistory-pushing paths.recreateConnectorFromStatecallscreateConnectorWITHOUT_fromServer.cycleLineStyle's internal recreate intentionally keeps_fromServer: true._fromSync(delete path) vs_fromServer(create path) are distinct flags on distinct functions — guard each on its own function's flag.fromGroup/toGroupno longer exist when undoing a delete (connected object also deleted),recreateConnectorFromStateno-ops gracefully rather than throwing.Test Results
cargo test --workspace --lib: pass — all 4 lib crates compiled and ran cleanly (0 tests defined; server crate is binary-only with no lib target, matching CI)
node --check connectors.js: ok
Note: issue #193 is a JS-only change (connectors.js). There is no JS unit harness in this repo; the Rust suite is run to confirm no regression, and the connector undo/redo behavior is verified manually in-browser.
Implementation Summary
Fixed in
crates/hero_whiteboard_admin/static/web/js/whiteboard/connectors.js(JS-only; no server/RPC contract change).Changes
WhiteboardHistory.push({ type: 'create', id })increateConnector.history.jsonly replaystype: 'create'for Konva objects in the object registry; connectors are Arrows in a module-local map, so that action both failed to undo and resurrected a broken connector on deep undo.createConnectornow pushes a connector-awaretype: 'custom'undo/redo pair (the establishedcomments.jspattern), guarded so it does not fire when loading from server (_fromServer) or while history is replaying (WhiteboardHistory.isEnabled() === false).deleteConnectornow captures connector state (id, fromId, toId, lineStyle, stroke, strokeWidth) and pushes atype: 'custom'action. Undo restores the connector with its original endpoints and style, re-persists it viaconnector.create, and re-broadcasts over WebSocket so other clients stay consistent. Redo deletes it again.recreateConnectorFromState(state)(graceful no-op if either endpoint group is gone) andremoveConnectorNoHistory(id, skipPersist)(canonical teardown; server delete + WS broadcast gated so sync-originated deletes do not re-issue them — preserving prior behavior exactly)._historyStatereference) so a subsequent redo targets the live connector instead of a stale id.cycleLineStyleis unaffected: it destroys directly (not viadeleteConnector) and recreates with_fromServer: true, so both guards suppress history. No spurious entries from line-style cycling or remote style updates.Behavior after fix
createentries remain on the undo stack; deep undo never resurrects a broken connector.Tests
cargo test --workspace --lib: no Rust regression (workspace compiles and runs clean; this is a JS-only change with no JS unit harness in the repo).node --check connectors.js: ok.