@designcrowd/library.brand-page 5.18.1-undo-33 → 5.18.1-undo-34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-986OpXvU.js → CreatedOnBrandCrowd-de-de-D-2WhY-F.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CTZuZ366.js → CreatedOnBrandCrowd-en-us-B7ldKpJK.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CHrTgX9-.js → CreatedOnBrandCrowd-es-es-BatQY8Tt.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-xJAvxQ-x.js → CreatedOnBrandCrowd-fr-ca-CFivOJkN.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-xJAvxQ-x.js → CreatedOnBrandCrowd-fr-fr-CFivOJkN.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-lpRIGIcs.js → CreatedOnBrandCrowd-pt-br-wHkXfYLP.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-lpRIGIcs.js → CreatedOnBrandCrowd-pt-pt-wHkXfYLP.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CZuim3ee.js → CreatedOnDcom-de-de--HwniV-Y.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DG4I5f4s.js → CreatedOnDcom-en-us-Bqm5NnRX.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-Hpieh1wX.js → CreatedOnDcom-es-es-COGQIuEn.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DWr5P-df.js → CreatedOnDcom-fr-ca-eVDHO7MX.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-CHfOyS6V.js → CreatedOnDcom-fr-fr-BhGKbdoG.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-bQwCCDDI.js → CreatedOnDcom-pt-br-5kDZKPZc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-bo-i_h0N.js → CreatedOnDcom-pt-pt-CnTcVxct.js} +1 -1
  15. package/lib/src/brand-page-maker/stores/history/history-store.js +47 -18
  16. package/lib/src/index.mjs +1 -1
  17. package/lib/src/{lib-DNHBgZyy.js → lib-B5jbN-ro.js} +3878 -3875
  18. package/package.json +1 -1
  19. package/src/brand-page-maker/stores/history/history-store.test.ts +32 -0
  20. package/src/brand-page-maker/stores/history/history-store.ts +48 -18
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "5.18.1-undo-33",
4
+ "version": "5.18.1-undo-34",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -357,6 +357,38 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
357
357
  expect(store.currentIndex).toBe(2);
358
358
  });
359
359
 
