@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
@@ -393,7 +393,7 @@ export declare const useUiContentSettingsStore: import("pinia").StoreDefinition<
393
393
  pushMode(item: ContentSettingsModeStackItem): void;
394
394
  resetMode(): void;
395
395
  updateItemValue(payload: UpdateItemValuePayload<unknown>): void;
396
- updateMultipleItemValues({ id, name, value, }: UpdateMultipleItemValuesPayload<any>): void;
396
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }: UpdateMultipleItemValuesPayload<any>): void;
397
397
  updateTextStyleType({ id, name, value, }: UpdateMultipleItemValuesPayload<any>): void;
398
398
  updateTextStyles({ id, name, value, }: UpdateMultipleItemValuesPayload<any>): void;
399
399
  getTextStyles({ id, name }: {
@@ -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,11 +227,15 @@ 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();
@@ -134,7 +134,7 @@ declare const _default: import("vue").DefineComponent<{}, {
134
134
  content: TContent;
135
135
  }>(payload: P): Promise<void>;
136
136
  _captureTemplateSwitchIfEnabled(): void;
137
- applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
137
+ applyHistorySnapshot(snapshot: import("../../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
138
138
  _syncLogoSettingsFromSnapshot(snapshot: import("../../../brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
139
139
  _navigateToActivePage(activePageId?: string): void;
140
140
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("../../../brand-page").BrandPageModel): void;
@@ -3856,7 +3856,7 @@ declare const _default: import("vue").DefineComponent<{}, {
3856
3856
  content: TContent;
3857
3857
  }>(payload: P): Promise<void>;
3858
3858
  _captureTemplateSwitchIfEnabled(): void;
3859
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
3859
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
3860
3860
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
3861
3861
  _navigateToActivePage(activePageId?: string): void;
3862
3862
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -4411,7 +4411,7 @@ 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;
4414
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4415
4415
  updateTextStyleType({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4416
4416
  updateTextStyles({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
4417
4417
  getTextStyles({ id, name }: {
@@ -3787,7 +3787,7 @@ 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;
3790
+ updateMultipleItemValues({ id, name, value, isHydrationWrite, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3791
3791
  updateTextStyleType({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3792
3792
  updateTextStyles({ id, name, value, }: import("../../../content-settings/models").UpdateMultipleItemValuesPayload<any>): void;
3793
3793
  getTextStyles({ id, name }: {
@@ -4425,7 +4425,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
4425
4425
  content: TContent;
4426
4426
  }>(payload: P): Promise<void>;
4427
4427
  _captureTemplateSwitchIfEnabled(): void;
4428
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
4428
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
4429
4429
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
4430
4430
  _navigateToActivePage(activePageId?: string): void;
4431
4431
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -184,7 +184,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
184
184
  content: TContent;
185
185
  }>(payload: P): Promise<void>;
186
186
  _captureTemplateSwitchIfEnabled(): void;
187
- applyHistorySnapshot(snapshot: import("../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
187
+ applyHistorySnapshot(snapshot: import("../../brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
188
188
  _syncLogoSettingsFromSnapshot(snapshot: import("../../brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
189
189
  _navigateToActivePage(activePageId?: string): void;
190
190
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("../../brand-page").BrandPageModel): void;
@@ -582,7 +582,7 @@ declare const _default: import("vue").DefineComponent<{}, {
582
582
  content: TContent;
583
583
  }>(payload: P): Promise<void>;
584
584
  _captureTemplateSwitchIfEnabled(): void;
585
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
585
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
586
586
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
587
587
  _navigateToActivePage(activePageId?: string): void;
588
588
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
package/package.json CHANGED
@@ -1,27 +1,12 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.0.6-tracker-1",
4
+ "version": "6.0.6-undo-3",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
8
8
  "types": "./lib/src/index.d.ts",
9
9
  "main": "./lib/src/index.mjs",
10
- "sideEffects": [
11
- "**/*.css"
12
- ],
13
- "exports": {
14
- ".": {
15
- "types": "./lib/src/index.d.ts",
16
- "import": "./lib/src/index.mjs"
17
- },
18
- "./tracker": {
19
- "types": "./lib/src/tracker.d.ts",
20
- "import": "./lib/src/tracker.mjs"
21
- },
22
- "./lib/src/css/*": "./lib/src/css/*",
23
- "./package.json": "./package.json"
24
- },
25
10
  "files": [
26
11
  "lib",
27
12
  "src"
@@ -12,7 +12,12 @@ import {
12
12
  PageHeaderMobile,
13
13
  PageStyles,
14
14
  } from '../models';
15
- import type { BrandPageModel, LogoContent, PageModel } from '../models';
15
+ import type {
16
+ BrandPageModel,
17
+ BrandPageType,
18
+ LogoContent,
19
+ PageModel,
20
+ } from '../models';
16
21
  import * as logoUtil from './logo.util';
17
22
 
18
23
  interface GetLogoVariationIndexFromLogoImageUrlTestCase {
@@ -212,4 +217,52 @@ describe('logo-util', () => {
212
217
  logoImage.logoDraftToken,
213
218
  );
214
219
  });
220
+
221
+ describe('getLogoImageFromModel', () => {
222
+ // Distinct tokens in the content block vs the header so each assertion
223
+ // proves which location the helper read from.
224
+ const buildModel = (brandPageType: BrandPageType): BrandPageModel => {
225
+ const model = createTestBrandPageModel();
226
+
227
+ model.brandPageType = brandPageType;
228
+ (model.pages[0].contents[0] as LogoContent).logoImage = {
229
+ logoDraftToken: 'content-token',
230
+ logoVariation: 0,
231
+ logoLastUpdate: undefined,
232
+ };
233
+ model.pageHeaderDesktop.logoImage = {
234
+ logoDraftToken: 'header-token',
235
+ logoVariation: 0,
236
+ logoLastUpdate: undefined,
237
+ };
238
+
239
+ return model;
240
+ };
241
+
242
+ test('reads the header logo for brand-site', () => {
243
+ const model = buildModel(BRAND_PAGE_TYPES.brandSite);
244
+
245
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
246
+ 'header-token',
247
+ );
248
+ });
249
+
250
+ test('reads the content-block logo for brand-page', () => {
251
+ const model = buildModel(BRAND_PAGE_TYPES.default);
252
+
253
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
254
+ 'content-token',
255
+ );
256
+ });
257
+
258
+ // Regression guard: brand-contact previously fell through to the header
259
+ // branch (the `=== brandPage` check) and read a stale header logo on undo.
260
+ test('reads the content-block logo for brand-contact', () => {
261
+ const model = buildModel(BRAND_PAGE_TYPES.brandContact);
262
+
263
+ expect(logoUtil.getLogoImageFromModel(model)?.logoDraftToken).toBe(
264
+ 'content-token',
265
+ );
266
+ });
267
+ });
215
268
  });
@@ -394,6 +394,29 @@ export const calculateOptimizedLogoDimensions = (
394
394
  return { width: currentWidth, height: currentHeight };
395
395
  };
396
396
 
397
+ /**
398
+ * Resolves the "current" logo image from a brand-page MODEL (the raw,
399
+ * deserialized shape — not the live stores, whose contents are wrapped in
400
+ * `settings.content`).
401
+ *
402
+ * Brand-site keeps its logo in the page header; brand-page and brand-contact
403
+ * keep it as a logo CONTENT block on the home page — the same `isBrandSite ?
404
+ * header : content` split the logo-settings store getters/setter use. This is
405
+ * the single source of truth for model-level logo reads: initial load and
406
+ * history-restore both call it so the two paths can't drift. They previously
407
+ * hard-coded `=== brandPage`, which silently excluded brand-contact and read a
408
+ * stale header logo on undo.
409
+ */
410
+ export const getLogoImageFromModel = (
411
+ model: BrandPageModel,
412
+ ): LogoImageModel | undefined => {
413
+ if (model.brandPageType === BRAND_PAGE_TYPES.brandSite) {
414
+ return model.pageHeaderDesktop?.logoImage;
415
+ }
416
+
417
+ return model.pages?.[0]?.contents?.find(isLogoContent)?.logoImage;
418
+ };
419
+
397
420
  export const recalculateLogoContentDimensions = async (
398
421
  pageData: any,
399
422
  editableLogoVariations: {
@@ -291,7 +291,7 @@ export default defineComponent({
291
291
 
292
292
  // Capture baseline for undo/redo when the feature is active for this
293
293
  // session — see useUndoRedoFlag.ts for the gate (LaunchDarkly flag +
294
- // brand-site + non-admin maker).
294
+ // non-admin maker; all three brands).
295
295
  if (useUndoRedoFlag().value) {
296
296
  useHistoryStore().captureBaseline();
297
297
  }
@@ -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',
@@ -503,7 +507,24 @@ export const useRichTextEditor = (
503
507
  editor.on('update', () => {
504
508
  const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
505
509
 
506
- emit('update:modelValue', sanitizedData.sanitizedHtml);
510
+ // An update fired while the editor is unfocused is TipTap rewriting the
511
+ // model back into its canonical HTML during mount rehydration or a
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.
519
+ //
520
+ // Known limitation: a programmatic toolbar command run on an editor the
521
+ // user hasn't focused yet would also merge (losing that undo step). Today
522
+ // the RTE only mounts inside an already-open settings drawer and the user
523
+ // focuses it before formatting, so this doesn't occur — revisit if a
524
+ // consumer ever drives toolbar actions on an unfocused editor.
525
+ emit('update:modelValue', sanitizedData.sanitizedHtml, {
526
+ isHydration: !editor.isFocused,
527
+ });
507
528
  });
508
529
 
509
530
  editor.on('selectionUpdate', () => {
@@ -1,30 +1,24 @@
1
1
  import { computed, type ComputedRef } from 'vue';
2
2
  import { useBrandPageMakerUserStore } from '@brand-page-maker/stores/user';
3
- import { useBrandPageMakerStore } from '@brand-page-maker/stores/brandpage-maker/brandpage-maker-store';
4
3
 
5
4
  /**
6
- * Returns whether undo/redo is active for the current maker session. All
7
- * three conditions must hold:
5
+ * Returns whether undo/redo is active for the current maker session. Both
6
+ * conditions must hold:
8
7
  *
9
8
  * 1. `userStore.features.canUseUndoRedo` is true. The flag is populated by
10
9
  * the consumer (BrandCrowd.Net / web-renderer) via the
11
10
  * `WebsiteDesignUserFeatures.CanUseUndoRedo` boolean on the ViewModel —
12
- * same precedent as RichTextEditor / GettyVideos / AITextGenerator.
13
- * 2. The maker is a brand-site (Website Maker). V1 scope is brand-site
14
- * only; brand-page (single-page profile) and brand-contact (vCard) are
15
- * out of scope.
16
- * 3. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
11
+ * same precedent as RichTextEditor / GettyVideos / AITextGenerator. The
12
+ * one flag governs all three brands (brand-site, brand-page,
13
+ * brand-contact) — there is no per-brand gating.
14
+ * 2. The session is NOT in admin-maker mode (`userStore.isAdmin`). The
17
15
  * admin maker uses the same components but a different editing flow
18
16
  * (template authoring) where undo/redo is out of scope.
19
17
  */
20
18
  export function useUndoRedoFlag(): ComputedRef<boolean> {
21
19
  const userStore = useBrandPageMakerUserStore();
22
- const brandPageMakerStore = useBrandPageMakerStore();
23
20
 
24
21
  return computed(
25
- () =>
26
- userStore.features.canUseUndoRedo &&
27
- brandPageMakerStore.isBrandSite &&
28
- !userStore.isAdmin,
22
+ () => userStore.features.canUseUndoRedo && !userStore.isAdmin,
29
23
  );
30
24
  }
@@ -727,6 +727,59 @@ describe('brand page maker store module', () => {
727
727
  expect(pageNavigationStore.activePageId).toBe(expectedFirstId);
728
728
  });
729
729
 
730
+ // BP-5813: the templates sidebar highlight binds to `selectedTemplateId`,
731
+ // which isn't part of `brandPageData`. It rides along as a history-entry
732
+ // sidecar; applyHistorySnapshot restores it so the highlight reverts in
733
+ // lock-step with the canvas.
734
+ test('applyHistorySnapshot restores selectedTemplateId from the sidecar', async () => {
735
+ const makerStore = useBrandPageMakerStore();
736
+
737
+ await makerStore.initialise(INITIALISE_PAYLOAD);
738
+
739
+ makerStore.setSelectedTemplateId({ templateId: 'template-current' });
740
+
741
+ // Undo lands on an entry captured while 'template-previous' was selected.
742
+ await makerStore.applyHistorySnapshot(
743
+ makerStore.brandPageData,
744
+ undefined,
745
+ undefined,
746
+ 'template-previous',
747
+ );
748
+
749
+ expect(makerStore.selectedTemplateId).toBe('template-previous');
750
+ });
751
+
752
+ test('applyHistorySnapshot restores a cleared (false) selectedTemplateId', async () => {
753
+ const makerStore = useBrandPageMakerStore();
754
+
755
+ await makerStore.initialise(INITIALISE_PAYLOAD);
756
+
757
+ makerStore.setSelectedTemplateId({ templateId: 'template-current' });
758
+
759
+ await makerStore.applyHistorySnapshot(
760
+ makerStore.brandPageData,
761
+ undefined,
762
+ undefined,
763
+ false,
764
+ );
765
+
766
+ expect(makerStore.selectedTemplateId).toBe(false);
767
+ });
768
+
769
+ test('applyHistorySnapshot leaves selectedTemplateId untouched when the sidecar is undefined', async () => {
770
+ const makerStore = useBrandPageMakerStore();
771
+
772
+ await makerStore.initialise(INITIALISE_PAYLOAD);
773
+
774
+ makerStore.setSelectedTemplateId({ templateId: 'template-current' });
775
+
776
+ // An entry with no sidecar (e.g. captured before the field existed) must
777
+ // not clobber the current selection.
778
+ await makerStore.applyHistorySnapshot(makerStore.brandPageData);
779
+
780
+ expect(makerStore.selectedTemplateId).toBe('template-current');
781
+ });
782
+
730
783
  // BP-5506 follow-up: undoing a non-page-structural edit (e.g. a title
731
784
  // change) must NOT push a new route. The router push fires
732
785
  // `router.afterEach`, and BrandPageMaker.mixin's afterEach hook resets
@@ -793,6 +793,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
793
793
  snapshot: BrandPageModel,
794
794
  activePageId?: string,
795
795
  logoEditsStr?: string,
796
+ selectedTemplateId?: string | false,
796
797
  ): Promise<void> {
797
798
  const styleSettingsStore = useUiStyleSettingsStore();
798
799
  const navigationStyleSettingsStore = useUiNavigationStyleSettingsStore();
@@ -883,6 +884,16 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
883
884
  this.cookieOptions = snapshot.cookieOptions;
884
885
  this.saveContactSettings = snapshot.saveContactSettings;
885
886
 
887
+ // 7b. Restore the selected-template pointer that drives the templates
888
+ // sidebar highlight. It's a UI pointer, not part of the persisted
889
+ // model, so it rides along as a history-entry sidecar (like
890
+ // `activePageId`). Its watchers only reset scroll position (no
891
+ // template re-application), so setting it here is safe during apply.
892
+ // (BP-5813)
893
+ if (selectedTemplateId !== undefined) {
894
+ this.setSelectedTemplateId({ templateId: selectedTemplateId });
895
+ }
896
+
886
897
  // 8. Re-sync the logo-settings sidebar's `selectedLogoImage` from the
887
898
  // restored header logo. The preview rehydrates via the page contents,
888
899
  // but `uiLogoSettings` holds its own copy that drives the sidebar
@@ -945,14 +956,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
945
956
  }
946
957
  }
947
958
 
948
- const logoImage =
949
- snapshot.brandPageType === BRAND_PAGE_TYPES.default
950
- ? (
951
- snapshot.pages?.[0]?.contents?.find(
952
- (c) => c.type === 'logo',
953
- ) as any
954
- )?.logoImage
955
- : snapshot.pageHeaderDesktop?.logoImage;
959
+ const logoImage = logoUtil.getLogoImageFromModel(snapshot);
956
960
 
957
961
  if (!logoImage) return;
958
962
 
@@ -1273,14 +1277,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1273
1277
  brandPageType: _brandPageType,
1274
1278
  });
1275
1279
 
1276
- let logoImage =
1277
- brandPageModel.brandPageType === BRAND_PAGE_TYPES.default
1278
- ? (
1279
- brandPageModel.pages?.[0].contents.find(
1280
- (c) => c.type === 'logo',
1281
- ) as any
1282
- )?.logoImage
1283
- : brandPageModel.pageHeaderDesktop?.logoImage;
1280
+ let logoImage = logoUtil.getLogoImageFromModel(brandPageModel);
1284
1281
 
1285
1282
  if (!logoImage?.logoDraftToken) {
1286
1283
  logoImage = logoUtil.safeGetLogoImageFromLogoImageUrl(
@@ -16,6 +16,8 @@ const mockMakerStore = {
16
16
  isBrandPageLoaded: false,
17
17
  applyHistorySnapshot: vi.fn(async () => undefined),
18
18
  setIsBrandPageLoaded: vi.fn(),
19
+ selectedTemplateId: false as string | false,
20
+ setSelectedTemplateId: vi.fn(),
19
21
  };
20
22
 
21
23
  const mockPageNavigationStore = {
@@ -64,6 +66,8 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
64
66
  beforeEach(() => {
65
67
  setActivePinia(createPinia());
66
68
  mockMakerStore.applyHistorySnapshot.mockClear();
69
+ mockMakerStore.setSelectedTemplateId.mockClear();
70
+ mockMakerStore.selectedTemplateId = false;
67
71
  mutationCounter = 0;
68
72
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
69
73
  (mockMakerStore.brandPageData as any).v = 1;
@@ -443,6 +447,46 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
443
447
  });
444
448
  });
445
449
 
450
+ // BP-5813: the templates sidebar highlight binds to
451
+ // `brandPageMakerStore.selectedTemplateId`, which is NOT part of
452
+ // `brandPageData`. It rides along as a per-entry sidecar so undo/redo can
453
+ // restore the highlight in lock-step with the canvas.
454
+ describe('selectedTemplateId sidecar (BP-5813 — template sidebar highlight)', () => {
455
+ it('captures the current selectedTemplateId on each entry', async () => {
456
+ const store = useHistoryStore();
457
+
458
+ mockMakerStore.selectedTemplateId = 'template-a';
459
+ await store.captureBaseline();
460
+
461
+ expect(store.entries[0].selectedTemplateId).toBe('template-a');
462
+
463
+ mockMakerStore.selectedTemplateId = 'template-b';
464
+ captureMutated(store);
465
+
466
+ expect(store.entries[1].selectedTemplateId).toBe('template-b');
467
+ });
468
+
469
+ it('passes the target entry selectedTemplateId to applyHistorySnapshot on undo', async () => {
470
+ const store = useHistoryStore();
471
+
472
+ mockMakerStore.selectedTemplateId = 'template-a';
473
+ await store.captureBaseline();
474
+
475
+ mockMakerStore.selectedTemplateId = 'template-b';
476
+ captureMutated(store);
477
+
478
+ await store.undo();
479
+
480
+ // 4th arg is the baseline entry's template id — the sidebar reverts
481
+ // with the canvas instead of lagging on the undone template.
482
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
483
+ const callArgs = mockMakerStore.applyHistorySnapshot.mock
484
+ .calls[0] as any[];
485
+
486
+ expect(callArgs[3]).toBe('template-a');
487
+ });
488
+ });
489
+
446
490
  it('reentrancy guard: capture no-ops when isApplyingSnapshot is true', async () => {
447
491
  const store = useHistoryStore();
448
492
 
@@ -505,6 +549,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
505
549
  expect.objectContaining(baselineBrandPageData),
506
550
  undefined,
507
551
  expect.any(String),
552
+ false,
508
553
  );
509
554
  });
510
555
 
@@ -767,6 +812,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
767
812
  expect.anything(),
768
813
  'page-home',
769
814
  expect.any(String),
815
+ false,
770
816
  );
771
817
  });
772
818
 
@@ -801,6 +847,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
801
847
  expect.anything(),
802
848
  'page-home',
803
849
  expect.any(String),
850
+ false,
804
851
  );
805
852
  });
806
853
 
@@ -842,6 +889,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
842
889
  expect.anything(),
843
890
  'page-about',
844
891
  expect.any(String),
892
+ false,
845
893
  );
846
894
  });
847
895
 
@@ -879,6 +927,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
879
927
  expect.anything(),
880
928
  'page-new',
881
929
  expect.any(String),
930
+ false,
882
931
  );
883
932
  });
884
933
 
@@ -912,6 +961,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
912
961
  expect.anything(),
913
962
  'page-home',
914
963
  expect.any(String),
964
+ false,
915
965
  );
916
966
 
917
967
  // Now redo. Pointer moves forward to entry 1 (page-x captured),
@@ -922,6 +972,7 @@ describe('history-store (single stack + JSON-string snapshots)', () => {
922
972
  expect.anything(),
923
973
  'page-x',
924
974
  expect.any(String),
975
+ false,
925
976
  );
926
977
  });
927
978
  });