@designcrowd/library.brand-page 6.2.1 → 6.2.2-live-preview-1

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 (123) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-CtvPtYF6.js → CreatedOnBrandCrowd-de-de-Cn0M-eZ1.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-eGF-EjAX.js → CreatedOnBrandCrowd-en-us-DRLMSDZr.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CSemsOtT.js → CreatedOnBrandCrowd-es-es-BglTu73U.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-DzylX-AT.js → CreatedOnBrandCrowd-fr-ca-Bj6YZ_el.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-DzylX-AT.js → CreatedOnBrandCrowd-fr-fr-Bj6YZ_el.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-QMUxXQsE.js → CreatedOnBrandCrowd-pt-br-DEoOmQ1F.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-QMUxXQsE.js → CreatedOnBrandCrowd-pt-pt-DEoOmQ1F.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-M7OoJt-1.js → CreatedOnDcom-de-de-BZ7gTt4Z.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-D7hMM0AI.js → CreatedOnDcom-en-us-CD6Km5sH.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CLw-BLp2.js → CreatedOnDcom-es-es-CNpZHvEl.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BRu_kmeu.js → CreatedOnDcom-fr-ca-URS-ydJ1.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-IMnEv_yM.js → CreatedOnDcom-fr-fr-D2AVeA3H.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-Bt6v1p27.js → CreatedOnDcom-pt-br-C32frnFs.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-BVsFJIJg.js → CreatedOnDcom-pt-pt-TDYGjRXS.js} +1 -1
  15. package/lib/src/brand-page/models/brand-page-context.model.d.ts +0 -2
  16. package/lib/src/brand-page/models/brand-page-context.model.js +0 -12
  17. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +0 -1
  18. package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +0 -9
  19. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +7 -43
  20. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +6 -42
  21. package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +0 -14
  22. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +0 -42
  23. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +0 -42
  24. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +0 -42
  25. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +0 -42
  26. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +0 -42
  27. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +0 -42
  28. package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +3 -0
  29. package/lib/src/brand-page-maker/composables/useEventTracker.js +10 -0
  30. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +0 -42
  31. package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +2 -4
  32. package/lib/src/{bundled-translations-KY0aSmMp.js → bundled-translations-BYUfWBT3.js} +9 -9
  33. package/lib/src/{bundled-translations.de-DE-BIFi4bYj.js → bundled-translations.de-DE-MmzUu1tt.js} +13 -13
  34. package/lib/src/{bundled-translations.es-ES-CHlKuPLO.js → bundled-translations.es-ES-D-tG33XL.js} +1 -1
  35. package/lib/src/{bundled-translations.fr-CA-DR1fvm8e.js → bundled-translations.fr-CA-CNc1H1h3.js} +1 -1
  36. package/lib/src/{bundled-translations.fr-FR-DnpWVIQg.js → bundled-translations.fr-FR-DX0-yepT.js} +1 -1
  37. package/lib/src/{bundled-translations.pt-BR-p8szTOiT.js → bundled-translations.pt-BR-ih1-LRgy.js} +9 -9
  38. package/lib/src/{bundled-translations.pt-PT-DEk39NhW.js → bundled-translations.pt-PT-Dq2CmD1p.js} +1 -1
  39. package/lib/src/bundles/bundled-translations.de-DE.json +3 -1
  40. package/lib/src/bundles/bundled-translations.es-ES.json +3 -1
  41. package/lib/src/bundles/bundled-translations.fr-CA.json +3 -1
  42. package/lib/src/bundles/bundled-translations.fr-FR.json +3 -1
  43. package/lib/src/bundles/bundled-translations.json +3 -1
  44. package/lib/src/bundles/bundled-translations.pt-BR.json +3 -1
  45. package/lib/src/bundles/bundled-translations.pt-PT.json +3 -1
  46. package/lib/src/composables/useTranslate.d.ts +1 -1
  47. package/lib/src/composables/useTranslate.js +1 -1
  48. package/lib/src/css/library.brand-page-brandCrowd.min.css +4 -4
  49. package/lib/src/css/library.brand-page-brandPage.min.css +4 -4
  50. package/lib/src/css/library.brand-page-designCom.min.css +4 -4
  51. package/lib/src/css/library.brand-page-preview.min.css +4 -4
  52. package/lib/src/index.mjs +2 -2
  53. package/lib/src/{lib-B38VVCSw.js → lib-Dm9AuOJL.js} +16909 -16783
  54. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +2 -0
  55. package/lib/src/metadata-builder/builders/item-id-utils.js +8 -2
  56. package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +8 -1
  57. package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +6 -0
  58. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItem.mixin.d.ts +1 -0
  59. package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +5 -0
  60. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +5 -42
  61. package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.d.ts +9 -1
  62. package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.js +31 -9
  63. package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +1 -0
  64. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +5 -42
  65. package/lib/src/shared/page-navigation/store/page-navigation-store.d.ts +2 -1
  66. package/lib/src/shared/page-navigation/store/page-navigation-store.js +22 -9
  67. package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +7 -1
  68. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +10 -43
  69. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.js +12 -35
  70. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +6 -42
  71. package/lib/src/shared/page-settings/duplicate-page.util.d.ts +4 -0
  72. package/lib/src/shared/page-settings/duplicate-page.util.js +82 -0
  73. package/lib/src/shared/page-settings/models.d.ts +1 -0
  74. package/lib/src/shared/page-settings/store/page-settings-store.d.ts +5 -0
  75. package/lib/src/shared/page-settings/store/page-settings-store.js +90 -7
  76. package/lib/src/shared/page-settings/utils.d.ts +3 -0
  77. package/lib/src/shared/page-settings/utils.js +6 -0
  78. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -56
  79. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +0 -42
  80. package/lib/src/shared/side-bar/components/SideBarTabButton.mixin.d.ts +1 -0
  81. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +1 -42
  82. package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +1 -0
  83. package/lib/src/tracker.mjs +1 -1
  84. package/lib/src/{useEventTracker-C5Ql5p7u.js → useEventTracker-DwPTmzpa.js} +13 -3
  85. package/package.json +1 -1
  86. package/src/brand-page/models/brand-page-context.model.ts +0 -23
  87. package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +0 -10
  88. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +8 -49
  89. package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +1 -1
  90. package/src/brand-page-maker/composables/useEventTracker.ts +13 -0
  91. package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +2 -4
  92. package/src/bundles/bundled-translations.de-DE.json +3 -1
  93. package/src/bundles/bundled-translations.es-ES.json +3 -1
  94. package/src/bundles/bundled-translations.fr-CA.json +3 -1
  95. package/src/bundles/bundled-translations.fr-FR.json +3 -1
  96. package/src/bundles/bundled-translations.json +3 -1
  97. package/src/bundles/bundled-translations.pt-BR.json +3 -1
  98. package/src/bundles/bundled-translations.pt-PT.json +3 -1
  99. package/src/composables/useTranslate.ts +2 -1
  100. package/src/metadata-builder/builders/item-id-utils.ts +9 -2
  101. package/src/shared/ecommerce-settings/components/product-config-field/ProductConfigOption.vue +0 -2
  102. package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.ts +32 -10
  103. package/src/shared/page-navigation/store/page-navigation-store.ts +41 -13
  104. package/src/shared/page-settings/components/page-settings/NestedDraggable.vue +3 -0
  105. package/src/shared/page-settings/components/page-settings/PageElement.vue +2 -0
  106. package/src/shared/page-settings/components/page-settings/PageSettingMenu.test.ts +105 -0
  107. package/src/shared/page-settings/components/page-settings/PageSettingMenu.vue +16 -0
  108. package/src/shared/page-settings/components/page-settings/PageSettings.duplicate.test.ts +108 -0
  109. package/src/shared/page-settings/components/page-settings/PageSettings.mixin.ts +22 -50
  110. package/src/shared/page-settings/components/page-settings/PageSettings.vue +1 -0
  111. package/src/shared/page-settings/components/page-settings/i18n/page-settings.de-DE.json +3 -1
  112. package/src/shared/page-settings/components/page-settings/i18n/page-settings.es-ES.json +3 -1
  113. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-CA.json +3 -1
  114. package/src/shared/page-settings/components/page-settings/i18n/page-settings.fr-FR.json +3 -1
  115. package/src/shared/page-settings/components/page-settings/i18n/page-settings.json +3 -1
  116. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-BR.json +3 -1
  117. package/src/shared/page-settings/components/page-settings/i18n/page-settings.pt-PT.json +3 -1
  118. package/src/shared/page-settings/duplicate-page.util.test.ts +382 -0
  119. package/src/shared/page-settings/duplicate-page.util.ts +124 -0
  120. package/src/shared/page-settings/models.ts +1 -0
  121. package/src/shared/page-settings/store/page-settings-store.test.ts +441 -1
  122. package/src/shared/page-settings/store/page-settings-store.ts +145 -11
  123. package/src/shared/page-settings/utils.ts +10 -0
