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

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 (64) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DD0OBnij.js → CreatedOnBrandCrowd-de-de-BSFTp6yl.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-DysrpsmM.js → CreatedOnBrandCrowd-en-us-rPQrOY1L.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-B6twGr9h.js → CreatedOnBrandCrowd-es-es-awd8-QFa.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-yxl0ih_4.js → CreatedOnBrandCrowd-fr-ca-CwJ4J7Ow.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-yxl0ih_4.js → CreatedOnBrandCrowd-fr-fr-CwJ4J7Ow.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BtXDrIcx.js → CreatedOnBrandCrowd-pt-br-BXYfTLlD.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BtXDrIcx.js → CreatedOnBrandCrowd-pt-pt-BXYfTLlD.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-ho_ROy1L.js → CreatedOnDcom-de-de-kX6u1lNK.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-C91p9kls.js → CreatedOnDcom-en-us-Bkq5sgFs.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-B3FYmObE.js → CreatedOnDcom-es-es-zCAC9ujj.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-D40m7fPx.js → CreatedOnDcom-fr-ca-BYz6Wzg5.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-Cd57oYyc.js → CreatedOnDcom-fr-fr-CN-wAaSH.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Du57mWxG.js → CreatedOnDcom-pt-br-BrK4Ngg-.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-fYoPZWwr.js → CreatedOnDcom-pt-pt-AfI_g7xJ.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 +1 -1
  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 +1 -1
  22. package/lib/src/brand-page-maker/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
  23. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
  24. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
  25. package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
  26. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +1 -1
  27. package/lib/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.js +16 -0
  28. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.d.ts +6 -7
  29. package/lib/src/brand-page-maker/composables/useUndoRedoFlag.js +7 -12
  30. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +1 -1
  31. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +12 -7
  32. package/lib/src/brand-page-maker/stores/history/history-store.d.ts +6 -0
  33. package/lib/src/brand-page-maker/stores/history/history-store.js +11 -1
  34. package/lib/src/index.mjs +83 -84
  35. package/lib/src/{lib-lMNy66QO.js → lib-BYFWAvIq.js} +22625 -22276
  36. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +13 -0
  37. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +49 -0
  38. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
  39. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
  40. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
  41. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.d.ts +1 -0
  42. package/lib/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.js +18 -7
  43. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
  44. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +1 -1
  45. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +1 -1
  46. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
  47. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -1
  48. package/package.json +1 -16
  49. package/src/brand-page/utils/logo.util.test.ts +54 -1
  50. package/src/brand-page/utils/logo.util.ts +23 -0
  51. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +1 -1
  52. package/src/brand-page-maker/composables/rich-text-editor/useRichTextEditor.ts +17 -0
  53. package/src/brand-page-maker/composables/useUndoRedoFlag.ts +7 -13
  54. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.test.ts +53 -0
  55. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +13 -16
  56. package/src/brand-page-maker/stores/history/history-store.test.ts +51 -0
  57. package/src/brand-page-maker/stores/history/history-store.ts +19 -0
  58. package/src/metadata-builder/builders/reorder-pages-builder.ts +80 -0
  59. package/src/shared/content-settings/components/text-style-field/TextStyleField.mixin.ts +19 -11
  60. package/lib/src/tracker.d.ts +0 -2
  61. package/lib/src/tracker.js +0 -12
  62. package/lib/src/tracker.mjs +0 -12
  63. package/lib/src/useEventTracker-DmQ6AkNn.js +0 -356
  64. package/src/tracker.ts +0 -28