360
+ it('drops a stale merge arm if no mutation follows in the same tick', async () => {
361
+ // Repro for the "first pre-purchase logo edit not captured" bug:
362
+ // a hydration site (e.g. ContentSettings.mounted on tab switch)
363
+ // armed the flag, but the user navigated away before the flagged
364
+ // write happened. A LATER unrelated mutation must NOT inherit
365
+ // the stale arm and merge — it must commit normally.
366
+ const store = useHistoryStore();
367
+
368
+ await store.captureBaseline();
369
+ await elapseSettleWindow();
370
+
371
+ // Land a real entry first.
372
+ liveData.v = 100;
373
+ await flushWatcher();
374
+
375
+ expect(store.entries.length).toBe(2);
376
+
377
+ // Hydration site arms the flag, but no mutation follows in the
378
+ // same tick — caller navigated away.
379
+ store.mergeNextMutation();
380
+ // Advance past the macrotask cleanup, no mutation in between.
381
+ await vi.advanceTimersByTimeAsync(10);
382
+
383
+ // Some time later, a real edit happens.
384
+ liveData.unrelated = 'edit';
385
+ await flushWatcher();
386
+
387
+ // Must push, not merge.
388
+ expect(store.entries.length).toBe(3);
389
+ expect(store.currentIndex).toBe(2);
390
+ });
391
+
360
392
  it('clears any armed merge flag during snapshot apply (undo/redo)', async () => {
361
393
  const store = useHistoryStore();
362
394
 
@@ -153,13 +153,19 @@ export const useHistoryStore = defineStore('history', () => {
153
153
  }
154
154
  }
155
155
 
156
- // One-shot flag set by hydration write sites (TipTap mount, lazy
157
- // paymentConfig fill, stale-page-ref clear, ContentSettings RTE
158
- // rewrap) before their imperative write. The watcher's next flush
159
- // consumes the flag and merges the snapshot into the current head
160
- // instead of pushing — preventing phantom undo entries on first
161
- // sidebar open / section click. Cleared on apply and reset.
162
- const mergeNextFlush = ref(false);
156
+ // Two-phase merge flag. Hydration write sites call `mergeNextMutation()`
157
+ // immediately before their imperative write, which sets `mergeArmed`.
158
+ // When the watcher's `scheduleAutoCapture` arms a new idle timer, it
159
+ // transfers `mergeArmed` into `pendingTimerIsMerge`. The flush then
160
+ // consumes `pendingTimerIsMerge` and merges into the current head
161
+ // instead of pushing.
162
+ //
163
+ // The microtask cleanup on `mergeArmed` prevents stale flags surviving
164
+ // when the user navigates away (e.g. sidebar tab switch) before the
165
+ // flagged write actually happens — a previously-set arm is dropped
166
+ // synchronously after the current tick if no watcher fired.
167
+ const mergeArmed = ref(false);
168
+ let pendingTimerIsMerge = false;
163
169
 
164
170
  // Returns 'noop' when no-change vs current head, 'oversized' when over
165
171
  // MAX_ENTRY_BYTES, else 'committed'. Truncates the redo tail, caps at
@@ -286,11 +292,12 @@ export const useHistoryStore = defineStore('history', () => {
286
292
  currentIndex.value === 0 &&
287
293
  Date.now() - baselineCapturedAt < BASELINE_SETTLE_MS;
288
294
 
289
- // Merge: a hydration write site flagged this flush — replace the
290
- // current head in place instead of pushing. Consumed once.
291
- const shouldMerge = mergeNextFlush.value;
295
+ // Merge: the timer that fired this flush was armed during a
296
+ // hydration burst — replace the current head in place instead of
297
+ // pushing. Consumed once.
298
+ const shouldMerge = pendingTimerIsMerge;
292
299
 
293
- mergeNextFlush.value = false;
300
+ pendingTimerIsMerge = false;
294
301
 
295
302
  if (inSettleWindow) {
296
303
  entries.value = [
@@ -334,12 +341,20 @@ export const useHistoryStore = defineStore('history', () => {
334
341
 
335
342
  // Monotonic — first mutation arms; subsequent ones inside the window
336
343
  // don't reset. Gated on isApplyingSnapshot so undo/redo don't leave
337
- // a stale timer ticking.
344
+ // a stale timer ticking. If a hydration site armed `mergeArmed`
345
+ // immediately before the mutation that triggered this run, transfer
346
+ // the intent into `pendingTimerIsMerge` so the resulting flush
347
+ // merges instead of commits.
338
348
  const scheduleAutoCapture = (): void => {
339
349
  if (isApplyingSnapshot.value) return;
340
350
 
341
351
  if (idleTimerId !== null) return;
342
352
 
353
+ if (mergeArmed.value) {
354
+ pendingTimerIsMerge = true;
355
+ mergeArmed.value = false;
356
+ }
357
+
343
358
  idleTimerId = setTimeout(flush, CAPTURE_DEBOUNCE_MS);
344
359
  maxWaitTimerId = setTimeout(flush, CAPTURE_MAX_WAIT_MS);
345
360
  };
@@ -491,9 +506,10 @@ export const useHistoryStore = defineStore('history', () => {
491
506
  const targetEntry = entries.value[targetIndex];
492
507
 
493
508
  isApplyingSnapshot.value = true;
494
- // Clear any unconsumed merge flag — a stale flag would fold the
509
+ // Clear any unconsumed merge intent — a stale flag would fold the
495
510
  // post-apply tick into the restored entry and erase the redo.
496
- mergeNextFlush.value = false;
511
+ mergeArmed.value = false;
512
+ pendingTimerIsMerge = false;
497
513
  cancelPendingAutoCapture();
498
514
 
499
515
  try {
@@ -568,7 +584,8 @@ export const useHistoryStore = defineStore('history', () => {
568
584
  if (!baselineCaptured.value) return;
569
585
 
570
586
  cancelPendingAutoCapture();
571
- mergeNextFlush.value = false;
587
+ mergeArmed.value = false;
588
+ pendingTimerIsMerge = false;
572
589
 
573
590
  entries.value = [];
574
591
  currentIndex.value = -1;
@@ -579,10 +596,23 @@ export const useHistoryStore = defineStore('history', () => {
579
596
  }
580
597
 
581
598
  // Hydration write sites call this immediately before their imperative
582
- // mutation; the next watcher flush merges into the current head
583
- // instead of pushing. Idempotent and one-shot.
599
+ // mutation. The arm is auto-cleared on the next Vue tick — `nextTick`
600
+ // runs AFTER all queued reactive effects (including the watcher), so
601
+ // if the hydration write triggered `scheduleAutoCapture`, the arm has
602
+ // already been transferred into `pendingTimerIsMerge` before this
603
+ // cleanup fires. If no mutation followed (e.g. the user navigated
604
+ // away before the flagged write), the cleanup drops the stale arm so
605
+ // it can't be consumed by an unrelated future mutation.
584
606
  function mergeNextMutation(): void {
585
- mergeNextFlush.value = true;
607
+ mergeArmed.value = true;
608
+ // setTimeout(0) is a macrotask — runs AFTER the synchronous write
609
+ // and AFTER Vue's microtask flush of the watcher's reactive effect.
610
+ // If `scheduleAutoCapture` fired in between (the imperative write
611
+ // triggered the watcher), the arm has already been transferred into
612
+ // `pendingTimerIsMerge` before this cleanup runs.
613
+ setTimeout(() => {
614
+ mergeArmed.value = false;
615
+ }, 0);
586
616
  }
587
617
 
588
618
  // --- Instrumentation (NFR-04a) ---