@@ -9,6 +9,7 @@ const E = {
9
9
  WEBSITE_MAKER_ADD_PAGE: "website_maker_add_page",
10
10
  WEBSITE_MAKER_REMOVE_PAGE: "website_maker_remove_page",
11
11
  WEBSITE_MAKER_REORDER_PAGE: "website_maker_reorder_page",
12
+ WEBSITE_MAKER_DUPLICATE_PAGE: "website_maker_duplicate_page",
12
13
  WEBSITE_MAKER_OPEN: "website_maker_open",
13
14
  WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL: "website_maker_publish_upsell_option_modal",
14
15
  WEBSITE_MAKER_TEMPLATE_SEARCH: "website_maker_template_search",
@@ -62,6 +63,7 @@ const E = {
62
63
  REMOVE_WATERMARK_FOOTER_SETTINGS: "remove_watermark_footer_settings",
63
64
  PREVIEW_WATERMARK: "preview_watermark",
64
65
  ADD_NEW_PAGE: "add_new_page",
66
+ DUPLICATE_PAGE: "duplicate_page",
65
67
  ADD_CONTENT: "add_content",
66
68
  EDITOR_PUBLISH_TOP: "editor_publish_top",
67
69
  EDITOR_PUBLISH_BOTTOM: "editor_publish_bottom",
@@ -99,14 +101,14 @@ const E = {
99
101
  upsellOpenEventProps: {},
100
102
  domainUpsellOpenEventProps: {},
101
103
  publishOpenEventProps: {}
102
- }), l = I([]), _ = (o, A, e = !1, t = "maker_editor") => {
104
+ }), l = I([]), _ = (A, o, e = !1, t = "maker_editor") => {
103
105
  l.value.forEach((s) => {
104
106
  s({
105
107
  scope: t,
106
- eventName: o,
108
+ eventName: A,
107
109
  eventProps: {
108
110
  ...e ? {} : T.value,
109
- ...A
111
+ ...o
110
112
  }
111
113
  });
112
114
  });
@@ -216,6 +218,14 @@ const E = {
216
218
  page_name: e
217
219
  });
