@designcrowd/library.brand-page 6.0.6-undo-2 → 6.0.6-undo-4

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 (57) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-BSFTp6yl.js → CreatedOnBrandCrowd-de-de-BqD7lX6T.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-rPQrOY1L.js → CreatedOnBrandCrowd-en-us-BXFZ-Vv_.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-awd8-QFa.js → CreatedOnBrandCrowd-es-es-qTAVOIsd.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-ca-LVZ7sAKH.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js → CreatedOnBrandCrowd-fr-fr-LVZ7sAKH.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js → CreatedOnBrandCrowd-pt-br-TSeJN2wb.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BXYfTLlD.js → CreatedOnBrandCrowd-pt-pt-TSeJN2wb.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-kX6u1lNK.js → CreatedOnDcom-de-de-BLLcDw_4.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Bkq5sgFs.js → CreatedOnDcom-en-us-BVdWxcIs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-zCAC9ujj.js → CreatedOnDcom-es-es-BZRzR4GZ.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BYz6Wzg5.js → CreatedOnDcom-fr-ca-B97f7ekB.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-CN-wAaSH.js → CreatedOnDcom-fr-fr-CNWS12Rb.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-BrK4Ngg-.js → CreatedOnDcom-pt-br-CsASsHeV.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-AfI_g7xJ.js → CreatedOnDcom-pt-pt-CsYqjPls.js} +1 -1
  15. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +5 -5
  16. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +6 -6
  17. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +1 -1
  18. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  19. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +23 -13
  20. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +1 -1
  21. package/lib/src/index.mjs +1 -1
  22. package/lib/src/{lib-BYFWAvIq.js → lib-BNqCRgBN.js} +15895 -15862
  23. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +5 -0
  24. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +29 -0
  25. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +6 -6
  26. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +5 -5
  27. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +5 -5
  28. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +5 -5
  29. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +5 -0
  30. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +5 -5
  31. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -67
  32. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +4 -6
  33. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +5 -0
  34. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +5 -0
  35. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -0
  36. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +19 -0
  37. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +5 -0
  38. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +5 -0
  39. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +5 -0
  40. package/lib/src/shared/content-settings/models.d.ts +11 -0
  41. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +5 -5
  42. package/lib/src/shared/content-settings/store/content-settings-store.js +39 -5
  43. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +5 -5
  44. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +5 -5
  45. package/package.json +1 -1
  46. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +24 -15
  47. package/src/shared/content-settings/components/GenericTextInput.mixin.test.ts +119 -0
  48. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +34 -0
  49. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +10 -5
  50. package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +4 -7
  51. package/src/shared/content-settings/components/text-field/TextField.vue +2 -1
  52. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +149 -0
  53. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +26 -0
  54. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +2 -1
  55. package/src/shared/content-settings/models.ts +11 -0
  56. package/src/shared/content-settings/store/content-settings-store.test.ts +137 -0
  57. package/src/shared/content-settings/store/content-settings-store.ts +45 -0
@@ -4,6 +4,7 @@ import { RESERVED_FIELD_NAMES } from '../constants';
4
4
  import { UI_EVENT_SOURCES } from '../../models';
5
5
  import { CONTENT_SETTINGS_DISPLAY_MODES, ITEM_CATEGORIES, ITEM_CATEGORY_NAMES, } from '@brand-page-maker/models';
6
6
  import { useUiStore, useBrandPageMakerUserStore, useBrandPageMakerStore, useVideoStore, } from '@brand-page-maker/stores';
7
+ import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
7
8
  import { useUiSideBarStore } from '../../side-bar/store';
8
9
  import { createDefaultContentItemGroupState, createDefaultContentItemState, } from '../../navigation-style-settings/store';
9
10
  import { adminValidatorsDict, dummyValidator, validatorDict, } from '@brand-page-maker/validators';