@@ -0,0 +1,13 @@
1
+ import type { BrandPageModel } from '../../brand-page/models';
2
+ import { BaseMetadataBuilder, type BuilderResult, type MetadataBuilderRequest } from '../models';
3
+ import type { Logger } from '../logger';
4
+ export interface ReorderPagesPayload {
5
+ pageOrder: string[];
6
+ }
7
+ export declare class ReorderPagesBuilder extends BaseMetadataBuilder<ReorderPagesPayload> {
8
+ constructor(request: MetadataBuilderRequest<ReorderPagesPayload>, logger?: Logger);
9
+ validateAsync(): Promise<{
10
+ errors: string[];
11
+ }>;
12
+ buildAsync(model: BrandPageModel): Promise<BuilderResult>;
13
+ }
@@ -0,0 +1,49 @@
1
+ import { BaseMetadataBuilder, } from '../models';
2
+ export class ReorderPagesBuilder extends BaseMetadataBuilder {
3
+ constructor(request, logger) {
4
+ super(request, logger);
5
+ }
6
+ async validateAsync() {
7
+ const errors = [];
8
+ const payload = this.request.payload;
9
+ if (!payload?.pageOrder?.length) {
10
+ errors.push('page_order is required for reorder_pages');
11
+ }
12
+ return { errors };
13
+ }
14
+ async buildAsync(model) {
15
+ this.log('Build called');
16
+ const validation = await this.validateAsync();
17
+ if (validation.errors.length) {
18
+ return { request: this.request, model, errors: validation.errors };
19
+ }
20
+ const { pageOrder } = this.request.payload;
21
+ // Build pageId → routeId lookup from nav.routes
22
+ const pageIdToRouteId = new Map();
23
+ for (const route of model.nav.routes) {
24
+ pageIdToRouteId.set(route.pageId, route.id);
25
+ }
26
+ // Build new nav.orders from the flat page_order (array of pageIds)
27
+ const newOrders = [];
28
+ const seenRouteIds = new Set();
29
+ for (const pageId of pageOrder) {
30
+ const routeId = pageIdToRouteId.get(pageId);
31
+ if (!routeId)
32
+ continue;
33
+ newOrders.push({ routeId, subRoutes: [] });
34
+ seenRouteIds.add(routeId);
35
+ }
36
+ // Safety: append any routes not mentioned (don't lose pages)
37
+ for (const route of model.nav.routes) {
38
+ const routeId = pageIdToRouteId.get(route.pageId);
39
+ if (routeId && !seenRouteIds.has(routeId)) {
40
+ newOrders.push({ routeId, subRoutes: [] });
41
+ }
42
+ }
43
+ model.nav.orders = newOrders;
44
+ this.log({
45
+ message: `Reordered pages: ${pageOrder.join(', ')}`,
46
+ });
47
+ return { request: this.request, model };
48
+ }
49
+ }
@@ -147,7 +147,7 @@ declare const _default: import("vue").DefineComponent<{}, {
147
147
  content: TContent;
148
148
  }>(payload: P): Promise<void>;
149
149
  _captureTemplateSwitchIfEnabled(): void;
150
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
150
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
151
151
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
152
152
  _navigateToActivePage(activePageId?: string): void;
153
153
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -676,7 +676,7 @@ declare const _default: import("vue").DefineComponent<{}, {
676
676
  content: TContent;
677
677
  }>(payload: P): Promise<void>;
678
678
  _captureTemplateSwitchIfEnabled(): void;
679
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
679
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
680
680
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
681
681
  _navigateToActivePage(activePageId?: string): void;
682
682
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -620,7 +620,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
620
620
  content: TContent;
621
621
  }>(payload: P): Promise<void>;
622
622
  _captureTemplateSwitchIfEnabled(): void;
623
- applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string): Promise<void>;
623
+ applyHistorySnapshot(snapshot: import("@brand-page").BrandPageModel, activePageId?: string, logoEditsStr?: string, selectedTemplateId?: string | false): Promise<void>;
624
624
  _syncLogoSettingsFromSnapshot(snapshot: import("@brand-page").BrandPageModel, logoEditsStr?: string): Promise<void>;
625
625
  _navigateToActivePage(activePageId?: string): void;
626
626
  _fallbackSidebarIfSelectedContentMissing(snapshot: import("@brand-page").BrandPageModel): void;
