Engineering Journal
Schema Editor
Schema Editor

Postmortem: Undo Worked for Everything Except Property Panel Edits

2026-06-04

TLDR

Our snapshot undo system operated reliably across canvas draw, move, and delete interactions, but silently failed for property panel edits (width, height, coordinates). The root cause: the before-state for property edits was captured inside change handlers, after input fields had updated. This produced identical before and after snapshots (before === after), making Ctrl+Z a no-op. Moving before-state capture to focus resolved the issue.
Incident StageEvent Listening HookState Snapshot OutcomeResulting User Experience
Initial Deploymentchange / blurbefore equals afterNo-op (Ctrl+Z does nothing)
Postmortem Resolutionfocus (Entry Point)before holds initial valueReverts property changes properly

Problem statement: the asymmetric undo failure

Canvas interactions (dragging components, placing symbols, deleting selection) worked correctly with Ctrl+Z.

However, when users modified an element's X position in the property inspector from 100 to 250 and hit Ctrl+Z, the element stayed at 250.

Pressing Ctrl+Z again skipped the position edit completely and reverted the prior canvas move.


Technical failure mode: identical state pair capture

Inspectors were binding snapshot capture directly to input change events:

// DEPRECATED: Capturing before-state inside 'change' handler
$('#prop-x').on('change', function() {
  const before = captureFullState(); // BUG: Reads DOM input value AFTER modification!
  applyXPosition(this.value);
  const after = captureFullState();

pushHistory('X Position', before, after); // Identical state pair! });

Because change fires after user input finishes, captureFullState() read the modified DOM value for both snapshots.

The undo stack received { before: X=250, after: X=250 }. Restoring the entry simply re-applied position 250.


The fix & architecture: separating interaction start from commit

We decoupled interaction initiation (focus) from transaction commit (change):

let propBeforeState = null;

// 1. Capture before-state BEFORE user types $(document).on('focus', '#prop-x, #prop-y, #prop-w, #prop-h, #prop-rotation', () => { propBeforeState = captureFullState(); });

// 2. Commit transaction on change using the saved before-state $(document).on('change', '#prop-x, #prop-y, #prop-w, #prop-h, #prop-rotation', function() { const before = propBeforeState || captureFullState(); propBeforeState = null;

applyPropertyFromInput(this); const after = captureFullState();

pushHistory('Property Edit', before, after); });

Rule of thumb: Always separate the initiation of a user edit (focus, pointerdown) from its commit (change, pointerup). Capture before-state at initiation to guarantee valid undo history.
Read this post in the full Engineering Journal →