@@ -210,6 +211,15 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
210
211
  if (!item) {
211
212
  return;
212
213
  }
214
+ // Hydration write-backs (RTE canonical HTML on mount, lazy config
215
+ // hydration, stale-id clears) fold into the current undo head instead
216
+ // of pushing an entry. Arming here — same tick as the write — is what
217
+ // makes the merge reliable: the flag travelled with the payload
218
+ // through the debounced field chain, so emit-time arming would lapse
219
+ // before the write lands (BP-5810).
220
+ if (payload.isHydrationWrite) {
221
+ useHistoryStore().mergeNextMutation();
222
+ }
213
223
  item.settings.content[payload.name] = payload.value;
214
224
  this.validateItem({
215
225
  id: payload.id,
@@ -217,29 +227,45 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
217
227
  });
218
228
  this.onContentChanged();
219
229
  },
220
- updateMultipleItemValues({ id, name, value, }) {
230
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }) {
221
231
  const item = this.items.find((i) => i.id === id);
222
232
  if (!item) {
223
233
  return;
224
234
  }
235
+ // Same hydration-write contract as updateItemValue.
236
+ if (isHydrationWrite) {
237
+ useHistoryStore().mergeNextMutation();
238
+ }
225
239
  item.settings.content = vueDeepMerge(item.settings.content, value);
226
240
  this.validateItem({ id, name });
227
241
  this.onContentChanged();
228
242
  },
