Skip to content

fix(widgets): deleting a duplicated widget also deleted the original - #425

Open
shakurt wants to merge 1 commit into
perf/widget-canvas-drag-performancefrom
fix/duplicate-widget-instance-id
Open

fix(widgets): deleting a duplicated widget also deleted the original#425
shakurt wants to merge 1 commit into
perf/widget-canvas-drag-performancefrom
fix/duplicate-widget-instance-id

Conversation

@shakurt

@shakurt shakurt commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #424. Review that one first; this PR's own diff is only the four files below. Once #424 merges, GitHub retargets this base to feat/free-widget-canvas automatically.

Problem

Duplicating a widget and then deleting the duplicate sometimes removed the original as well.

Root cause

Three bugs chained together.

  1. resolveLayoutChange('duplicate') ignored the newWidget argument and generated its own throwaway local id. The instance id issued by createUserWidgetApi was discarded, so the duplicate entered the layout with an id the server had never seen.

  2. Because that id did not look like a server id, the next sync tried to remap it. The mapping picked its target with synced.find(s => s.widgetKey === w.id), which returns the first record for that widget key. With two instances of the same widget that is the original's record, so the duplicate was assigned the original's instance id and the layout ended up holding two widgets with identical ids.

  3. remove filters by instance id, so removing either one dropped both, and deleteUserWidgetApi was called with the shared id.

validateLayout does reject duplicate instance ids, but the id map is applied after the commit inside the sync callback, so it never ran against the corrupted result.

Changes

  • duplicate now uses the supplied newWidget, keeping the server issued instance id, and refuses an id already present in the layout.
  • Add buildInstanceIdMap, which prefers the index aligned record, requires a matching widgetKey, and never hands out an id that another widget already holds or that an earlier widget claimed in the same pass. Both sync paths use it.
  • Add dedupeInstanceIds to sanitizeLayout so layouts already corrupted by this bug are repaired on load: the first widget keeps the id, any later collision is reassigned a fresh one. Without this, affected users stay broken after upgrading.
  • removeWidget only calls deleteUserWidgetApi for real server ids instead of any non-empty string.

Testing

npm test (43 tests), npm run compile, biome check and npm run build all pass. New tests cover the id mapping guards, the dedupe repair, and a duplicate-then-remove case asserting the original survives.

Manual check: duplicate a widget, reload so a sync round trip happens, delete the duplicate, confirm the original stays.

…s instance id

Deleting a duplicated widget sometimes removed the original one too.

Three bugs chained together to cause it:

1. resolveLayoutChange('duplicate') ignored the newWidget argument and
   generated its own throwaway local id, so the instance id issued by
   createUserWidgetApi was discarded and the duplicate entered the layout
   with an id the server had never seen.

2. Because that id did not look like a server id, the next sync tried to
   remap it. The mapping picked its target with
   synced.find(s => s.widgetKey === w.id), which returns the first record
   for that widget key. With two instances of the same widget that is the
   original's record, so the duplicate was assigned the original's
   instance id and the layout ended up holding two widgets with identical
   ids.

3. remove filters by instance id, so removing either one dropped both, and
   deleteUserWidgetApi was called with the shared id.

Fixes:
- duplicate now uses the supplied newWidget, keeping the server issued
  instance id, and refuses an id that is already present in the layout.
- Add buildInstanceIdMap, which prefers the index aligned record, requires
  a matching widgetKey, and never hands out an id that another widget
  already holds or that an earlier widget claimed in the same pass. Both
  sync paths use it.
- Add dedupeInstanceIds to sanitizeLayout so layouts already corrupted by
  this bug are repaired on load: the first widget keeps the id and any
  later collision is reassigned a fresh one.
- removeWidget only calls deleteUserWidgetApi for real server ids instead
  of any non-empty string.

Covered by tests for the id mapping guards, the dedupe repair, and a
duplicate-then-remove case asserting the original survives.
@shakurt shakurt changed the title Fix/duplicate widget instance id fix(widgets): deleting a duplicated widget also deleted the original Aug 29, 2026
@shakurt
shakurt changed the base branch from feat/free-widget-canvas to perf/widget-canvas-drag-performance August 29, 2026 18:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant