@designcrowd/library.brand-page 6.0.6-tracker-1 → 6.0.6-undo-3

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 (93) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DD0OBnij.js → CreatedOnBrandCrowd-de-de-x7BcD7Sj.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DysrpsmM.js → CreatedOnBrandCrowd-en-us-CG-ThD_q.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-B6twGr9h.js → CreatedOnBrandCrowd-es-es-aab4_Re2.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-yxl0ih_4.js → CreatedOnBrandCrowd-fr-ca-Drl8X5uh.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-yxl0ih_4.js → CreatedOnBrandCrowd-fr-fr-Drl8X5uh.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BtXDrIcx.js → CreatedOnBrandCrowd-pt-br-8Ib7T70i.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BtXDrIcx.js → CreatedOnBrandCrowd-pt-pt-8Ib7T70i.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ho_ROy1L.js → CreatedOnDcom-de-de-FJtydzc5.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-C91p9kls.js → CreatedOnDcom-en-us-BMiXru6P.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-B3FYmObE.js → CreatedOnDcom-es-es-B8Srdnku.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-D40m7fPx.js → CreatedOnDcom-fr-ca-tcXGiBNt.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-Cd57oYyc.js → CreatedOnDcom-fr-fr-nE1cuUEU.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Du57mWxG.js → CreatedOnDcom-pt-br-DxCCXCg8.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-fYoPZWwr.js → CreatedOnDcom-pt-pt-Csabip9G.js} +1 -1
  15. package/lib/src/brand-page/utils/logo.util.d.ts +14 -0
  16. package/lib/src/brand-page/utils/logo.util.js +19 -0
  17. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
  18. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +2 -2
  19. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +1 -1
  20. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.mixin.d.ts +1 -1
  21. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +3 -3
  22. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
  23. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +1 -1
  24. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +2 -2
  25. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
  26. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
  27. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
  28. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +25 -4
  29. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
  30. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
  31. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
  32. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
  33. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +7 -1
  34. package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
  35. package/lib/src/index.mjs +83 -84
  36. package/lib/src/{lib-lMNy66QO.js → lib-C_UlYMJy.js} +32835 -32458
  37. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
  38. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
  39. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.d.ts +5 -0
  40. package/lib/src/shared/content-settings/components/GenericTextInput.mixin.js +29 -0
  41. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +3 -3
  42. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +2 -2
  43. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +1 -1
  44. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +2 -2
  45. package/lib/src/shared/content-settings/components/email-field/EmailField.mixin.d.ts +5 -0
  46. package/lib/src/shared/content-settings/components/focused-content-item-settings/FocusedContentItemSettings.mixin.d.ts +1 -1
  47. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +0 -67
  48. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.js +4 -6
  49. package/lib/src/shared/content-settings/components/tel-field/TelField.mixin.d.ts +5 -0
  50. package/lib/src/shared/content-settings/components/text-field/TextField.mixin.d.ts +5 -0
  51. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.d.ts +2 -0
  52. package/lib/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.js +19 -0
  53. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1 -0
  54. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
  55. package/lib/src/shared/content-settings/components/textarea-field/TextareaField.mixin.d.ts +5 -0
  56. package/lib/src/shared/content-settings/components/url-field/UrlField.mixin.d.ts +5 -0
  57. package/lib/src/shared/content-settings/components/youtube-video-field/YoutubeVideoField.mixin.d.ts +5 -0
  58. package/lib/src/shared/content-settings/models.d.ts +11 -0
  59. package/lib/src/shared/content-settings/store/content-settings-store.d.ts +1 -1
  60. package/lib/src/shared/content-settings/store/content-settings-store.js +15 -1
  61. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  62. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -2
  63. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -2
  64. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
  65. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  66. package/package.json +1 -16
  67. package/src/brand-page/utils/logo.util.test.ts +54 -1
  68. package/src/brand-page/utils/logo.util.ts +23 -0
  69. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
  70. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +26 -5
  71. package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
  72. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
  73. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
  74. package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
  75. package/src/brand-page-maker/stores/history/history-store.ts +19 -0
  76. package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
  77. package/src/shared/content-settings/components/GenericTextInput.mixin.test.ts +119 -0
  78. package/src/shared/content-settings/components/GenericTextInput.mixin.ts +34 -0
  79. package/src/shared/content-settings/components/page-dropdown-field/PageDropdownField.vue +10 -5
  80. package/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.ts +4 -7
  81. package/src/shared/content-settings/components/text-field/TextField.vue +2 -1
  82. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.test.ts +149 -0
  83. package/src/shared/content-settings/components/text-items-field/TextItemsField.mixin.ts +26 -0
  84. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +19 -11
  85. package/src/shared/content-settings/components/textarea-field/TextareaField.vue +2 -1
  86. package/src/shared/content-settings/models.ts +11 -0
  87. package/src/shared/content-settings/store/content-settings-store.test.ts +110 -0
  88. package/src/shared/content-settings/store/content-settings-store.ts +17 -0
  89. package/lib/src/tracker.d.ts +0 -2
  90. package/lib/src/tracker.js +0 -12
  91. package/lib/src/tracker.mjs +0 -12
  92. package/lib/src/useEventTracker-DmQ6AkNn.js +0 -356
  93. package/src/tracker.ts +0 -28