229
- updateTextStyleType({ id, name, value, }) {
243
+ updateTextStyleType({ id, name, value, isHydrationWrite, }) {
230
244
  const item = this.items.find((i) => i.id === id);
231
245
  if (!item) {
232
246
  return;
233
247
  }
248
+ // Same hydration-write contract as updateItemValue.
249
+ if (isHydrationWrite) {
250
+ useHistoryStore().mergeNextMutation();
251
+ }
234
252
  item.settings.content[`${name}Style`].textStyleType = vueDeepMerge(item.settings.content, value);
235
253
  this.validateItem({ id, name });
236
254
  this.onContentChanged();
237
255
  },
238
- updateTextStyles({ id, name, value, }) {
256
+ updateTextStyles({ id, name, value, isHydrationWrite, }) {
239
257
  const item = this.items.find((i) => i.id === id);
240
258
  if (!item) {
241
259
  return;
242
260
  }
261
+ // Same hydration-write contract as updateItemValue. The RTE's
262
+ // mount-time style sync (setInitialStyleSettings) lands here ~100ms
263
+ // BEFORE its canonical text write-back — if this write opens a plain
264
+ // capture burst, the flagged text write joins it and both commit as a
265
+ // phantom entry (BP-5810's second act).
266
+ if (isHydrationWrite) {
267
+ useHistoryStore().mergeNextMutation();
268
+ }
243
269
  item.settings.content[`${name}Style`] = vueDeepMerge(item.settings.content[`${name}Style`], value);
244
270
  this.validateItem({ id, name });
245
271
  this.onContentChanged();
@@ -251,21 +277,29 @@ export const useUiContentSettingsStore = defineStore('uiContentSettings', {
251
277
  }
252
278
  return item.settings.content[`${name}Style`];
253
279
  },
254
- updateTextItemTextStyleType({ id, name, value, index, }) {
280
+ updateTextItemTextStyleType({ id, name, value, index, isHydrationWrite, }) {
255
281
  const item = this.items.find((i) => i.id === id);
256
282
  if (!item) {
257
283
  return;
258
284
  }
285
+ // Same hydration-write contract as updateItemValue.
286
+ if (isHydrationWrite) {
287
+ useHistoryStore().mergeNextMutation();
288
+ }
259
289
  item.settings.content.textItems[index].textStyle.textStyleType =
260
290
  vueDeepMerge(item.settings.content.textItems[index].textStyle.textStyleType, value);
261
291
  this.validateItem({ id, name });
262
292
  this.onContentChanged();
263
293
  },
264
- updateTextItemTextStyleTypeWithStyles({ id, name, value, index, }) {
294
+ updateTextItemTextStyleTypeWithStyles({ id, name, value, index, isHydrationWrite, }) {
265
295
  const item = this.items.find((i) => i.id === id);
266
296
  if (!item) {
267
297
  return;
268
298
  }
299
+ // Same hydration-write contract as updateItemValue.
300
+ if (isHydrationWrite) {
301
+ useHistoryStore().mergeNextMutation();
302
+ }
269
303
  item.settings.content.textItems[index].textStyle = vueDeepMerge(item.settings.content.textItems[index].textStyle, value);
270
304
  this.validateItem({ id, name });
271
305
  this.onContentChanged();
@@ -4411,15 +4411,15 @@ declare const _default: import("vue").DefineComponent<{}, {
4411
4411
  pushMode(item: import("../../../content-settings/store").ContentSettingsModeStackItem): void;
4412
4412
  resetMode(): void;
4413
4413
  updateItemValue(payload: import("../../../content-settings/models").UpdateItemValuePayload<unknown>): void;
4414
- updateMultipleItemValues({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4415
- updateTextStyleType({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4416
- updateTextStyles({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4414
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4415
+ updateTextStyleType({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4416
+ updateTextStyles({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4417
4417
  getTextStyles({ id, name }: {
4418
4418
  id: string;
4419
4419
  name: string;
4420
4420
  }): unknown;
4421
- updateTextItemTextStyleType({ id, name, value, index, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4422
- updateTextItemTextStyleTypeWithStyles({ id, name, value, index, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4421
+ updateTextItemTextStyleType({ id, name, value, index, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4422
+ updateTextItemTextStyleTypeWithStyles({ id, name, value, index, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4423
4423
  _setActiveCategoryTab({ activeCategoryTab, }: {
4424
4424
  activeCategoryTab: import("../../../../brand-page-maker/models").ItemCategory;
4425
4425
  }): void;
@@ -3787,15 +3787,15 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
3787
3787
  pushMode(item: import("../../../content-settings/store").ContentSettingsModeStackItem): void;
3788
3788
  resetMode(): void;
3789
3789
  updateItemValue(payload: UpdateItemValuePayload<unknown>): void;
3790
- updateMultipleItemValues({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3791
- updateTextStyleType({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3792
- updateTextStyles({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3790
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3791
+ updateTextStyleType({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3792
+ updateTextStyles({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3793
3793
  getTextStyles({ id, name }: {
3794
3794
  id: string;
3795
3795
  name: string;
3796
3796
  }): unknown;
3797
- updateTextItemTextStyleType({ id, name, value, index, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3798
- updateTextItemTextStyleTypeWithStyles({ id, name, value, index, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3797
+ updateTextItemTextStyleType({ id, name, value, index, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3798
+ updateTextItemTextStyleTypeWithStyles({ id, name, value, index, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3799
3799
  _setActiveCategoryTab({ activeCategoryTab, }: {
3800
3800
  activeCategoryTab: import("../../../../brand-page-maker/models").ItemCategory;
3801
3801
  }): void;
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": "6.0.6-undo-2",
4
+ "version": "6.0.6-undo-4",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -211,10 +211,6 @@ export const useRichTextEditor = (
211
211
  ): UseRichTextEditorReturn => {
212
212
  const historyStore = useHistoryStore();
213
213
 
214
- // TipTap rehydrates and writes back canonical HTML on mount — fold
215
- // that into the current head instead of pushing a phantom entry.
216
- historyStore.mergeNextMutation();
217
-
218
214
  const editor = new Editor({
219
215
  content: model.value ?? '',
220
216
  editable: !props.readOnly,
@@ -492,6 +488,14 @@ export const useRichTextEditor = (
492
488
  );
493
489
 
494
490
  editor.on('transaction', ({ transaction }) => {
491
+ // Style detection only makes sense for user styling — an unfocused
492
+ // editor's transactions are mount rehydration / programmatic setContent
493
+ // churn, and clearing textStyleType from those would push a phantom
494
+ // style write into the model (BP-5810 family).
495
+ if (!editor.isFocused) {
496
+ return;
497
+ }
498
+
495
499
  if (isStyleChanged(transaction) && props.styles.textStyleType) {
496
500
  onSelectTextStyleType({
497
501
  label: 'None',
@@ -505,22 +509,22 @@ export const useRichTextEditor = (
505
509
 
506
510
  // An update fired while the editor is unfocused is TipTap rewriting the
507
511
  // model back into its canonical HTML during mount rehydration or a
508
- // programmatic setContent — not a user edit. Fold it into the current
509
- // head so merely selecting a section doesn't push a phantom undo entry
510
- // (BP-5810). The construction-time arm above lapses before this async
511
- // write-back fires, so re-arm here. Genuine edits happen while focused and
512
- // still push a new entry.
512
+ // programmatic setContent — not a user edit. Tag the emit so the write
513
+ // folds into the current undo head instead of pushing a phantom entry
514
+ // (BP-5810). The flag travels WITH the value through the debounced
515
+ // field → store chain and arms the history merge at the moment of the
516
+ // store write — arming here at emit time would lapse before the
517
+ // debounce fires. Genuine edits happen while focused and commit
518
+ // normally.
513
519
  //
514
520
  // Known limitation: a programmatic toolbar command run on an editor the
515
521
  // user hasn't focused yet would also merge (losing that undo step). Today
516
522
  // the RTE only mounts inside an already-open settings drawer and the user
517
523
  // focuses it before formatting, so this doesn't occur — revisit if a
518
524
  // consumer ever drives toolbar actions on an unfocused editor.
519
- if (!editor.isFocused && !historyStore.isApplyingSnapshot) {
520
- historyStore.mergeNextMutation();
521
- }
522
-
523
- emit('update:modelValue', sanitizedData.sanitizedHtml);
525
+ emit('update:modelValue', sanitizedData.sanitizedHtml, {
526
+ isHydration: !editor.isFocused,
527
+ });
524
528
  });
525
529
 
526
530
  editor.on('selectionUpdate', () => {
@@ -621,7 +625,11 @@ export const useRichTextEditor = (
621
625
  );
622
626
 
623
627
  if (globalStyles) {
624
- // sync content settings style
628
+ // sync content settings style — a mount-time re-derivation, not a user
629
+ // edit. Flag it so the store folds it into the current undo head: this
630
+ // write lands ~100ms BEFORE the editor's canonical text write-back, and
631
+ // an unflagged write here would open a plain capture burst that
632
+ // swallows the flagged text write into a phantom entry (BP-5810).
625
633
  contentSettingStore.updateTextStyles({
626
634
  id: props.rawId,
627
635
  name: props.name,
@@ -635,6 +643,7 @@ export const useRichTextEditor = (
635
643
  lineHeight: globalStyles.lineHeight,
636
644
  letterSpacing: globalStyles.letterSpacing,
637
645
  },
646
+ isHydrationWrite: !editor.isFocused,
638
647
  });
639
648
 
640
649
  // sync toolbar editor styles
@@ -0,0 +1,119 @@
1
+ import { defineComponent } from 'vue';
2
+ import { mount } from '@vue/test-utils';
3
+ import GenericTextInputMixin from './GenericTextInput.mixin';
4
+
5
+ // BP-5810: the RTE attaches `{ isHydration: true }` meta to its canonical
6
+ // write-back emits. GenericTextInput must carry that through its 100ms
7
+ // debounce onto the outgoing payload as `isHydrationWrite` — and downgrade
8
+ // the burst as soon as a single non-hydration write lands in it.
9
+
10
+ const DEBOUNCE_WAIT_MS = 100;
11
+
12
+ interface EmittedPayload {
13
+ id: string;
14
+ name: string;
15
+ value: string | false;
16
+ isHydrationWrite?: boolean;
17
+ }
18
+
19
+ const TestHost = defineComponent({
20
+ mixins: [GenericTextInputMixin],
21
+ template: '<div />',
22
+ });
23
+
24
+ const mountHost = () =>
25
+ mount(TestHost, {
26
+ props: {
27
+ id: 'item-1',
28
+ name: 'title',
29
+ value: '<p>seed</p>',
30
+ showRichTextEditor: true,
31
+ },
32
+ });
33
+
34
+ const emittedPayload = (
35
+ wrapper: ReturnType<typeof mountHost>,
36
+ call: number,
37
+ ): EmittedPayload =>
38
+ wrapper.emitted('update:modelValue')![call][0] as EmittedPayload;
39
+
40
+ describe('GenericTextInput hydration burst flag', () => {
41
+ beforeEach(() => {
42
+ vi.useFakeTimers();
43
+ });
44
+
45
+ afterEach(() => {
46
+ vi.useRealTimers();
47
+ });
48
+
49
+ test('a hydration-only burst emits a payload flagged isHydrationWrite', async () => {
50
+ const wrapper = mountHost();
51
+
52
+ wrapper.vm.onRteModelValue('<p style="text-align: center;"></p>', {
53
+ isHydration: true,
54
+ });
55
+
56
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
57
+
58
+ const emitted = wrapper.emitted('update:modelValue');
59
+
60
+ expect(emitted).toHaveLength(1);
61
+ expect(emitted![0][0]).toEqual({
62
+ id: 'item-1',
63
+ name: 'title',
64
+ value: '<p style="text-align: center;"></p>',
65
+ isHydrationWrite: true,
66
+ });
67
+ });
68
+
69
+ test('coalesced hydration writes in one burst stay flagged', async () => {
70
+ const wrapper = mountHost();
71
+
72
+ wrapper.vm.onRteModelValue('<p>a</p>', { isHydration: true });
73
+ wrapper.vm.onRteModelValue('<p>b</p>', { isHydration: true });
74
+
75
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
76
+
77
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
78
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
79
+ expect(emittedPayload(wrapper, 0).value).toBe('<p>b</p>');
80
+ });
81
+
82
+ test('a user write in the same burst downgrades it to a normal edit', async () => {
83
+ const wrapper = mountHost();
84
+
85
+ wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
86
+ // Focused typing emits without meta.
87
+ wrapper.vm.onRteModelValue('<p>typed</p>');
88
+
89
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
90
+
91
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
92
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
93
+ });
94
+
95
+ test('a plain write (no meta) is never flagged', async () => {
96
+ const wrapper = mountHost();
97
+
98
+ wrapper.vm.text = '<p>typed</p>';
99
+
100
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
101
+
102
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(1);
103
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
104
+ });
105
+
106
+ test('the flag resets between bursts', async () => {
107
+ const wrapper = mountHost();
108
+
109
+ wrapper.vm.onRteModelValue('<p>canonical</p>', { isHydration: true });
110
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
111
+
112
+ wrapper.vm.text = '<p>typed later</p>';
113
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_WAIT_MS + 10);
114
+
115
+ expect(wrapper.emitted('update:modelValue')).toHaveLength(2);
116
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
117
+ expect(emittedPayload(wrapper, 1).isHydrationWrite).toBeUndefined();
118
+ });
119
+ });
@@ -108,6 +108,13 @@ export default defineComponent({
108
108
  isDirty: false,
109
109
  emitChangeDebounce: undefined as DebounceHandle | undefined,
110
110
  lengthError: false,
111
+ // Whether every write in the current debounce burst was an RTE
112
+ // hydration write-back (see UpdateItemValuePayload.isHydrationWrite).
113
+ // undefined = no burst pending; any user write downgrades it to false.
114
+ burstIsHydration: undefined as boolean | undefined,
115
+ // One-shot channel from onRteModelValue into the `text` setter for
116
+ // the write currently being applied.
117
+ pendingWriteIsHydration: false,
111
118
  };
112
119
  },
113
120
  computed: {
@@ -144,6 +151,15 @@ export default defineComponent({
144
151
  return unescapeContentText(this.value);
145
152
  },
146
153
  set(newVal: string | false): void {
154
+ // A burst is hydration only if EVERY write in it is hydration —
155
+ // one real keystroke downgrades the whole burst to a user edit
156
+ // (the canonicalisation then rides inside that legitimate entry).
157
+ const writeIsHydration = this.pendingWriteIsHydration;
158
+
159
+ this.pendingWriteIsHydration = false;
160
+ this.burstIsHydration =
161
+ this.burstIsHydration !== false && writeIsHydration;
162
+
147
163
  if (newVal === false) {
148
164
  this._value = false;
149
165
  } else {
@@ -179,12 +195,16 @@ export default defineComponent({
179
195
  this._value = false;
180
196
  }
181
197
 
198
+ // Toggling is a user action — downgrade any pending hydration burst
199
+ // so this emit commits as a normal entry.
200
+ this.burstIsHydration = false;
182
201
  this.emitChange();
183
202
  },
184
203
 
185
204
  showRichTextEditor(newVal: boolean) {
186
205
  if (!newVal && this._value && typeof this._value === 'string') {
187
206
  this._value = encodeContentText(this._value);
207
+ this.burstIsHydration = false;
188
208
  this.emitChange();
189
209
  }
190
210
  },
@@ -208,11 +228,25 @@ export default defineComponent({
208
228
  this.isDirty = false;
209
229
  },
210
230
 
231
+ // RTE branch handler: receives the hydration meta the RichTextEditor
232
+ // attaches to its update:modelValue emits (a plain v-model binding
233
+ // would drop the second argument), records it for the `text` setter,
234
+ // then applies the write.
235
+ onRteModelValue(newVal: string, meta?: { isHydration?: boolean }): void {
236
+ this.pendingWriteIsHydration = meta?.isHydration === true;
237
+ this.text = newVal;
238
+ },
239
+
211
240
  emitChange() {
241
+ const isHydrationWrite = this.burstIsHydration === true;
242
+
243
+ this.burstIsHydration = undefined;
244
+
212
245
  this.$emit('update:modelValue', {
213
246
  id: this.id,
214
247
  name: this.name,
215
248
  value: this._value,
249
+ ...(isHydrationWrite ? { isHydrationWrite: true } : {}),
216
250
  });
217
251
  },
218
252
  },
@@ -24,7 +24,6 @@ import { Button } from '@designcrowd/fe-shared-lib';
24
24
  import { ref, computed, onMounted } from 'vue';
25
25
  import { useUiPageSettingsStore } from '../../../page-settings/store';
26
26
  import { useBrandPageMakerUserStore } from '../../../../brand-page-maker/stores';
27
- import { useHistoryStore } from '../../../../brand-page-maker/stores/history/history-store';
28
27
  import { pageSettingTr } from '../../../../composables/useTranslate';
29
28
  import PageSelectorDropdown from '../../../page-navigation/PageSelectorDropdown.vue';
30
29
  import { usePageNavigationStore } from '../../../page-navigation/store/page-navigation-store';
@@ -44,14 +43,18 @@ const props = withDefaults(defineProps<Props>(), {
44
43
  const emit = defineEmits<{
45
44
  (
46
45
  e: 'update:modelValue',
47
- payload: { id: string; name: string; value: string },
46
+ payload: {
47
+ id: string;
48
+ name: string;
49
+ value: string;
50
+ isHydrationWrite?: boolean;
51
+ },
48
52
  ): void;
49
53
  }>();
50
54
 
51
55
  const pageNavigationStore = usePageNavigationStore();
52
56
  const pageSettingsStore = useUiPageSettingsStore();
53
57
  const userStore = useBrandPageMakerUserStore();
54
- const historyStore = useHistoryStore();
55
58
 
56
59
  const selectedPage = ref('');
57
60
 
@@ -111,12 +114,14 @@ onMounted(() => {
111
114
  if (routeByPageId) {
112
115
  selectedPage.value = routeByPageId.displayName;
113
116
  } else {
114
- // Stale page-id clear at mount — fold into current head.
115
- historyStore.mergeNextMutation();
117
+ // Stale page-id clear at mount — a hydration write, not a user edit.
118
+ // The flag makes the content-settings store fold it into the current
119
+ // undo head at write time (see UpdateItemValuePayload.isHydrationWrite).
116
120
  emit('update:modelValue', {
117
121
  id: props.id,
118
122
  name: props.name,
119
123
  value: '',
124
+ isHydrationWrite: true,
120
125
  });
121
126
  }
122
127
  }
@@ -15,7 +15,6 @@ import { useShopConfigStore } from '@shared/ecommerce-settings/store/shop-settin
15
15
  import { paymentConfigTr } from '@src/composables/useTranslate';
16
16
  import brandPageClient from '../../../../clients/brand-page-client';
17
17
  import { useUiPageSettingsStore } from '@shared/page-settings/store';
18
- import { useHistoryStore } from '@brand-page-maker/stores/history/history-store';
19
18
 
20
19
  export default defineComponent({
21
20
  props: {
@@ -42,7 +41,6 @@ export default defineComponent({
42
41
  const uiPageSettingsStore = useUiPageSettingsStore();
43
42
  const eventManagerStore = useEventManagerStore();
44
43
  const shopConfigStore = useShopConfigStore();
45
- const historyStore = useHistoryStore();
46
44
 
47
45
  return {
48
46
  brandPageToken: computed(() => brandPageContainerStore.brandPageToken),
@@ -53,7 +51,6 @@ export default defineComponent({
53
51
  eventManagerStore,
54
52
  shopConfigStore,
55
53
  uiPageSettingsStore,
56
- historyStore,
57
54
  };
58
55
  },
59
56
  emits: ['update:modelValue'],
@@ -113,16 +110,16 @@ export default defineComponent({
113
110
  paymentConfig: this.shopConfigStore.paymentConfig,
114
111
  };
115
112
 
113
+ // Lazy paymentConfig hydration — a hydration write, not a user edit.
114
+ // The flag makes the content-settings store fold it into the current
115
+ // undo head at write time (see UpdateItemValuePayload.isHydrationWrite).
116
116
  const payload: UpdateItemValuePayload<PaymentConfig> = {
117
117
  id: this.id,
118
118
  name: this.name,
119
119
  value,
120
+ isHydrationWrite: true,
120
121
  };
121
122
 
122
- // Lazy paymentConfig hydration — fold into current head instead
123
- // of pushing a phantom entry.
124
- this.historyStore.mergeNextMutation();
125
-
126
123
  this.$emit('update:modelValue', payload);
127
124
  }
128
125
  },
@@ -4,7 +4,7 @@
4
4
  v-if="showRichTextEditor"
5
5
  :id="`${id}-${name}`"
6
6
  ref="richTextEditorRef"
7
- v-model="text"
7
+ :model-value="text"
8
8
  :attrs="attrs"
9
9
  :label="label"
10
10
  :placeholder="placeholder"
@@ -17,6 +17,7 @@
17
17
  :content-type="contentType"
18
18
  @focus="onFocus"
19
19
  @blur="onBlur"
20
+ @update:model-value="onRteModelValue"
20
21
  />
21
22
  <TextInput
22
23
  v-else