@designcrowd/library.brand-page 6.1.3 → 6.1.4-ast-prepurchase
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.
- package/lib/src/{CreatedOnBrandCrowd-de-de-Lk8J1vHg.js → CreatedOnBrandCrowd-de-de-BFDE6VfS.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-BbXj5fb1.js → CreatedOnBrandCrowd-en-us-CRhl6-Cd.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-CjnqvYCi.js → CreatedOnBrandCrowd-es-es-VaP_-7dv.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-ca-BySGqvWO.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-Dbjv4qpN.js → CreatedOnBrandCrowd-fr-fr-BySGqvWO.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-BZIOPEvL.js → CreatedOnBrandCrowd-pt-br-BHjXVcSk.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-BZIOPEvL.js → CreatedOnBrandCrowd-pt-pt-BHjXVcSk.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-aN4JMRDI.js → CreatedOnDcom-de-de-DIFO5IZY.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-f3VD6Pz4.js → CreatedOnDcom-en-us-CcpDa04T.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CQhNdtRP.js → CreatedOnDcom-es-es-DHwY0lWp.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CFsC_XoO.js → CreatedOnDcom-fr-ca-BP3P0hJL.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-J4ZtCBO9.js → CreatedOnDcom-fr-fr-Bai1giy3.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-C1yFugh_.js → CreatedOnDcom-pt-br-GgKiDrbK.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-ycOo5lDn.js → CreatedOnDcom-pt-pt-uuuxCYR3.js} +1 -1
- package/lib/src/PreviewModal-DY2aAN0a.js +19931 -0
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.d.ts +0 -1
- package/lib/src/brand-page/components/page-footer/PageFooter.mixin.js +1 -4
- package/lib/src/brand-page/models/brand-page-context.model.d.ts +2 -0
- package/lib/src/brand-page/models/brand-page-context.model.js +12 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +2 -0
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.js +14 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +134 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +50 -0
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.d.ts +1 -0
- package/lib/src/brand-page-maker/components/navigation/Navigation.mixin.js +3 -0
- package/lib/src/brand-page-maker/components/preview/Preview.mixin.d.ts +14 -0
- package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +130 -0
- package/lib/src/brand-page-maker/composables/useEventTracker.d.ts +1 -0
- package/lib/src/brand-page-maker/composables/useEventTracker.js +1 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +132 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +22 -6
- package/lib/src/bundled-translations-D42tE0D2.js +179 -0
- package/lib/src/bundled-translations.de-DE-D9aWGgFt.js +179 -0
- package/lib/src/bundled-translations.es-ES-lk4B0xQ7.js +179 -0
- package/lib/src/bundled-translations.fr-CA-DhOBNt6q.js +179 -0
- package/lib/src/bundled-translations.fr-FR-CnpafQeG.js +179 -0
- package/lib/src/bundled-translations.pt-BR-C4PnKrd9.js +179 -0
- package/lib/src/bundled-translations.pt-PT-CfKDVQC-.js +179 -0
- package/lib/src/bundles/bundled-translations.de-DE.json +5 -0
- package/lib/src/bundles/bundled-translations.es-ES.json +5 -0
- package/lib/src/bundles/bundled-translations.fr-CA.json +5 -0
- package/lib/src/bundles/bundled-translations.fr-FR.json +5 -0
- package/lib/src/bundles/bundled-translations.json +5 -0
- package/lib/src/bundles/bundled-translations.pt-BR.json +5 -0
- package/lib/src/bundles/bundled-translations.pt-PT.json +5 -0
- package/lib/src/clients/brand-page-client.d.ts +1 -0
- package/lib/src/composables/useTranslate.d.ts +2 -1
- package/lib/src/composables/useTranslate.js +2 -1
- package/lib/src/constants/brand-page-maker.d.ts +2 -0
- package/lib/src/constants/brand-page-maker.js +5 -0
- package/lib/src/constants/live-preview.constants.d.ts +15 -0
- package/lib/src/constants/live-preview.constants.js +15 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +264 -121
- package/lib/src/css/library.brand-page-brandPage.min.css +268 -121
- package/lib/src/css/library.brand-page-designCom.min.css +264 -121
- package/lib/src/css/library.brand-page-preview.min.css +264 -121
- package/lib/src/index.d.ts +2 -1
- package/lib/src/index.mjs +102150 -225
- package/lib/src/lib.js +2 -1
- package/lib/src/preview-modal.d.ts +5 -0
- package/lib/src/preview-modal.js +12 -0
- package/lib/src/preview-modal.mjs +6 -0
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +130 -0
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +130 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +130 -0
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +130 -0
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +144 -0
- package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +130 -0
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +130 -0
- package/lib/src/tracker.mjs +1 -1
- package/lib/src/{useEventTracker-DmQ6AkNn.js → useEventTracker-Dmc2jFdN.js} +5 -4
- package/package.json +5 -1
- package/src/brand-page/components/button/Button.vue +11 -0
- package/src/brand-page/components/button/Button.vue.test.ts +90 -0
- package/src/brand-page/components/image-gallery/masonry/MasonryGallery.vue +1 -0
- package/src/brand-page/components/images-section/ImagesSection.vue +10 -0
- package/src/brand-page/components/media/Video.vue +60 -1
- package/src/brand-page/components/page-footer/PageFooter.mixin.ts +1 -4
- package/src/brand-page/components/page-footer/PageFooter.vue +0 -1
- package/src/brand-page/components/payment/Payment.vue +3 -0
- package/src/brand-page/components/payment/request-payment/RequestPayment.vue +1 -0
- package/src/brand-page/components/payment/support-me/SupportMe.vue +3 -0
- package/src/brand-page/components/shop/CartModal.vue +9 -0
- package/src/brand-page/components/shop/Shop.vue +19 -0
- package/src/brand-page/components/shop/Shop.vue.test.ts +182 -2
- package/src/brand-page/components/social-links/SocialLinks.vue +1 -9
- package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +28 -8
- package/src/brand-page/components/social-links/SocialLinksItems.vue +6 -2
- package/src/brand-page/components/testimonial-section/TestimonialSection.vue +2 -2
- package/src/brand-page/components/video/Video.vue +3 -0
- package/src/brand-page/models/brand-page-context.model.ts +23 -0
- package/src/brand-page-maker/components/action-bar/ActionBar.mixin.ts +15 -0
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.test.ts +95 -0
- package/src/brand-page-maker/components/action-bar/ActionBarNavigation.vue +19 -4
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +57 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.preview-gate.test.ts +113 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.vue +26 -1
- package/src/brand-page-maker/components/navigation/Navigation.mixin.ts +3 -0
- package/src/brand-page-maker/components/navigation/Navigation.vue +1 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModal.test.ts +162 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModal.vue +347 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.test.ts +226 -0
- package/src/brand-page-maker/components/preview-modal/PreviewModalWithState.vue +149 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.de-DE.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.es-ES.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-CA.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.fr-FR.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-BR.json +6 -0
- package/src/brand-page-maker/components/preview-modal/i18n/preview-modal.pt-PT.json +6 -0
- package/src/brand-page-maker/components/rich-text-editor/RichTextEditor.vue +29 -17
- package/src/brand-page-maker/composables/useEventTracker.ts +1 -0
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +106 -0
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +40 -8
- package/src/bundles/bundled-translations.de-DE.json +5 -0
- package/src/bundles/bundled-translations.es-ES.json +5 -0
- package/src/bundles/bundled-translations.fr-CA.json +5 -0
- package/src/bundles/bundled-translations.fr-FR.json +5 -0
- package/src/bundles/bundled-translations.json +5 -0
- package/src/bundles/bundled-translations.pt-BR.json +5 -0
- package/src/bundles/bundled-translations.pt-PT.json +5 -0
- package/src/clients/brand-page-client.ts +2 -0
- package/src/composables/useTranslate.ts +2 -0
- package/src/constants/brand-page-maker.ts +5 -0
- package/src/constants/live-preview.constants.ts +15 -0
- package/src/lib.ts +5 -1
- package/src/preview-modal.ts +14 -0
- package/src/shared/common/i18n/common.de-DE.json +1 -0
- package/src/shared/common/i18n/common.es-ES.json +1 -0
- package/src/shared/common/i18n/common.fr-CA.json +1 -0
- package/src/shared/common/i18n/common.fr-FR.json +1 -0
- package/src/shared/common/i18n/common.json +1 -0
- package/src/shared/common/i18n/common.pt-BR.json +1 -0
- package/src/shared/common/i18n/common.pt-PT.json +1 -0
- package/src/shared/ecommerce-settings/store/cart-store.test.ts +38 -0
- package/lib/src/bundled-translations-2HkRBbBD.js +0 -177
- package/lib/src/bundled-translations.de-DE-CL9ZEhU2.js +0 -177
- package/lib/src/bundled-translations.es-ES-uj7Sliu0.js +0 -177
- package/lib/src/bundled-translations.fr-CA-BMnBtr_w.js +0 -177
- package/lib/src/bundled-translations.fr-FR-9PEZedzq.js +0 -177
- package/lib/src/bundled-translations.pt-BR-B3oNDkoe.js +0 -177
- package/lib/src/bundled-translations.pt-PT-DM7Vsf-h.js +0 -177
- package/lib/src/lib-CVwyHyBz.js +0 -121538
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { flushPromises, shallowMount } from '@vue/test-utils';
|
|
4
|
+
import PreviewModalWithState from './PreviewModalWithState.vue';
|
|
5
|
+
import PreviewModal from './PreviewModal.vue';
|
|
6
|
+
import {
|
|
7
|
+
useBrandPageContainerStore,
|
|
8
|
+
useUiStore,
|
|
9
|
+
} from '@brand-page-maker/stores';
|
|
10
|
+
import { UI_SCREENSIZE_MODES } from '@shared/models';
|
|
11
|
+
|
|
12
|
+
const PREVIEW_URL = 'https://brand.site/_p/request?token=signed';
|
|
13
|
+
|
|
14
|
+
const mountModal = () =>
|
|
15
|
+
shallowMount(PreviewModalWithState, {
|
|
16
|
+
props: { visible: false },
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const modalProps = (wrapper: ReturnType<typeof mountModal>) =>
|
|
20
|
+
wrapper.findComponent(PreviewModal).props();
|
|
21
|
+
|
|
22
|
+
describe('PreviewModalWithState', () => {
|
|
23
|
+
let containerStore: ReturnType<typeof useBrandPageContainerStore>;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
setActivePinia(createPinia());
|
|
27
|
+
containerStore = useBrandPageContainerStore();
|
|
28
|
+
containerStore.isProduction = false;
|
|
29
|
+
containerStore.designDraftToken = 'draft-123';
|
|
30
|
+
containerStore.livePreviewUrl = PREVIEW_URL;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('saves first, then points the iframe at the just-saved draft', async () => {
|
|
34
|
+
const save = vi
|
|
35
|
+
.spyOn(containerStore, 'saveBrandPage')
|
|
36
|
+
.mockResolvedValue({ saved: true, canPublish: true } as any);
|
|
37
|
+
|
|
38
|
+
const wrapper = mountModal();
|
|
39
|
+
|
|
40
|
+
await wrapper.setProps({ visible: true });
|
|
41
|
+
await flushPromises();
|
|
42
|
+
|
|
43
|
+
expect(save).toHaveBeenCalledOnce();
|
|
44
|
+
|
|
45
|
+
const props = modalProps(wrapper);
|
|
46
|
+
|
|
47
|
+
expect(props.loading).toBe(false);
|
|
48
|
+
expect(props.errored).toBe(false);
|
|
49
|
+
expect(props.previewUrl).toBe(PREVIEW_URL);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('saves without publishing, so previewing a live site never republishes it', async () => {
|
|
53
|
+
containerStore.brandPageStatus = 'Published';
|
|
54
|
+
|
|
55
|
+
const save = vi
|
|
56
|
+
.spyOn(containerStore, 'saveBrandPage')
|
|
57
|
+
.mockResolvedValue({ saved: true, canPublish: true } as any);
|
|
58
|
+
|
|
59
|
+
const wrapper = mountModal();
|
|
60
|
+
|
|
61
|
+
await wrapper.setProps({ visible: true });
|
|
62
|
+
await flushPromises();
|
|
63
|
+
|
|
64
|
+
expect(containerStore.isPublished).toBe(true);
|
|
65
|
+
expect(save).toHaveBeenCalledWith({ saveOnly: true });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('does not open the preview when the save fails', async () => {
|
|
69
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue(false as any);
|
|
70
|
+
|
|
71
|
+
const wrapper = mountModal();
|
|
72
|
+
|
|
73
|
+
await wrapper.setProps({ visible: true });
|
|
74
|
+
await flushPromises();
|
|
75
|
+
|
|
76
|
+
const props = modalProps(wrapper);
|
|
77
|
+
|
|
78
|
+
expect(props.errored).toBe(true);
|
|
79
|
+
expect(props.previewUrl).toBe('');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('errors when the save returns no preview url', async () => {
|
|
83
|
+
containerStore.livePreviewUrl = undefined;
|
|
84
|
+
|
|
85
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
86
|
+
saved: true,
|
|
87
|
+
canPublish: true,
|
|
88
|
+
} as any);
|
|
89
|
+
|
|
90
|
+
const wrapper = mountModal();
|
|
91
|
+
|
|
92
|
+
await wrapper.setProps({ visible: true });
|
|
93
|
+
await flushPromises();
|
|
94
|
+
|
|
95
|
+
const props = modalProps(wrapper);
|
|
96
|
+
|
|
97
|
+
expect(props.errored).toBe(true);
|
|
98
|
+
expect(props.previewUrl).toBe('');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('on a phone the preview fills the screen and the device toggle is gone', async () => {
|
|
102
|
+
useUiStore().screenSizeMode = UI_SCREENSIZE_MODES.mobile;
|
|
103
|
+
|
|
104
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
105
|
+
saved: true,
|
|
106
|
+
canPublish: true,
|
|
107
|
+
} as any);
|
|
108
|
+
|
|
109
|
+
const wrapper = mountModal();
|
|
110
|
+
|
|
111
|
+
await wrapper.setProps({ visible: true });
|
|
112
|
+
await flushPromises();
|
|
113
|
+
|
|
114
|
+
const props = modalProps(wrapper);
|
|
115
|
+
|
|
116
|
+
// Not 'mobile' — that mode draws a phone bezel, which on a phone would be a
|
|
117
|
+
// device frame inside a device.
|
|
118
|
+
expect(props.deviceMode).toBe('desktop');
|
|
119
|
+
expect(props.showDeviceToggle).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// The mobile work must not reach desktop — the toggle is still there and it
|
|
123
|
+
// still drives the frame.
|
|
124
|
+
test('desktop keeps the device toggle and honours the viewport it picks', async () => {
|
|
125
|
+
useUiStore().screenSizeMode = UI_SCREENSIZE_MODES.desktop;
|
|
126
|
+
|
|
127
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
128
|
+
saved: true,
|
|
129
|
+
canPublish: true,
|
|
130
|
+
} as any);
|
|
131
|
+
|
|
132
|
+
const wrapper = mountModal();
|
|
133
|
+
|
|
134
|
+
await wrapper.setProps({ visible: true });
|
|
135
|
+
await flushPromises();
|
|
136
|
+
|
|
137
|
+
expect(modalProps(wrapper).showDeviceToggle).toBe(true);
|
|
138
|
+
|
|
139
|
+
await wrapper
|
|
140
|
+
.findComponent(PreviewModal)
|
|
141
|
+
.vm.$emit('update:deviceMode', 'mobile');
|
|
142
|
+
await flushPromises();
|
|
143
|
+
|
|
144
|
+
expect(modalProps(wrapper).deviceMode).toBe('mobile');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// The preview sits at z-1000 and drops to z-40 for a host flow modal. Tracking
|
|
148
|
+
// only the publish modal left the "Upgrade to publish" prompt stranded behind the
|
|
149
|
+
// preview, so publishing from inside the preview couldn't be completed.
|
|
150
|
+
test.each([
|
|
151
|
+
['publish options', 'isPublishModalVisible'],
|
|
152
|
+
['upgrade prompt', 'isUpgradeModalVisible'],
|
|
153
|
+
])("steps back so the host's %s modal stacks on top", async (_, flag) => {
|
|
154
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
155
|
+
saved: true,
|
|
156
|
+
canPublish: true,
|
|
157
|
+
} as any);
|
|
158
|
+
|
|
159
|
+
const wrapper = mountModal();
|
|
160
|
+
|
|
161
|
+
await wrapper.setProps({ visible: true });
|
|
162
|
+
await flushPromises();
|
|
163
|
+
|
|
164
|
+
expect(modalProps(wrapper).flowModalOpen).toBe(false);
|
|
165
|
+
|
|
166
|
+
(containerStore as any)[flag] = true;
|
|
167
|
+
await flushPromises();
|
|
168
|
+
|
|
169
|
+
expect(modalProps(wrapper).flowModalOpen).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test('publish is delegated to the host publish flow', async () => {
|
|
173
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
174
|
+
saved: true,
|
|
175
|
+
canPublish: true,
|
|
176
|
+
} as any);
|
|
177
|
+
|
|
178
|
+
const wrapper = mountModal();
|
|
179
|
+
|
|
180
|
+
await wrapper.setProps({ visible: true });
|
|
181
|
+
await flushPromises();
|
|
182
|
+
|
|
183
|
+
await wrapper.findComponent(PreviewModal).vm.$emit('publish');
|
|
184
|
+
await flushPromises();
|
|
185
|
+
|
|
186
|
+
// The host (BrandPageMaker → bc-net) runs the actual publish flow so
|
|
187
|
+
// pre-purchase reaches checkout; the wrapper just relays the intent.
|
|
188
|
+
expect(wrapper.emitted('publish')).toHaveLength(1);
|
|
189
|
+
expect(modalProps(wrapper).publishing).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Announced on the way out, not while the preview covers the editor.
|
|
193
|
+
test('raises the deferred save banner when the preview closes', async () => {
|
|
194
|
+
vi.spyOn(containerStore, 'saveBrandPage').mockResolvedValue({
|
|
195
|
+
saved: true,
|
|
196
|
+
canPublish: true,
|
|
197
|
+
} as any);
|
|
198
|
+
|
|
199
|
+
const showSaveSuccessMessage = vi
|
|
200
|
+
.spyOn(containerStore, 'showSaveSuccessMessage')
|
|
201
|
+
.mockResolvedValue(undefined);
|
|
202
|
+
|
|
203
|
+
const wrapper = mountModal();
|
|
204
|
+
|
|
205
|
+
await wrapper.setProps({ visible: true });
|
|
206
|
+
await flushPromises();
|
|
207
|
+
|
|
208
|
+
expect(showSaveSuccessMessage).not.toHaveBeenCalled();
|
|
209
|
+
|
|
210
|
+
await wrapper.findComponent(PreviewModal).vm.$emit('close');
|
|
211
|
+
await flushPromises();
|
|
212
|
+
|
|
213
|
+
expect(wrapper.emitted('close')).toHaveLength(1);
|
|
214
|
+
expect(showSaveSuccessMessage).toHaveBeenCalledOnce();
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// BP-5957: the modal uppercases its labels on BrandCrowd, not Design.com.
|
|
218
|
+
test.each([true, false])(
|
|
219
|
+
'passes isDesignCom=%s to the modal',
|
|
220
|
+
(isDesignCom) => {
|
|
221
|
+
containerStore.isDesignCom = isDesignCom;
|
|
222
|
+
|
|
223
|
+
expect(modalProps(mountModal()).isDesignCom).toBe(isDesignCom);
|
|
224
|
+
},
|
|
225
|
+
);
|
|
226
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<PreviewModal
|
|
3
|
+
data-component="preview-modal-with-state"
|
|
4
|
+
:visible="visible"
|
|
5
|
+
:preview-url="previewUrl"
|
|
6
|
+
:edit-label="previewModalTr('edit')"
|
|
7
|
+
:cta="cta"
|
|
8
|
+
:device-mode="effectiveDeviceMode"
|
|
9
|
+
:show-device-toggle="!isMobileScreenSize"
|
|
10
|
+
:is-design-com="containerStore.isDesignCom"
|
|
11
|
+
:loading="loading"
|
|
12
|
+
:errored="errored"
|
|
13
|
+
:publishing="publishing"
|
|
14
|
+
:flow-modal-open="isFlowModalOpen"
|
|
15
|
+
@update:device-mode="deviceMode = $event"
|
|
16
|
+
@edit="onClose"
|
|
17
|
+
@publish="onPublish"
|
|
18
|
+
@retry="openPreview"
|
|
19
|
+
@close="onClose"
|
|
20
|
+
/>
|
|
21
|
+
</template>
|
|
22
|
+
|
|
23
|
+
<script setup lang="ts">
|
|
24
|
+
import { ref, computed, watch, type PropType } from 'vue';
|
|
25
|
+
import PreviewModal from './PreviewModal.vue';
|
|
26
|
+
import {
|
|
27
|
+
useBrandPageContainerStore,
|
|
28
|
+
useBrandPageMakerStore,
|
|
29
|
+
useUiStore,
|
|
30
|
+
} from '@brand-page-maker/stores';
|
|
31
|
+
import { BRAND_PAGE_TYPES } from '@brand-page';
|
|
32
|
+
import { previewModalTr } from '@src/composables/useTranslate';
|
|
33
|
+
import { UI_SCREENSIZE_MODES, type UiDeviceMode } from '@shared/models';
|
|
34
|
+
|
|
35
|
+
const props = defineProps({
|
|
36
|
+
visible: { type: Boolean, default: false },
|
|
37
|
+
cta: {
|
|
38
|
+
type: String as PropType<'publish' | 'edit' | 'none'>,
|
|
39
|
+
default: 'publish',
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const emit = defineEmits<{ close: []; publish: [] }>();
|
|
44
|
+
|
|
45
|
+
const containerStore = useBrandPageContainerStore();
|
|
46
|
+
const makerStore = useBrandPageMakerStore();
|
|
47
|
+
const uiStore = useUiStore();
|
|
48
|
+
|
|
49
|
+
const isMobileScreenSize = computed(
|
|
50
|
+
() => uiStore.screenSizeMode === UI_SCREENSIZE_MODES.mobile,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
// The preview must step back for any modal opening over the editor.
|
|
54
|
+
const isFlowModalOpen = computed(() => containerStore.isAnyModalVisible);
|
|
55
|
+
|
|
56
|
+
// brandSite is desktop-primary; brand-page and brand-contact are mobile-first.
|
|
57
|
+
const defaultDeviceMode = (): UiDeviceMode =>
|
|
58
|
+
makerStore.brandPageType === BRAND_PAGE_TYPES.brandSite
|
|
59
|
+
? 'desktop'
|
|
60
|
+
: 'mobile';
|
|
61
|
+
|
|
62
|
+
const deviceMode = ref<UiDeviceMode>(defaultDeviceMode());
|
|
63
|
+
|
|
64
|
+
const effectiveDeviceMode = computed<UiDeviceMode>(() =>
|
|
65
|
+
isMobileScreenSize.value ? 'desktop' : deviceMode.value,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const previewUrl = ref('');
|
|
69
|
+
const loading = ref(false);
|
|
70
|
+
const errored = ref(false);
|
|
71
|
+
const publishing = ref(false);
|
|
72
|
+
|
|
73
|
+
// Save first so the preview is never stale; saveOnly stops a live site republishing.
|
|
74
|
+
const openPreview = async (): Promise<void> => {
|
|
75
|
+
loading.value = true;
|
|
76
|
+
errored.value = false;
|
|
77
|
+
previewUrl.value = '';
|
|
78
|
+
|
|
79
|
+
// Pre-purchase the host owns the draft and already saved it.
|
|
80
|
+
if (!containerStore.isPrePurchase) {
|
|
81
|
+
const result = await containerStore.saveBrandPage({ saveOnly: true });
|
|
82
|
+
|
|
83
|
+
if (!result || !result.saved) {
|
|
84
|
+
errored.value = true;
|
|
85
|
+
loading.value = false;
|
|
86
|
+
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// The save hands back the signed preview URL — no second call needed.
|
|
92
|
+
const url = containerStore.livePreviewUrl;
|
|
93
|
+
|
|
94
|
+
if (!url) {
|
|
95
|
+
errored.value = true;
|
|
96
|
+
loading.value = false;
|
|
97
|
+
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
previewUrl.value = url;
|
|
102
|
+
loading.value = false;
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
// The preview covers the editor, so its save is announced on the way out. No-ops
|
|
106
|
+
// when the save wasn't ours (pre-purchase) or was already announced (publish).
|
|
107
|
+
const onClose = async (): Promise<void> => {
|
|
108
|
+
emit('close');
|
|
109
|
+
|
|
110
|
+
await containerStore.showSaveSuccessMessage();
|
|
111
|
+
await makerStore.showNextNotificationMessage();
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const onPublish = (): void => {
|
|
115
|
+
// Route through the host's publish flow so pre-purchase still reaches checkout.
|
|
116
|
+
publishing.value = true;
|
|
117
|
+
emit('publish');
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
watch(
|
|
121
|
+
() => props.visible,
|
|
122
|
+
(isVisible) => {
|
|
123
|
+
if (isVisible) {
|
|
124
|
+
deviceMode.value = defaultDeviceMode();
|
|
125
|
+
openPreview();
|
|
126
|
+
} else {
|
|
127
|
+
previewUrl.value = '';
|
|
128
|
+
errored.value = false;
|
|
129
|
+
publishing.value = false;
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Clear the publishing overlay once the host's publish flow has taken over.
|
|
135
|
+
watch(
|
|
136
|
+
() => containerStore.isSaving,
|
|
137
|
+
(saving, wasSaving) => {
|
|
138
|
+
if (publishing.value && wasSaving && !saving) {
|
|
139
|
+
publishing.value = false;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
watch(isFlowModalOpen, (isOpen) => {
|
|
145
|
+
if (publishing.value && isOpen) {
|
|
146
|
+
publishing.value = false;
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
</script>
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
<div class="style-editor">
|
|
64
64
|
<Dropdown
|
|
65
65
|
ref="textStyleTypeDropdownRef"
|
|
66
|
-
v-click-outside="(e:
|
|
66
|
+
v-click-outside="(e: Event) => clickOutsideTextStyle(e)"
|
|
67
67
|
data-test-tag="text-style-dropdown"
|
|
68
68
|
class="style-editor-button style-editor-button-textstyle"
|
|
69
69
|
:title="
|
|
@@ -96,7 +96,7 @@
|
|
|
96
96
|
</Dropdown>
|
|
97
97
|
<Dropdown
|
|
98
98
|
ref="fontStyleDropdownRef"
|
|
99
|
-
v-click-outside="(e:
|
|
99
|
+
v-click-outside="(e: Event) => clickOutsideFontStyle(e)"
|
|
100
100
|
data-test-tag="font-style-dropdown"
|
|
101
101
|
:title="fontSettings.selectedFont"
|
|
102
102
|
icon-size="xs"
|
|
@@ -305,7 +305,7 @@
|
|
|
305
305
|
<div
|
|
306
306
|
v-if="isSpacingDropdownOpen"
|
|
307
307
|
ref="spacingPopupRef"
|
|
308
|
-
v-click-outside="(e:
|
|
308
|
+
v-click-outside="(e: Event) => clickOutsideTextSpacing(e)"
|
|
309
309
|
class="popup-container tw-max-w-[310px] spacing-popup"
|
|
310
310
|
data-test-tag="spacing-toolbar"
|
|
311
311
|
>
|
|
@@ -335,7 +335,7 @@
|
|
|
335
335
|
<div
|
|
336
336
|
v-if="isHyperlinkPopupOpen"
|
|
337
337
|
ref="linkPopupRef"
|
|
338
|
-
v-click-outside="(e:
|
|
338
|
+
v-click-outside="(e: Event) => clickOutsideHyperlink(e)"
|
|
339
339
|
class="popup-container tw-w-[300px] hyperlink-popup"
|
|
340
340
|
data-test-tag="hyperlink-toolbar"
|
|
341
341
|
>
|
|
@@ -886,31 +886,43 @@ const onSelectTextStyleType = (option: ButtonGroupOption) => {
|
|
|
886
886
|
textStyleTypeDropdownRef.value?.hideMenu();
|
|
887
887
|
};
|
|
888
888
|
|
|
889
|
-
// Event handlers for dropdowns and popups
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
889
|
+
// Event handlers for dropdowns and popups.
|
|
890
|
+
//
|
|
891
|
+
// `v-click-outside` also fires these for a WINDOW BLUR, not just a click: its
|
|
892
|
+
// `detectIframe` option (on by default) listens for blur to catch "focus moved into
|
|
893
|
+
// an iframe", and hands the blur event straight to this handler. A blur event's
|
|
894
|
+
// target is `window`, which has no `closest` — so reading it as an element threw
|
|
895
|
+
// `target.closest is not a function`. Latent until the editor gained an iframe (the
|
|
896
|
+
// live preview), which is exactly the case `detectIframe` exists to report.
|
|
897
|
+
//
|
|
898
|
+
// Focus leaving for an iframe IS outside the toolbar, so a non-element target must
|
|
899
|
+
// close the menu, not bail out.
|
|
900
|
+
const isOutside = (event: Event, selector: string): boolean => {
|
|
901
|
+
const { target } = event;
|
|
902
|
+
|
|
903
|
+
return !(target instanceof Element) || !target.closest(selector);
|
|
904
|
+
};
|
|
905
|
+
|
|
906
|
+
const clickOutsideTextStyle = (event: Event) => {
|
|
907
|
+
if (isOutside(event, '.toolbar-editor-button')) {
|
|
893
908
|
textStyleTypeDropdownRef.value?.hideMenu();
|
|
894
909
|
}
|
|
895
910
|
};
|
|
896
911
|
|
|
897
|
-
const clickOutsideFontStyle = (event:
|
|
898
|
-
|
|
899
|
-
if (!target.closest('.toolbar-editor-button')) {
|
|
912
|
+
const clickOutsideFontStyle = (event: Event) => {
|
|
913
|
+
if (isOutside(event, '.toolbar-editor-button')) {
|
|
900
914
|
fontStyleDropdownRef.value?.hideMenu();
|
|
901
915
|
}
|
|
902
916
|
};
|
|
903
917
|
|
|
904
|
-
const clickOutsideTextSpacing = (event:
|
|
905
|
-
|
|
906
|
-
if (!target.closest('.spacing-popup')) {
|
|
918
|
+
const clickOutsideTextSpacing = (event: Event) => {
|
|
919
|
+
if (isOutside(event, '.spacing-popup')) {
|
|
907
920
|
isSpacingDropdownOpen.value = false;
|
|
908
921
|
}
|
|
909
922
|
};
|
|
910
923
|
|
|
911
|
-
const clickOutsideHyperlink = (event:
|
|
912
|
-
|
|
913
|
-
if (!target.closest('.hyperlink-popup')) {
|
|
924
|
+
const clickOutsideHyperlink = (event: Event) => {
|
|
925
|
+
if (isOutside(event, '.hyperlink-popup')) {
|
|
914
926
|
isHyperlinkPopupOpen.value = false;
|
|
915
927
|
}
|
|
916
928
|
};
|
|
@@ -92,6 +92,7 @@ export const UPGRADE_CTA_LOCATIONS = {
|
|
|
92
92
|
ADD_CONTENT: 'add_content',
|
|
93
93
|
EDITOR_PUBLISH_TOP: 'editor_publish_top',
|
|
94
94
|
EDITOR_PUBLISH_BOTTOM: 'editor_publish_bottom',
|
|
95
|
+
EDITOR_PREVIEW_PUBLISH: 'editor_preview_publish',
|
|
95
96
|
SLUG_CHANGE_MAKER_SIDEBAR_SETTINGS: 'slug_change_maker_sidebar_settings',
|
|
96
97
|
SLUG_CHANGE_MANAGE_PAGE: 'slug_change_manage_page',
|
|
97
98
|
GOOGLE_ADSENSE: 'google_adsense',
|
package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { useBrandPageContainerStore } from '@brand-page-maker/stores';
|
|
4
|
+
import brandPageClient from '../../../clients/brand-page-client';
|
|
5
|
+
|
|
6
|
+
vi.mock('../../../clients/brand-page-client', () => ({
|
|
7
|
+
default: {
|
|
8
|
+
saveBrandPageAsync: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const SAVED = {
|
|
13
|
+
designDraftToken: 'draft-123',
|
|
14
|
+
status: 'Published',
|
|
15
|
+
slug: 'my-site',
|
|
16
|
+
canPublish: true,
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('brand-page-container-store saveBrandPage', () => {
|
|
20
|
+
let store: ReturnType<typeof useBrandPageContainerStore>;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
vi.clearAllMocks();
|
|
24
|
+
setActivePinia(createPinia());
|
|
25
|
+
|
|
26
|
+
store = useBrandPageContainerStore();
|
|
27
|
+
store.designDraftToken = 'draft-123';
|
|
28
|
+
store.brandPageStatus = 'Published';
|
|
29
|
+
|
|
30
|
+
// The export is exercised by its own tests; stub it so these cover only the
|
|
31
|
+
// endpoint choice.
|
|
32
|
+
vi.spyOn(store, 'exportDesignAsync').mockResolvedValue({
|
|
33
|
+
shouldPublish: true,
|
|
34
|
+
} as any);
|
|
35
|
+
|
|
36
|
+
vi.mocked(brandPageClient.saveBrandPageAsync).mockResolvedValue(
|
|
37
|
+
SAVED as any,
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const requestBody = () =>
|
|
42
|
+
vi.mocked(brandPageClient.saveBrandPageAsync).mock.calls[0][1];
|
|
43
|
+
|
|
44
|
+
test('saveOnly asks a published site not to republish', async () => {
|
|
45
|
+
await store.saveBrandPage({ saveOnly: true });
|
|
46
|
+
|
|
47
|
+
expect(brandPageClient.saveBrandPageAsync).toHaveBeenCalledOnce();
|
|
48
|
+
expect(requestBody()).toMatchObject({ saveOnly: true });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('a normal save leaves saveOnly off', async () => {
|
|
52
|
+
await store.saveBrandPage();
|
|
53
|
+
|
|
54
|
+
expect(requestBody()).toMatchObject({ saveOnly: false });
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// The preview covers the editor, so the banner would land behind it. The trigger
|
|
58
|
+
// stays UP so the preview can raise it on close.
|
|
59
|
+
test('saveOnly defers the save-success banner instead of raising it', async () => {
|
|
60
|
+
const showSaveSuccessMessage = vi
|
|
61
|
+
.spyOn(store, 'showSaveSuccessMessage')
|
|
62
|
+
.mockResolvedValue(undefined);
|
|
63
|
+
|
|
64
|
+
await store.saveBrandPage({ saveOnly: true });
|
|
65
|
+
|
|
66
|
+
expect(showSaveSuccessMessage).not.toHaveBeenCalled();
|
|
67
|
+
expect(store.triggerSaveSuccessMessage).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('a normal save still raises the banner', async () => {
|
|
71
|
+
const showSaveSuccessMessage = vi
|
|
72
|
+
.spyOn(store, 'showSaveSuccessMessage')
|
|
73
|
+
.mockResolvedValue(undefined);
|
|
74
|
+
|
|
75
|
+
await store.saveBrandPage();
|
|
76
|
+
|
|
77
|
+
expect(showSaveSuccessMessage).toHaveBeenCalled();
|
|
78
|
+
expect(store.triggerSaveSuccessMessage).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// The live preview is a full-viewport takeover that steps back on this getter, so a
|
|
82
|
+
// modal missing from it opens BEHIND the preview — the bug this replaced, three times.
|
|
83
|
+
test.each([
|
|
84
|
+
['isPublishModalVisible'],
|
|
85
|
+
['isUpgradeModalVisible'],
|
|
86
|
+
['isContextualUpgradeModalVisible'],
|
|
87
|
+
['isStripeOnboardingModalVisible'],
|
|
88
|
+
['isHostModalVisible'],
|
|
89
|
+
])('isAnyModalVisible covers %s', (flag) => {
|
|
90
|
+
expect(store.isAnyModalVisible).toBe(false);
|
|
91
|
+
|
|
92
|
+
store[flag] = true;
|
|
93
|
+
|
|
94
|
+
expect(store.isAnyModalVisible).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('saveOnly does not change shouldPublish', async () => {
|
|
98
|
+
await store.saveBrandPage({ saveOnly: true });
|
|
99
|
+
|
|
100
|
+
// `shouldPublish` stays derived from isPublished. Sending false would flip
|
|
101
|
+
// BrandPageService's `!shouldPublish` branch and start firing a
|
|
102
|
+
// "brand page saved" marketing event on every preview open.
|
|
103
|
+
expect(store.exportDesignAsync).toHaveBeenCalledWith();
|
|
104
|
+
expect(requestBody()).toMatchObject({ shouldPublish: true });
|
|
105
|
+
});
|
|
106
|
+
});
|