@@ -26,6 +26,12 @@ export interface HistoryEntry {
26
26
  activePageId: string;
27
27
  /** JSON-stringified `editableLogoStore.logoEdits`. Undefined if logo tab never opened. */
28
28
  logoEdits?: string;
29
+ /**
30
+ * Selected template id at capture time — drives the templates sidebar
31
+ * highlight. `false` when no template is selected. Sidecar to
32
+ * `brandPageData` (not part of the persisted model), like `activePageId`.
33
+ */
34
+ selectedTemplateId?: string | false;
29
35
  }
30
36
 
31
37
  // Monotonic burst — first mutation starts the timer, subsequent mutations
@@ -150,6 +156,10 @@ export const useHistoryStore = defineStore('history', () => {
150
156
  return usePageNavigationStore().activePageId ?? '';
151
157
  }
152
158
 
159
+ function getCurrentSelectedTemplateId(): string | false {
160
+ return useBrandPageMakerStore().selectedTemplateId;
161
+ }
162
+
153
163
  function serializeCurrentLogoEdits(): string | undefined {
154
164
  try {
155
165
  const editableLogoStore = useUiEditableLogoStore();
@@ -187,6 +197,7 @@ export const useHistoryStore = defineStore('history', () => {
187
197
  label?: string;
188
198
  activePageId: string;
189
199
  logoEdits?: string;
200
+ selectedTemplateId?: string | false;
190
201
  }): 'committed' | 'oversized' | 'noop' {
191
202
  const current = entries.value[currentIndex.value];
192
203
 
@@ -219,6 +230,7 @@ export const useHistoryStore = defineStore('history', () => {
219
230
  sizeBytes: args.snapshot.length,
220
231
  activePageId: args.activePageId,
221
232
  logoEdits: args.logoEdits,
233
+ selectedTemplateId: args.selectedTemplateId,
222
234
  };
223
235
 
224
236
  // FR-11: truncate redo tail, then append.
@@ -293,6 +305,7 @@ export const useHistoryStore = defineStore('history', () => {
293
305
 
294
306
  const activePageId = getCurrentActivePageId();
295
307
  const logoEdits = serializeCurrentLogoEdits();
308
+ const selectedTemplateId = getCurrentSelectedTemplateId();
296
309
 
297
310
  // Settle window: fold post-init reactive cascades back into the
298
311
  // baseline so undo doesn't light up before the user has done
@@ -317,6 +330,7 @@ export const useHistoryStore = defineStore('history', () => {
317
330
  sizeBytes: snapshot.length,
318
331
  activePageId,
319
332
  logoEdits,
333
+ selectedTemplateId,
320
334
  },
321
335
  ];
322
336
 
@@ -333,6 +347,7 @@ export const useHistoryStore = defineStore('history', () => {
333
347
  sizeBytes: snapshot.length,
334
348
  activePageId,
335
349
  logoEdits,
350
+ selectedTemplateId,
336
351
  };
337
352
  entries.value = next;
338
353
 
@@ -345,6 +360,7 @@ export const useHistoryStore = defineStore('history', () => {
345
360
  actor: 'user',
346
361
  activePageId,
347
362
  logoEdits,
363
+ selectedTemplateId,
348
364
  });
349
365
  };
350
366
 
@@ -402,6 +418,7 @@ export const useHistoryStore = defineStore('history', () => {
402
418
  sizeBytes: snapshot.length,
403
419
  activePageId: getCurrentActivePageId(),
404
420
  logoEdits: serializeCurrentLogoEdits(),
421
+ selectedTemplateId: getCurrentSelectedTemplateId(),
405
422
  },
406
423
  ];
407
424
  currentIndex.value = 0;
@@ -427,6 +444,7 @@ export const useHistoryStore = defineStore('history', () => {
427
444
  snapshot,
428
445
  activePageId: getCurrentActivePageId(),
429
446
  logoEdits: serializeCurrentLogoEdits(),
447
+ selectedTemplateId: getCurrentSelectedTemplateId(),
430
448
  ...args,
431
449
  });
432
450
  }
@@ -531,6 +549,7 @@ export const useHistoryStore = defineStore('history', () => {
531
549
  snapshot,
532
550
  resolvedActivePageId,
533
551
  targetEntry?.logoEdits,
552
+ targetEntry?.selectedTemplateId,
534
553
  );
535
554
  } finally {
536
555
  isApplyingSnapshot.value = false;
@@ -0,0 +1,80 @@
1
+ import type { BrandPageModel, NavOrder } from '../../brand-page/models';
2
+ import {
3
+ BaseMetadataBuilder,
4
+ type BuilderResult,
5
+ type MetadataBuilderRequest,
6
+ } from '../models';
7
+ import type { Logger } from '../logger';
8
+
9
+ export interface ReorderPagesPayload {
10
+ pageOrder: string[];
11
+ }
12
+
13
+ export class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
14
+ constructor(
15
+ request: MetadataBuilderRequest<ReorderPagesPayload>,
16
+ logger?: Logger,
17
+ ) {
18
+ super(request, logger);
19
+ }
20
+
21
+ async validateAsync(): Promise<{ errors: string[] }> {
22
+ const errors: string[] = [];
23
+ const payload = this.request.payload;
24
+
25
+ if (!payload?.pageOrder?.length) {
26
+ errors.push('page_order is required for reorder_pages');
27
+ }
28
+
29
+ return { errors };
30
+ }
31
+
32
+ async buildAsync(model: BrandPageModel): Promise<BuilderResult> {
33
+ this.log('Build called');
34
+
35
+ const validation = await this.validateAsync();
36
+
37
+ if (validation.errors.length) {
38
+ return { request: this.request, model, errors: validation.errors };
39
+ }
40
+
41
+ const { pageOrder } = this.request.payload!;
42
+
43
+ // Build pageId → routeId lookup from nav.routes
44
+ const pageIdToRouteId = new Map<string, string>();
45
+
46
+ for (const route of model.nav.routes) {
47
+ pageIdToRouteId.set(route.pageId, route.id);
48
+ }
49
+
50
+ // Build new nav.orders from the flat page_order (array of pageIds)
51
+ const newOrders: NavOrder[] = [];
52
+ const seenRouteIds = new Set<string>();
53
+
54
+ for (const pageId of pageOrder) {
55
+ const routeId = pageIdToRouteId.get(pageId);
56
+
57
+ if (!routeId) continue;
58
+
59
+ newOrders.push({ routeId, subRoutes: [] });
60
+ seenRouteIds.add(routeId);
61
+ }
62
+
63
+ // Safety: append any routes not mentioned (don't lose pages)
64
+ for (const route of model.nav.routes) {
65
+ const routeId = pageIdToRouteId.get(route.pageId);
66
+
67
+ if (routeId && !seenRouteIds.has(routeId)) {
68
+ newOrders.push({ routeId, subRoutes: [] });
69
+ }
70
+ }
71
+
72
+ model.nav.orders = newOrders;
73
+
74
+ this.log({
75
+ message: `Reordered pages: ${pageOrder.join(', ')}`,
76
+ });
77
+
78
+ return { request: this.request, model };
79
+ }
80
+ }
@@ -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
@@ -0,0 +1,149 @@
1
+ import { defineComponent } from 'vue';
2
+ import { mount } from '@vue/test-utils';
3
+ import { createPinia, setActivePinia } from 'pinia';
4
+ import type { Content, LogoTheme, TextItem } from '@brand-page/models';
5
+ import TextItemsFieldMixin from './TextItemsField.mixin';
6
+
7
+ // BP-5810: TextItemsField re-wraps per-item TextareaField payloads into one
8
+ // textItems array payload behind its own 250ms debounce. The hydration flag
9
+ // must survive this second hop (AND rule: any user action in the burst
10
+ // downgrades it).
11
+
12
+ const DEBOUNCE_IN_MS = 250;
13
+
14
+ interface EmittedPayload {
15
+ id: string;
16
+ name: string;
17
+ value: TextItem[];
18
+ isHydrationWrite?: boolean;
19
+ }
20
+
21
+ const TestHost = defineComponent({
22
+ mixins: [TextItemsFieldMixin],
23
+ template: '<div />',
24
+ });
25
+
26
+ const makeTextItem = (text: string): TextItem =>
27
+ ({
28
+ text,
29
+ textStyle: {
30
+ fontSize: '16px',
31
+ textAlign: 'left',
32
+ textStyleType: 'paragraphs',
33
+ applyAutoScale: true,
34
+ },
35
+ }) as TextItem;
36
+
37
+ const mountHost = () =>
38
+ mount(TestHost, {
39
+ props: {
40
+ id: 'item-1',
41
+ name: 'textItems',
42
+ content: { id: 'item-1', type: 'text' } as Content,
43
+ value: [makeTextItem('<p>one</p>'), makeTextItem('<p>two</p>')],
44
+ isAdmin: false,
45
+ logoTheme: {
46
+ logoTextColor: '#000000',
47
+ logoBackgroundColor: '#ffffff',
48
+ } as unknown as LogoTheme,
49
+ showRichTextEditor: true,
50
+ },
51
+ });
52
+
53
+ const emittedPayload = (
54
+ wrapper: ReturnType<typeof mountHost>,
55
+ call: number,
56
+ ): EmittedPayload =>
57
+ wrapper.emitted('update:model-value')![call][0] as EmittedPayload;
58
+
59
+ describe('TextItemsField hydration burst flag', () => {
60
+ beforeEach(() => {
61
+ setActivePinia(createPinia());
62
+ vi.useFakeTimers();
63
+ });
64
+
65
+ afterEach(() => {
66
+ vi.useRealTimers();
67
+ });
68
+
69
+ test('hydration-only item writes emit a flagged textItems payload', async () => {
70
+ const wrapper = mountHost();
71
+
72
+ wrapper.vm.onTextAreaTextChange(
73
+ {
74
+ id: 'item-1',
75
+ name: 'textItems',
76
+ value: '<p style="text-align: center;">one</p>',
77
+ isHydrationWrite: true,
78
+ },
79
+ 0,
80
+ );
81
+ wrapper.vm.onTextAreaTextChange(
82
+ {
83
+ id: 'item-1',
84
+ name: 'textItems',
85
+ value: '<p style="text-align: center;">two</p>',
86
+ isHydrationWrite: true,
87
+ },
88
+ 1,
89
+ );
90
+
91
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
92
+
93
+ expect(wrapper.emitted('update:model-value')).toHaveLength(1);
94
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBe(true);
95
+ expect(emittedPayload(wrapper, 0).value[0].text).toBe(
96
+ '<p style="text-align: center;">one</p>',
97
+ );
98
+ });
99
+
100
+ test('an unflagged item write keeps the payload unflagged', async () => {
101
+ const wrapper = mountHost();
102
+
103
+ wrapper.vm.onTextAreaTextChange(
104
+ { id: 'item-1', name: 'textItems', value: '<p>typed</p>' },
105
+ 0,
106
+ );
107
+
108
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
109
+
110
+ expect(wrapper.emitted('update:model-value')).toHaveLength(1);
111
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
112
+ });
113
+
114
+ test('a user action in the burst downgrades a pending hydration flag', async () => {
115
+ const wrapper = mountHost();
116
+
117
+ wrapper.vm.onTextAreaTextChange(
118
+ {
119
+ id: 'item-1',
120
+ name: 'textItems',
121
+ value: '<p>canonical</p>',
122
+ isHydrationWrite: true,
123
+ },
124
+ 0,
125
+ );
126
+
127
+ // Style change is a user action and emits immediately.
128
+ wrapper.vm.setTextStyleOption(
129
+ {
130
+ id: 'item-1',
131
+ name: 'textItems',
132
+ value: { fontSize: '20px', textAlign: 'left' },
133
+ },
134
+ 1,
135
+ );
136
+
137
+ expect(wrapper.emitted('update:model-value')).toHaveLength(1);
138
+ expect(emittedPayload(wrapper, 0).isHydrationWrite).toBeUndefined();
139
+
140
+ // The trailing debounced emit must not resurrect the flag either.
141
+ await vi.advanceTimersByTimeAsync(DEBOUNCE_IN_MS + 10);
142
+
143
+ const calls = wrapper.emitted('update:model-value')!;
144
+
145
+ for (let i = 0; i < calls.length; i++) {
146
+ expect(emittedPayload(wrapper, i).isHydrationWrite).toBeUndefined();
147
+ }
148
+ });
149
+ });