218
220
  },
221
+ // The SOURCE page, not the copy: tracking the copy would fragment the
222
+ // dimension across "About Copy", "About Copy 2".
223
+ sendWebsiteManageDuplicatePageEvent: (e, t) => {
224
+ _(E.WEBSITE_MAKER_DUPLICATE_PAGE, {
225
+ page_name: e,
226
+ is_sub_page: !!t
227
+ });
228
+ },
219
229
  sendPublishOptionsForFreeEvent: () => {
220
230
  _(E.WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL, {
221
231
  upsell_option_continuation: i.FREEMIUM
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.2.1",
4
+ "version": "6.2.2-live-preview-1",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -48,17 +48,10 @@ export interface AbTestResult {
48
48
 
49
49
  const AB_TEST_KEYS = {
50
50
  BP_3926_FREEMIUM_WATERMARK: 'bc-bp-3926-freemium-cta-in-website-editor',
51
- // Two separate live-preview experiments — bc-net sends only the one for the
52
- // current purchase state, and a user's value on each side is independent
53
- // (BP-5873). Keys must match `CampaignNames.LivePreview*Test` in bc-net.
54
- BP_5873_LIVE_PREVIEW_PREPURCHASE: 'bc-bp-5873-live-preview-prepurchase',
55
- BP_5873_LIVE_PREVIEW_POSTPURCHASE: 'bc-bp-5873-live-preview-postpurchase',
56
51
  };
57
52
 
58
53
  export interface AbTest {
59
54
  bp_3926_freemium_watermark?: AbTestResult;
60
- bp_5873_live_preview_prepurchase?: AbTestResult;
61
- bp_5873_live_preview_postpurchase?: AbTestResult;
62
55
  }
63
56
 
64
57
  export const MEDIA_SIZES = {
@@ -139,21 +132,5 @@ export const getAbTestResults = (abtestKeyValues: AbTest): AbTest => {
139
132
  );
140
133
  }
141
134
 
142
- // AB Test for BP-5873 - Live preview (pre- and post-purchase run separately)
143
-
144
- if (abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE]) {
145
- abTests.bp_5873_live_preview_prepurchase = getValueType(
146
- AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE,
147
- abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_PREPURCHASE],
148
- );
149
- }
150
-
151
- if (abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE]) {
152
- abTests.bp_5873_live_preview_postpurchase = getValueType(
153
- AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE,
154
- abtestKeyValues[AB_TEST_KEYS.BP_5873_LIVE_PREVIEW_POSTPURCHASE],
155
- );
156
- }
157
-
158
135
  return abTests;
159
136
  };