@@ -383,6 +383,7 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
383
383
  onLetterSpacingChange(spacing: string): void;
384
384
  onLineHeightChange(lineHeight: string): void;
385
385
  generateFontFamilySelectorKey(): string;
386
+ syncTextStyleTypeSelection(): void;
386
387
  }, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, "update:modelValue"[], "update:modelValue", import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
387
388
  id: {
388
389
  type: StringConstructor;
@@ -124,13 +124,7 @@ export default defineComponent({
124
124
  this.debounceOnTextSizeChange = debounce(this.onTextSizeChange, TEXT_SIZE_CHANGE_DEBOUNCE_WAIT_IN_MS);
125
125
  this.debounceOnLetterSpacingChange = debounce(this.onLetterSpacingChange, TEXT_SIZE_CHANGE_DEBOUNCE_WAIT_IN_MS);
126
126
  this.debounceOnLineHeightChange = debounce(this.onLineHeightChange, TEXT_SIZE_CHANGE_DEBOUNCE_WAIT_IN_MS);
127
- const value = this.value;
128
- if (value.textStyleType) {
129
- const textStyleTypeOption = this.textStyleTypeOptions.find((x) => x.value === value.textStyleType);
130
- if (textStyleTypeOption) {
131
- textStyleTypeOption.isSelected = true;
132
- }
133
- }
127
+ this.syncTextStyleTypeSelection();
134
128
  },
135
129
  computed: {
136
130
  selectedTextStyle() {
@@ -320,9 +314,26 @@ export default defineComponent({
320
314
  generateFontFamilySelectorKey() {
321
315
  return `fontFamilySelector-${this.id}-${Date.now()}`;
322
316
  },
317
+ // Re-derive the text-style-type selection from the current `value`.
318
+ // Seeded in created() and re-run whenever `value` changes (e.g. undo/redo
319
+ // restores the model) so the dropdown title and `selectedTextStyle`
320
+ // reflect the model rather than a stale local flag. (BP-5811 / BP-5817)
321
+ syncTextStyleTypeSelection() {
322
+ const value = this.value;
323
+ this.textStyleTypeOptions.forEach((option) => {
324
+ // The leading Boolean(...) guard is deliberate — do NOT simplify to
325
+ // `option.value === value?.textStyleType`. The "none" option's value
326
+ // is `undefined`, so that form would SELECT it whenever no style is
327
+ // set, whereas the convention (see emitChange) is all-flags-false
328
+ // when there is no textStyleType.
329
+ option.isSelected =
330
+ Boolean(value?.textStyleType) && option.value === value.textStyleType;
331
+ });
332
+ },
323
333
  },
324
334
  watch: {
325
335
  value() {
336
+ this.syncTextStyleTypeSelection();
326
337
  this.$nextTick(() => {
327
338
  this.fontFamilySelectorKey = this.generateFontFamilySelectorKey();
328
339
  });
@@ -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;
@@ -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-2",
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
  }
@@ -503,6 +503,23 @@ export const useRichTextEditor = (
503
503
  editor.on('update', () => {
504
504
  const sanitizedData = sanitiseClientSideRTE(editor.getHTML());
505
505
 
506
+ // An update fired while the editor is unfocused is TipTap rewriting the
507
+ // 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.
513
+ //
514
+ // Known limitation: a programmatic toolbar command run on an editor the
515
+ // user hasn't focused yet would also merge (losing that undo step). Today
516
+ // the RTE only mounts inside an already-open settings drawer and the user
517
+ // focuses it before formatting, so this doesn't occur — revisit if a
518
+ // consumer ever drives toolbar actions on an unfocused editor.
519
+ if (!editor.isFocused && !historyStore.isApplyingSnapshot) {
520
+ historyStore.mergeNextMutation();
521
+ }
522
+
506
523
  emit('update:modelValue', sanitizedData.sanitizedHtml);
507
524
  });
508
525
 
@@ -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
  });