@@ -17,16 +17,6 @@ export default defineComponent({
17
17
 
18
18
  return {
19
19
  isAdminMaker: computed(() => containerStore.isAdminMaker),
20
- // BP-5873: the Preview button is behind an AB test, and pre/post purchase are
21
- // two separate experiments. Pick the one for the current purchase state — a
22
- // user's value on the other side is independent and must not leak across.
23
- isLivePreviewEnabled: computed(() => {
24
- const experiment = containerStore.isPrePurchase
25
- ? containerStore.abTests.bp_5873_live_preview_prepurchase
26
- : containerStore.abTests.bp_5873_live_preview_postpurchase;
27
-
28
- return !!experiment?.isVariation;
29
- }),
30
20
  isSaving: computed(() => brandPageMakerStore.isSaving),
31
21
  isPublished: computed(() => brandPageMakerStore.isPublished),
32
22
  isPublishButtonEnabled: computed(
@@ -3,31 +3,13 @@ import { createPinia, setActivePinia } from 'pinia';
3
3
  import { shallowMount } from '@vue/test-utils';
4
4
  import ActionBarNavigation from './ActionBarNavigation.vue';
5
5
  import { useBrandPageContainerStore } from '@brand-page-maker/stores';
6
- import { getAbTestResults } from '@brand-page/models/brand-page-context.model';
7
-
8
- // Must match `CampaignNames.LivePreview*` in bc-net. The two experiments share
9
- // one set of variation values — they're told apart by campaign key, not value.
10
- const PREPURCHASE = 'bc-bp-5873-live-preview-prepurchase';
11
- const POSTPURCHASE = 'bc-bp-5873-live-preview-postpurchase';
12
- const VARIATION_1 = 'bp-5873-variation-1';
13
- const CONTROL = 'bp-5873-control';
14
6
 
15
7
  const mountBar = () => shallowMount(ActionBarNavigation);
16
8
 
17
9
  const hasPreviewButton = (wrapper: ReturnType<typeof mountBar>) =>
18
10
  wrapper.find('[data-test-preview-button]').exists();
19
11
 
20
- // The AB value arrives as the raw bc-net campaign string, so go through the real
21
- // parser rather than hand-building an AbTestResult — that's the contract under test.
22
- const setAbTest = (
23
- store: ReturnType<typeof useBrandPageContainerStore>,
24
- key: string,
25
- value: string,
26
- ) => {
27
- store.abTests = getAbTestResults({ [key]: value } as never);
28
- };
29
-
30
- describe('ActionBarNavigation — BP-5873 preview AB test', () => {
12
+ describe('ActionBarNavigation — Preview button', () => {
31
13
  let containerStore: ReturnType<typeof useBrandPageContainerStore>;
32
14
 
33
15
  beforeEach(() => {
@@ -35,39 +17,18 @@ describe('ActionBarNavigation — BP-5873 preview AB test', () => {
35
17
  containerStore = useBrandPageContainerStore();
36
18
  });
37
19
 
38
- test('hides Preview when no AB value has been sent', () => {
39
- expect(hasPreviewButton(mountBar())).toBe(false);
40
- });
41
-
42
- test('post-purchase: shows Preview on variation-1, hides it on control', () => {
43
- containerStore.isPrePurchase = false;
44
-
45
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
46
- expect(hasPreviewButton(mountBar())).toBe(true);
47
-
48
- setAbTest(containerStore, POSTPURCHASE, CONTROL);
49
- expect(hasPreviewButton(mountBar())).toBe(false);
50
- });
51
-
52
- test('pre-purchase: shows Preview on variation-1, hides it on control', () => {
53
- containerStore.isPrePurchase = true;
20
+ test.each([
21
+ ['pre-purchase', true],
22
+ ['post-purchase', false],
23
+ ])('shows Preview to the customer %s', (_, isPrePurchase) => {
24
+ containerStore.isPrePurchase = isPrePurchase;
54
25
 
55
- setAbTest(containerStore, PREPURCHASE, VARIATION_1);
56
26
  expect(hasPreviewButton(mountBar())).toBe(true);
57
-
58
- setAbTest(containerStore, PREPURCHASE, CONTROL);
59
- expect(hasPreviewButton(mountBar())).toBe(false);
60
27
  });
61
28
 
62
- // Two separate experiments, so a value on one side must never satisfy the other —
63
- // otherwise a post-purchase variation would leak the button into pre-purchase.
64
- test('the two experiments never read each other', () => {
65
- containerStore.isPrePurchase = true;
66
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
67
- expect(hasPreviewButton(mountBar())).toBe(false);
29
+ test('hides Preview in the admin maker', () => {
30
+ containerStore.isAdminMaker = true;
68
31
 
69
- containerStore.isPrePurchase = false;
70
- setAbTest(containerStore, PREPURCHASE, VARIATION_1);
71
32
  expect(hasPreviewButton(mountBar())).toBe(false);
72
33
  });
73
34
 
@@ -75,8 +36,6 @@ describe('ActionBarNavigation — BP-5873 preview AB test', () => {
75
36
  // Save drops out. Preview must be a real column like the rest — while it was content
76
37
  // width, Publish ate the whole remaining row on a published site.
77
38
  test('Preview shares the mobile row equally with the other buttons', () => {
78
- containerStore.isPrePurchase = false;
79
- setAbTest(containerStore, POSTPURCHASE, VARIATION_1);
80
39
  containerStore.brandPageStatus = 'Published';
81
40
 
82
41
  const wrapper = mountBar();
@@ -4,7 +4,7 @@
4
4
  class="tw-flex tw-items-center tw-border-t tw-border-solid tw-border-grayscale-600 tw-h-12 tw-px-2 lg:tw-px-0 lg:tw-h-auto lg:tw-border-none tw-gap-3 lg:tw-gap-5 tw-w-full lg:tw-justify-end tw-box-border"
5
5
  >
6
6
  <Button
7
- v-if="!isAdminMaker && isLivePreviewEnabled"
7
+ v-if="!isAdminMaker"
8
8
  :label="constantsTr('previewDisplayName')"
9
9
  variant="no-border"
10
10
  :white="true"
@@ -39,6 +39,7 @@ export const EVENTS = {
39
39
  WEBSITE_MAKER_ADD_PAGE: 'website_maker_add_page',
40
40
  WEBSITE_MAKER_REMOVE_PAGE: 'website_maker_remove_page',
41
41
  WEBSITE_MAKER_REORDER_PAGE: 'website_maker_reorder_page',
42
+ WEBSITE_MAKER_DUPLICATE_PAGE: 'website_maker_duplicate_page',
42
43
  WEBSITE_MAKER_OPEN: 'website_maker_open',
43
44
  WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL:
44
45
  'website_maker_publish_upsell_option_modal',
@@ -107,6 +108,7 @@ export const UPGRADE_CTA_LOCATIONS = {
107
108
  REMOVE_WATERMARK_FOOTER_SETTINGS: 'remove_watermark_footer_settings',
108
109
  PREVIEW_WATERMARK: 'preview_watermark',
109
110
  ADD_NEW_PAGE: 'add_new_page',
111
+ DUPLICATE_PAGE: 'duplicate_page',
110
112
  ADD_CONTENT: 'add_content',
111
113
  EDITOR_PUBLISH_TOP: 'editor_publish_top',
112
114
  EDITOR_PUBLISH_BOTTOM: 'editor_publish_bottom',
@@ -346,6 +348,17 @@ const useEventTracker = () => {
346
348
  page_name: pageName,
347
349
  });
348
350
  },
351
+ // The SOURCE page, not the copy: tracking the copy would fragment the
352
+ // dimension across "About Copy", "About Copy 2".
353
+ sendWebsiteManageDuplicatePageEvent: (
354
+ sourcePageName: string,
355
+ isSubPage?: boolean,
356
+ ) => {
357
+ _publishEvent(EVENTS.WEBSITE_MAKER_DUPLICATE_PAGE, {
358
+ page_name: sourcePageName,
359
+ is_sub_page: Boolean(isSubPage),
360
+ });
361
+ },
349
362
  sendPublishOptionsForFreeEvent: () => {
350
363
  _publishEvent(EVENTS.WEBSITE_MAKER_PUBLISH_UPSELL_OPTION_MODAL, {
351
364
  upsell_option_continuation:
@@ -93,6 +93,7 @@ import {
93
93
  UI_SCREENSIZE_MODES,
94
94
  } from '@shared/models';
95
95
  import { PAGE_SECTIONS } from '@shared/page-settings/models';
96
+ import { toPageModels } from '@shared/page-settings/utils';
96
97
  import { useUiTemplateSettingsStore } from '@shared/template-settings/store';
97
98
  import { useBrandPageContainerStore } from '../brandpage-container';
98
99
  import { useLocalisedRouteStore } from '../route/localised-route-store';
@@ -1924,10 +1925,7 @@ export const useBrandPageMakerStore = defineStore('brandPageMaker', {
1924
1925
  const pageNavigationStore = usePageNavigationStore();
1925
1926
  const paletteStore = useUiColorPaletteStore();
1926
1927
 
1927
- const pages = pageSettingsStore.pages.map((p) => ({
1928
- ...p,
1929
- contents: p.contents.map((c) => c.settings.content),
1930
- })) as PageModel[];
1928
+ const pages = toPageModels(pageSettingsStore.pages);
1931
1929
 
1932
1930
  const backgroundStyles = backgroundSettingsStore.backgroundStyles;
1933
1931
  const logoStyles = styleSettingsStore.logoStyles;
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Seite anzeigen",
1857
1857
  "hidePage": "Seite ausblenden",
1858
1858
  "delete": "Seite löschen",
1859
- "addSubPage": "Unterseite hinzufügen"
1859
+ "addSubPage": "Unterseite hinzufügen",
1860
+ "duplicatePage": "Seite duplizieren",
1861
+ "duplicatePageTitle": "{{title}} Kopieren"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Navigation unter dem Logo anzeigen",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Mostrar página",
1857
1857
  "hidePage": "Ocultar página",
1858
1858
  "delete": "Eliminar página",
1859
- "addSubPage": "Añadir subpágina"
1859
+ "addSubPage": "Añadir subpágina",
1860
+ "duplicatePage": "Duplicar página",
1861
+ "duplicatePageTitle": "{{title}} copia"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Muestra la navegación debajo del logo",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Afficher la page",
1857
1857
  "hidePage": "Masquez la page",
1858
1858
  "delete": "Supprimer la page",
1859
- "addSubPage": "Ajouter une sous-page"
1859
+ "addSubPage": "Ajouter une sous-page",
1860
+ "duplicatePage": "Dupliquer la page",
1861
+ "duplicatePageTitle": "{{title}} Copie"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Afficher la navigation sous le logo",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Afficher la page",
1857
1857
  "hidePage": "Masquez la page",
1858
1858
  "delete": "Supprimer la page",
1859
- "addSubPage": "Ajouter une sous-page"
1859
+ "addSubPage": "Ajouter une sous-page",
1860
+ "duplicatePage": "Dupliquer la page",
1861
+ "duplicatePageTitle": "Copie de {{title}}"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Afficher la navigation sous le logo",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Show page",
1857
1857
  "hidePage": "Hide page",
1858
1858
  "delete": "Delete page",
1859
- "addSubPage": "Add subpage"
1859
+ "addSubPage": "Add subpage",
1860
+ "duplicatePage": "Duplicate page",
1861
+ "duplicatePageTitle": "{{title}} Copy"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Show navigation below logo",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Mostrar página",
1857
1857
  "hidePage": "Ocultar página",
1858
1858
  "delete": "Eliminar página",
1859
- "addSubPage": "Adicionar subpágina"
1859
+ "addSubPage": "Adicionar subpágina",
1860
+ "duplicatePage": "Duplicar página",
1861
+ "duplicatePageTitle": "{{title}} Copiar"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Mostrar navegação abaixo do logotipo",
@@ -1856,7 +1856,9 @@
1856
1856
  "showPage": "Mostrar página",
1857
1857
  "hidePage": "Ocultar página",
1858
1858
  "delete": "Eliminar página",
1859
- "addSubPage": "Adicionar subpágina"
1859
+ "addSubPage": "Adicionar subpágina",
1860
+ "duplicatePage": "Duplicar página",
1861
+ "duplicatePageTitle": "{{title}} Cópia"
1860
1862
  },
1861
1863
  "pageSettingsSection": {
1862
1864
  "showNavigationBelow": "Mostrar a navegação por baixo do logótipo",
@@ -142,7 +142,8 @@ const productConfigOptionTr = (key) => tr(`productConfigOption.${key}`);
142
142
  const editLogoSettingsTr = (key) => tr(`editLogoSettings.${key}`);
143
143
  const myUploadSettingsTr = (key, valuesToInterpolate = {}) =>
144
144
  tr(`myUploadSettings.${key}`, valuesToInterpolate);
145
- const pageSettingsTr = (key) => tr(`pageSettings.${key}`);
145
+ const pageSettingsTr = (key, valuesToInterpolate = {}) =>
146
+ tr(`pageSettings.${key}`, valuesToInterpolate);
146
147
  const pageSettingsSectionTr = (key) => tr(`pageSettingsSection.${key}`);
147
148
  const templateSettingsTr = (key) => tr(`templateSettings.${key}`);
148
149
  const unsplashLibraryTr = (key) => tr(`unsplashLibrary.${key}`);
@@ -10,6 +10,14 @@ export const END_ITEM_ANCHOR = '__end__';
10
10
  /** Generate a stable item id. Mirrors section-level `uuid.v4()` id generation. */
11
11
  export const generateItemId = (): string => uuidv4();
12
12
 
13
+ /** Every array field the minifier surfaces to the AI as a list of items. */
14
+ export const ITEM_ID_BEARING_FIELDS = [
15
+ 'images',
16
+ 'items',
17
+ 'textItems',
18
+ 'links',
19
+ ] as const;
20
+
13
21
  /**
14
22
  * Walk an entire BrandPageModel and backfill a stable uuid `id` onto every
15
23
  * nested array item that lacks one (gallery images, images/shapes/testimonial
@@ -23,8 +31,7 @@ export const generateItemId = (): string => uuidv4();
23
31
  */
24
32
  export const ensureBrandPageItemIds = (model: BrandPageModel): void => {
25
33
  const ensureContentItemIds = (content: Record<string, unknown>): void => {
26
- // Every array field that the minifier surfaces as a list of items.
27
- for (const field of ['images', 'items', 'textItems', 'links'] as const) {
34
+ for (const field of ITEM_ID_BEARING_FIELDS) {
28
35
  const arr = content[field];
29
36
 
30
37
  if (Array.isArray(arr)) ensureItemIds(arr as { id?: string }[]);
@@ -96,7 +96,6 @@ const onEditProduct = (index: number) => {
96
96
  product.enableNotes,
97
97
  );
98
98
  const tempProduct = cloneDeep(shopSettingStore.getProductById(product.id));
99
- console.log(tempProduct, 'tempProduct');
100
99
  productSettingStore.openModal(tempProduct);
101
100
  };
102
101
 
@@ -147,7 +146,6 @@ const onSaveProductModal = (event: {
147
146
  onItemsChange(newProductArray);
148
147
  // Update shop store
149
148
  if (event.isNewProduct) {
150
- console.log(event.product, 'new product');
151
149
  shopSettingStore.addProduct(event.product);
152
150
  } else {
153
151
  shopSettingStore.updateProduct(event.product.id, event.product);
@@ -24,10 +24,9 @@ export function refreshDuplicatedMetadata(
24
24
 
25
25
  model.pages.forEach((page) =>
26
26
  page.contents.forEach((content) => {
27
+ rotateMonetizationIds(content);
28
+
27
29
  if (isShopContent(content)) {
28
- // Shop products collide on the global product / sku_key tables. The shop's payment-config
29
- // link is written with throwaway ids (not the content id), so only the product ids rotate.
30
- content.products.forEach(rotateProductIds);
31
30
  disconnectPayment(content.shopPaymentConfig);
32
31
  } else if (isPayment(content)) {
33
32
  resetPaymentBlock(content, content.requestPaymentConfig);
@@ -40,6 +39,25 @@ export function refreshDuplicatedMetadata(
40
39
  return model;
41
40
  }
42
41
 
42
+ /**
43
+ * Rotate the client-minted ids the brand-page-api keys a **global** row on — shop product / sku_key
44
+ * and payment-option ids. A verbatim copy collides with its source on those tables.
45
+ *
46
+ * Shared with page duplication, which wants only this half: that copy keeps the same owner, so it
47
+ * keeps its payment connection.
48
+ */
49
+ export function rotateMonetizationIds(content: Content): void {
50
+ if (isShopContent(content)) {
51
+ // The shop's payment-config link is written with throwaway ids (not the content id), so only
52
+ // the product ids rotate here.
53
+ content.products.forEach(rotateProductIds);
54
+ } else if (isPayment(content)) {
55
+ rotatePaymentOptionIds(content.requestPaymentConfig);
56
+ } else if (isSupportMe(content)) {
57
+ rotatePaymentOptionIds(content.supportMeConfig);
58
+ }
59
+ }
60
+
43
61
  function rotateProductIds(product: ProductModel): void {
44
62
  product.id = uuid();
45
63
 
@@ -72,19 +90,23 @@ function rotateProductIds(product: ProductModel): void {
72
90
  });
73
91
  }
74
92
 
93
+ // `brand_page_product` is keyed on the payment-option id, so a verbatim copy would adopt the
94
+ // source's product rows on its next payment upsert.
95
+ function rotatePaymentOptionIds(config: PaymentConfig): void {
96
+ config.paymentOptions.forEach((option) => {
97
+ option.id = uuid();
98
+ });
99
+ }
100
+
75
101
  /**
76
102
  * request-payment / support-me content. The api keys `content_block_payment_config` on the content
77
- * id (PK) and `brand_page_product` on the payment-option id (PK), both global. A verbatim copy
78
- * would, on its next payment upsert, repoint the source's config link and adopt its product rows —
79
- * so rotate both to make the copy independent, then drop the connection.
103
+ * id (PK), so a verbatim copy would repoint the source's config link on its next payment upsert -
104
+ * re-mint it, then drop the connection. (The option ids are rotated upstream, for both kinds of
105
+ * duplication.)
80
106
  */
81
107
  function resetPaymentBlock(content: Content, config: PaymentConfig): void {
82
108
  content.id = uuid();
83
109
 
84
- config.paymentOptions.forEach((option) => {
85
- option.id = uuid();
86
- });
87
-
88
110
  disconnectPayment(config);
89
111
  }
90
112
 
@@ -38,13 +38,13 @@ const findParentRouteIdForRouteId = (
38
38
  return undefined;
39
39
  };
40
40
 
41
- // This function will find all the page slugs that exist at the depth that we're modifying, this way, we can avoid duplication of page slugs
42
- // This will help support the use of similar named routes, e.g /home/home-1 and /my-home/home-1
43
- const getPageSlugsForCurrentDepth = (
41
+ // Routes at one depth. Slugs and titles only need to be unique among siblings,
42
+ // so /home/home-1 and /my-home/home-1 can coexist.
43
+ const getRoutesForCurrentDepth = (
44
44
  navModel: NavModel,
45
45
  parentRouteId?: string,
46
46
  excludeRouteId?: string,
47
- ): string[] => {
47
+ ): NavRoute[] => {
48
48
  let ordersAtDepth: NavOrder[];
49
49
 
50
50
  if (!parentRouteId) {
@@ -60,14 +60,21 @@ const getPageSlugsForCurrentDepth = (
60
60
 
61
61
  const routeIdsAtDepth = ordersAtDepth.map((order) => order.routeId);
62
62
 
63
- return navModel.routes
64
- .filter(
65
- (route) =>
66
- routeIdsAtDepth.includes(route.id) && route.id !== excludeRouteId,
67
- )
68
- .map((route) => route.pageSlug);
63
+ return navModel.routes.filter(
64
+ (route) =>
65
+ routeIdsAtDepth.includes(route.id) && route.id !== excludeRouteId,
66
+ );
69
67
  };
70
68
 
69
+ const getPageSlugsForCurrentDepth = (
70
+ navModel: NavModel,
71
+ parentRouteId?: string,
72
+ excludeRouteId?: string,
73
+ ): string[] =>
74
+ getRoutesForCurrentDepth(navModel, parentRouteId, excludeRouteId).map(
75
+ (route) => route.pageSlug,
76
+ );
77
+
71
78
  export const usePageNavigationStore = defineStore('pageNavigation', {
72
79
  state: (): PageNavigationState => ({
73
80
  routes: [],
@@ -102,6 +109,11 @@ export const usePageNavigationStore = defineStore('pageNavigation', {
102
109
  getPageSlugsForDepth(parentRouteId?: string): string[] {
103
110
  return getPageSlugsForCurrentDepth(this.nav, parentRouteId);
104
111
  },
112
+ getDisplayNamesForDepth(parentRouteId?: string): string[] {
113
+ return getRoutesForCurrentDepth(this.nav, parentRouteId).map(
114
+ (route) => route.displayName,
115
+ );
116
+ },
105
117
  getParentRouteIdForPage(pageId: string): string | undefined {
106
118
  const route = this.routes.find((r) => r.pageId === pageId);
107
119
 
@@ -143,12 +155,28 @@ export const usePageNavigationStore = defineStore('pageNavigation', {
143
155
  this.routes = nav.routes;
144
156
  this.orders = nav.orders;
145
157
  },
146
- addNavRoute(newRoute: NavRoute, parentRouteId?: string): void {
158
+ addNavRoute(
159
+ newRoute: NavRoute,
160
+ parentRouteId?: string,
161
+ insertAfterRouteId?: string,
162
+ ): void {
147
163
  const newOrder: NavOrder = {
148
164
  routeId: newRoute.id,
149
165
  subRoutes: [],
150
166
  };
151
167
 
168
+ const addToSiblings = (siblings: NavOrder[]): void => {
169
+ const afterIndex = insertAfterRouteId
170
+ ? siblings.findIndex((order) => order.routeId === insertAfterRouteId)
171
+ : -1;
172
+
173
+ if (afterIndex === -1) {
174
+ siblings.push(newOrder);
175
+ } else {
176
+ siblings.splice(afterIndex + 1, 0, newOrder);
177
+ }
178
+ };
179
+
152
180
  if (parentRouteId) {
153
181
  // Find where to insert the new route into order
154
182
  const parentOrder = findParentOrderByRouteId(
@@ -157,7 +185,7 @@ export const usePageNavigationStore = defineStore('pageNavigation', {
157
185
  );
158
186
 
159
187
  if (parentOrder) {
160
- parentOrder.subRoutes.push(newOrder);
188
+ addToSiblings(parentOrder.subRoutes);
161
189
  } else {
162
190
  console.error(
163
191
  `Parent route with id ${parentRouteId} not found in nav orders.`,
@@ -165,7 +193,7 @@ export const usePageNavigationStore = defineStore('pageNavigation', {
165
193
  }
166
194
  } else {
167
195
  // Push to root of orders object as this is not a nested route
168
- this.orders.push(newOrder);
196
+ addToSiblings(this.orders);
169
197
  }
170
198
 
171
199
  this.routes.push(newRoute);
@@ -28,6 +28,7 @@
28
28
  :delete-page-selected="onDeletePageSelected"
29
29
  :change-page-visibility="onChangePageVisibility"
30
30
  :on-add-sub-page-selected="onAddSubPageSelected"
31
+ :on-duplicate-page-selected="onDuplicatePageSelected"
31
32
  :total-pages="totalPages"
32
33
  :visible-page-count="visiblePageCount"
33
34
  />
@@ -41,6 +42,7 @@
41
42
  :on-delete-page-selected="onDeletePageSelected"
42
43
  :on-change-page-visibility="onChangePageVisibility"
43
44
  :on-add-sub-page-selected="onAddSubPageSelected"
45
+ :on-duplicate-page-selected="onDuplicatePageSelected"
44
46
  :move="handleMove"
45
47
  @drag-start="handleStart"
46
48
  @drag-end="handleEnd"
@@ -92,6 +94,7 @@ interface Props {
92
94
  onDeletePageSelected: (pageId: string) => void;
93
95
  onChangePageVisibility: (pageId: string) => void;
94
96
  onAddSubPageSelected: (pageId: string) => void;
97
+ onDuplicatePageSelected: (pageId: string) => void;
95
98
  }
96
99
 
97
100
  const props = defineProps<Props>();
@@ -45,6 +45,7 @@
45
45
  :on-delete-page-selected="deletePageSelected"
46
46
  :on-change-page-visibility="changePageVisibility"
47
47
  :on-add-sub-page-selected="onAddSubPageSelected"
48
+ :on-duplicate-page-selected="onDuplicatePageSelected"
48
49
  :is-root-page="isRootPage"
49
50
  :total-pages="totalPages"
50
51
  :visible-page-count="visiblePageCount"
@@ -80,6 +81,7 @@ interface Props {
80
81
  deletePageSelected: (pageId: string) => void;
81
82
  changePageVisibility: (pageId: string) => void;
82
83
  onAddSubPageSelected: (pageId: string) => void;
84
+ onDuplicatePageSelected: (pageId: string) => void;
83
85
  }
84
86
 
85
87
  defineProps<Props>();