@designcrowd/library.brand-page 6.1.16 → 6.1.17-ast-header-button
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-CmNhDMIf.js → CreatedOnBrandCrowd-de-de-Cwq9enJs.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DgplxxWa.js → CreatedOnBrandCrowd-en-us-ILK04gFj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DFdhFOa1.js → CreatedOnBrandCrowd-es-es-CbjxLxtu.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-DjLCWBLB.js → CreatedOnBrandCrowd-fr-ca-BwBDiK9z.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-DjLCWBLB.js → CreatedOnBrandCrowd-fr-fr-BwBDiK9z.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CI2XD0OS.js → CreatedOnBrandCrowd-pt-br-DvhJROpG.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-CI2XD0OS.js → CreatedOnBrandCrowd-pt-pt-DvhJROpG.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CkLkBztx.js → CreatedOnDcom-de-de-BK9imWOh.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-frnxBJJv.js → CreatedOnDcom-en-us-CyH-_qz1.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CrqgiERj.js → CreatedOnDcom-es-es-Dh51qYEA.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CPoEcusj.js → CreatedOnDcom-fr-ca-BArRxwWX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-BqrRH3dV.js → CreatedOnDcom-fr-fr-CdglI-ZB.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-CpbVi3Sw.js → CreatedOnDcom-pt-br-DCehns-G.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-s0XZ3c7y.js → CreatedOnDcom-pt-pt-B8hl-oew.js} +1 -1
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.d.ts +27 -2
- package/lib/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.js +26 -0
- package/lib/src/brand-page/components/page-header/PageHeader.mixin.d.ts +3 -1
- package/lib/src/brand-page/components/page-header/PageHeader.mixin.js +12 -0
- package/lib/src/brand-page/models/index.d.ts +12 -0
- package/lib/src/brand-page/models/index.js +3 -0
- package/lib/src/brand-page/utils/button.util.d.ts +5 -0
- package/lib/src/brand-page/utils/button.util.js +4 -0
- package/lib/src/brand-page/utils/view-model.util.d.ts +1 -0
- package/lib/src/brand-page/utils/view-model.util.js +51 -1
- package/lib/src/brand-page/view-models.d.ts +3 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +1008 -0
- package/lib/src/{bundled-translations-B2q5GWTQ.js → bundled-translations-B8fAEel0.js} +1 -1
- package/lib/src/bundles/bundled-translations.json +7 -1
- package/lib/src/css/library.brand-page-brandCrowd.min.css +315 -122
- package/lib/src/css/library.brand-page-brandPage.min.css +315 -122
- package/lib/src/css/library.brand-page-designCom.min.css +315 -122
- package/lib/src/css/library.brand-page-preview.min.css +315 -122
- package/lib/src/index.d.ts +1 -0
- package/lib/src/index.mjs +45 -44
- package/lib/src/{lib-O_eJgJHP.js → lib-KFjPFfxn.js} +31522 -31118
- package/lib/src/lib.js +1 -0
- package/lib/src/shared/content-settings/components/content-settings/ContentSettings.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings/ReplaceImageSettingsForContent.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/content-settings-item/ContentSettingsItemField.mixin.d.ts +1 -1
- package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +1 -1
- package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.d.ts +11 -0
- package/lib/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.js +80 -0
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/delete-page-modal/DeletePageModal.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/new-page-modal/NewPageModal.mixin.d.ts +1 -1
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +113 -1
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +117 -2
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.js +43 -1
- package/lib/src/shared/page-settings/store/page-settings-store.d.ts +2 -2
- package/package.json +1 -1
- package/src/brand-page/components/button/Button.vue +35 -8
- package/src/brand-page/components/button/Button.vue.test.ts +59 -0
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.mixin.ts +29 -1
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue +36 -0
- package/src/brand-page/components/navigation/navigation-side-menu/NavigationSideMenu.vue.test.ts +175 -0
- package/src/brand-page/components/page/Page.vue +6 -0
- package/src/brand-page/components/page-header/HeaderLayout.vue +198 -4
- package/src/brand-page/components/page-header/PageHeader.mixin.ts +16 -1
- package/src/brand-page/components/page-header/PageHeader.vue +23 -1
- package/src/brand-page/components/page-header/PageHeader.vue.test.ts +378 -0
- package/src/brand-page/models/header-button.model.test.ts +86 -0
- package/src/brand-page/models/index.ts +16 -0
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +6 -0
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +2 -0
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +4 -0
- package/src/brand-page/utils/button.util.ts +8 -0
- package/src/brand-page/utils/view-model.util.test.ts +146 -1
- package/src/brand-page/utils/view-model.util.ts +71 -1
- package/src/brand-page/view-models.ts +3 -0
- package/src/bundles/bundled-translations.json +7 -1
- package/src/lib.ts +1 -0
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.test.ts +399 -0
- package/src/shared/ecommerce-settings/util/refreshDuplicatedMetadata.ts +99 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.button-section.test.ts +126 -0
- package/src/shared/page-settings/components/header-settings-panel/HeaderSettingsPanel.vue +43 -6
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.test.ts +167 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue +237 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts +160 -0
- package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue +37 -8
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.header-button.test.ts +113 -0
- package/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.ts +47 -0
- package/src/shared/page-settings/components/page-settings-section-styles/i18n/page-settings-section.json +7 -1
- package/src/shared/page-settings/store/page-settings-store.ts +2 -0
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
import type { BrandPageModel, Content } from '@brand-page/models';
|
|
3
|
+
import type { ProductModel } from '@brand-page/models/shop.model';
|
|
4
|
+
import type { PaymentConfig } from '@brand-page/models/payment.model';
|
|
5
|
+
import { isPayment, isShopContent, isSupportMe } from '@brand-page/utils';
|
|
6
|
+
import { generateSku } from './skuUtils';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Refresh a (migrated, current-version) BrandPageModel so it is safe to persist as a *duplicate*.
|
|
10
|
+
* Two things travel with a verbatim copy that must not: client-minted identifiers that the
|
|
11
|
+
* brand-page-api keys a global row on, and the source owner's embedded payment connection. This
|
|
12
|
+
* rotates the former to fresh values and removes the latter, in place — the new owner reconnects
|
|
13
|
+
* a payment provider explicitly. Runs after migration, so it only ever sees the current shape.
|
|
14
|
+
*
|
|
15
|
+
* See docs/architecture/capabilities/shop-duplication.md.
|
|
16
|
+
*/
|
|
17
|
+
export function refreshDuplicatedMetadata(
|
|
18
|
+
model: BrandPageModel,
|
|
19
|
+
): BrandPageModel {
|
|
20
|
+
// Site-level payment connection: the authoritative one at checkout — `initialiseShopStore` prefers
|
|
21
|
+
// it over any content-level config — so it leaks the source owner's account even after the
|
|
22
|
+
// content-level connections below are cleared. Disconnect it too; the new owner reconnects.
|
|
23
|
+
model.paymentConfig = undefined;
|
|
24
|
+
|
|
25
|
+
model.pages.forEach((page) =>
|
|
26
|
+
page.contents.forEach((content) => {
|
|
27
|
+
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
|
+
disconnectPayment(content.shopPaymentConfig);
|
|
32
|
+
} else if (isPayment(content)) {
|
|
33
|
+
resetPaymentBlock(content, content.requestPaymentConfig);
|
|
34
|
+
} else if (isSupportMe(content)) {
|
|
35
|
+
resetPaymentBlock(content, content.supportMeConfig);
|
|
36
|
+
}
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return model;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rotateProductIds(product: ProductModel): void {
|
|
44
|
+
product.id = uuid();
|
|
45
|
+
|
|
46
|
+
// The option id is the only value cross-referenced inside a product — SKUs point at product
|
|
47
|
+
// options by id and each skuKey embeds it — so it is remapped through one old→new map and
|
|
48
|
+
// reused everywhere it reappears. Every other id is standalone and just gets a fresh uuid.
|
|
49
|
+
const optionIdMap = new Map<string, string>();
|
|
50
|
+
|
|
51
|
+
product.variant.forEach((variant) => {
|
|
52
|
+
variant.id = uuid();
|
|
53
|
+
variant.options.forEach((option) => {
|
|
54
|
+
const newId = uuid();
|
|
55
|
+
|
|
56
|
+
optionIdMap.set(option.id, newId);
|
|
57
|
+
option.id = newId;
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
product.skus.forEach((sku) => {
|
|
62
|
+
sku.id = uuid();
|
|
63
|
+
|
|
64
|
+
sku.variant.forEach((skuVariant) => {
|
|
65
|
+
skuVariant.id = uuid();
|
|
66
|
+
skuVariant.option.id = optionIdMap.get(skuVariant.option.id) ?? uuid();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Regenerate from the now-remapped option so the key embeds the new option id and a fresh
|
|
70
|
+
// random suffix — this is what keeps sku_key globally unique on the copy's first upsert.
|
|
71
|
+
sku.skuKey = generateSku(product.name, sku.variant[0]?.option);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 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.
|
|
80
|
+
*/
|
|
81
|
+
function resetPaymentBlock(content: Content, config: PaymentConfig): void {
|
|
82
|
+
content.id = uuid();
|
|
83
|
+
|
|
84
|
+
config.paymentOptions.forEach((option) => {
|
|
85
|
+
option.id = uuid();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
disconnectPayment(config);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Drop the embedded payment connection (the source owner's payment/Stripe account). The section
|
|
93
|
+
* keeps its options and amounts but reverts to "not connected"; the new owner reconnects.
|
|
94
|
+
*/
|
|
95
|
+
function disconnectPayment(config: PaymentConfig | undefined): void {
|
|
96
|
+
if (config) {
|
|
97
|
+
config.paymentConfig = undefined;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { shallowMount } from '@vue/test-utils';
|
|
4
|
+
import type { VueWrapper } from '@vue/test-utils';
|
|
5
|
+
import { Toggle } from '@designcrowd/fe-shared-lib';
|
|
6
|
+
import type { LogoTheme } from '@brand-page';
|
|
7
|
+
import { VIEWPORTS } from '@brand-page';
|
|
8
|
+
import HeaderSettingsPanel from './HeaderSettingsPanel.vue';
|
|
9
|
+
import HeaderButtonSettings from '../page-settings-section-styles/HeaderButtonSettings.vue';
|
|
10
|
+
import { useUiPageSettingsStore } from '../../store';
|
|
11
|
+
import { FabricRenderClient } from '../../../../clients/fabric-render-client';
|
|
12
|
+
|
|
13
|
+
// BP-5852: the header call-to-action button is brandsite-only, its toggle lives
|
|
14
|
+
// in the Button section's HEADER (final design), and every write syncs both
|
|
15
|
+
// viewports.
|
|
16
|
+
describe('HeaderSettingsPanel — Button section', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
setActivePinia(createPinia());
|
|
19
|
+
// The Logo section renders alongside the Button one and resolves logo URLs
|
|
20
|
+
// through the client singleton.
|
|
21
|
+
FabricRenderClient.setInstance({ baseUrl: 'http://localhost:3000' });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const CollapsiblePanelStub = {
|
|
25
|
+
template: '<div><slot name="header-actions" /><slot /></div>',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mountPanel = (isBrandSite: boolean) =>
|
|
29
|
+
shallowMount(HeaderSettingsPanel, {
|
|
30
|
+
props: {
|
|
31
|
+
isAdminMaker: false,
|
|
32
|
+
isBrandSite,
|
|
33
|
+
logoTheme: {} as LogoTheme,
|
|
34
|
+
sectionName: 'header',
|
|
35
|
+
styleOptions: [],
|
|
36
|
+
},
|
|
37
|
+
global: {
|
|
38
|
+
// The sections live inside CollapsiblePanel slots, which a plain
|
|
39
|
+
// shallow stub would swallow — render both the header-actions slot
|
|
40
|
+
// (the toggle) and the body.
|
|
41
|
+
stubs: { CollapsiblePanel: CollapsiblePanelStub },
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const buttonToggle = (wrapper: ReturnType<typeof mountPanel>) =>
|
|
46
|
+
wrapper
|
|
47
|
+
.findAllComponents(Toggle)
|
|
48
|
+
.find(
|
|
49
|
+
(t) => t.attributes('data-test-header-button-toggle') !== undefined,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
test('brandsite: the header toggle renders, off, and the fields stay hidden', () => {
|
|
53
|
+
const wrapper = mountPanel(true);
|
|
54
|
+
|
|
55
|
+
expect(buttonToggle(wrapper)).toBeDefined();
|
|
56
|
+
// the stubbed Toggle carries its props as attributes
|
|
57
|
+
expect(buttonToggle(wrapper)?.attributes('model-value')).toBe('false');
|
|
58
|
+
expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('brand page / brand contact: no Button section at all', () => {
|
|
62
|
+
const wrapper = mountPanel(false);
|
|
63
|
+
|
|
64
|
+
expect(buttonToggle(wrapper)).toBeUndefined();
|
|
65
|
+
expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('toggling on writes isVisible to BOTH viewports and reveals the fields', async () => {
|
|
69
|
+
const wrapper = mountPanel(true);
|
|
70
|
+
|
|
71
|
+
await buttonToggle(wrapper)?.vm.$emit('update:model-value', true);
|
|
72
|
+
await wrapper.vm.$nextTick();
|
|
73
|
+
|
|
74
|
+
const sections = useUiPageSettingsStore().sections.header;
|
|
75
|
+
|
|
76
|
+
expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
|
|
77
|
+
expect(sections[VIEWPORTS.mobile].headerButton?.isVisible).toBe(true);
|
|
78
|
+
expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('expanding the collapsed section via the chevron switches the button on', async () => {
|
|
82
|
+
const wrapper = mountPanel(true);
|
|
83
|
+
|
|
84
|
+
// the Button panel is the last collapsible section
|
|
85
|
+
const panels = wrapper.findAllComponents(CollapsiblePanelStub);
|
|
86
|
+
const buttonPanel = panels[panels.length - 1] as VueWrapper;
|
|
87
|
+
|
|
88
|
+
await buttonPanel.vm.$emit('changed');
|
|
89
|
+
await wrapper.vm.$nextTick();
|
|
90
|
+
|
|
91
|
+
const sections = useUiPageSettingsStore().sections.header;
|
|
92
|
+
|
|
93
|
+
expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
|
|
94
|
+
expect(sections[VIEWPORTS.mobile].headerButton?.isVisible).toBe(true);
|
|
95
|
+
expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(true);
|
|
96
|
+
|
|
97
|
+
// collapsing again must NOT switch it off
|
|
98
|
+
await buttonPanel.vm.$emit('changed');
|
|
99
|
+
await wrapper.vm.$nextTick();
|
|
100
|
+
|
|
101
|
+
expect(sections[VIEWPORTS.desktop].headerButton?.isVisible).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('toggling off hides the fields but keeps the configuration for a re-enable', async () => {
|
|
105
|
+
const wrapper = mountPanel(true);
|
|
106
|
+
|
|
107
|
+
await buttonToggle(wrapper)?.vm.$emit('update:model-value', true);
|
|
108
|
+
// simulate a label edit, which the mixin dual-writes to both viewports
|
|
109
|
+
[VIEWPORTS.desktop, VIEWPORTS.mobile].forEach((viewport) => {
|
|
110
|
+
useUiPageSettingsStore().setPageSettingsSectionValue({
|
|
111
|
+
sectionName: 'header',
|
|
112
|
+
propName: 'headerButton',
|
|
113
|
+
value: { isVisible: true, text: 'Book now', buttonLinkType: 'url' },
|
|
114
|
+
viewport,
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
await buttonToggle(wrapper)?.vm.$emit('update:model-value', false);
|
|
118
|
+
await wrapper.vm.$nextTick();
|
|
119
|
+
|
|
120
|
+
const desktop = useUiPageSettingsStore().sections.header[VIEWPORTS.desktop];
|
|
121
|
+
|
|
122
|
+
expect(wrapper.findComponent(HeaderButtonSettings).exists()).toBe(false);
|
|
123
|
+
expect(desktop.headerButton?.isVisible).toBe(false);
|
|
124
|
+
expect(desktop.headerButton?.text).toBe('Book now');
|
|
125
|
+
});
|
|
126
|
+
});
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
:on-select="handleHeaderDisplayStyleChange"
|
|
65
65
|
:viewport="viewport"
|
|
66
66
|
:is-logo-visible="isLogoVisible"
|
|
67
|
+
:show-header-button="isHeaderButtonVisible"
|
|
67
68
|
/>
|
|
68
69
|
|
|
69
70
|
<NavigationStyleSettings
|
|
@@ -144,16 +145,29 @@
|
|
|
144
145
|
</div>
|
|
145
146
|
</CollapsiblePanel>
|
|
146
147
|
|
|
147
|
-
<!-- Button section hidden for now (BP-5854) — re-enable once BP-5852 implements
|
|
148
|
-
the header button. Left commented rather than removed since the shell
|
|
149
|
-
(CollapsiblePanel + collapsedSections.button state) is already wired up.
|
|
150
148
|
<CollapsiblePanel
|
|
149
|
+
v-if="isBrandSite"
|
|
151
150
|
data-component="collapsible-panel"
|
|
152
151
|
:title="pageSettingsTr('buttonSectionTitle')"
|
|
153
152
|
:is-collapsed="collapsedSections.button"
|
|
154
|
-
@changed="
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
@changed="onButtonSectionChanged"
|
|
154
|
+
>
|
|
155
|
+
<template #header-actions>
|
|
156
|
+
<Toggle
|
|
157
|
+
id="header-button-visible"
|
|
158
|
+
data-test-header-button-toggle
|
|
159
|
+
:model-value="isHeaderButtonVisible"
|
|
160
|
+
@update:model-value="onHeaderButtonToggle"
|
|
161
|
+
/>
|
|
162
|
+
</template>
|
|
163
|
+
<HeaderButtonSettings
|
|
164
|
+
v-if="isHeaderButtonVisible"
|
|
165
|
+
:value="headerButton"
|
|
166
|
+
:is-admin-maker="isAdminMaker"
|
|
167
|
+
:logo-theme="logoTheme"
|
|
168
|
+
@on-change="onHeaderButtonChange"
|
|
169
|
+
/>
|
|
170
|
+
</CollapsiblePanel>
|
|
157
171
|
</div>
|
|
158
172
|
</template>
|
|
159
173
|
<script>
|
|
@@ -165,6 +179,7 @@ import {
|
|
|
165
179
|
Toggle,
|
|
166
180
|
} from '@designcrowd/fe-shared-lib';
|
|
167
181
|
import PageSettingsSectionStyleMixin from '../page-settings-section-styles/PageSettingsSectionStyles.mixin.ts';
|
|
182
|
+
import HeaderButtonSettings from '../page-settings-section-styles/HeaderButtonSettings.vue';
|
|
168
183
|
import HeaderDisplayStyle from '../page-settings-section-styles/HeaderDisplayStyle.vue';
|
|
169
184
|
import HeaderNavigationType from '../page-settings-section-styles/HeaderNavigationType.vue';
|
|
170
185
|
import HeaderNavigationStyle from '../page-settings-section-styles/HeaderNavigationStyle.vue';
|
|
@@ -183,6 +198,7 @@ export default {
|
|
|
183
198
|
Slider,
|
|
184
199
|
TabMenu,
|
|
185
200
|
Toggle,
|
|
201
|
+
HeaderButtonSettings,
|
|
186
202
|
HeaderDisplayStyle,
|
|
187
203
|
HeaderNavigationType,
|
|
188
204
|
HeaderNavigationStyle,
|
|
@@ -222,6 +238,27 @@ export default {
|
|
|
222
238
|
toggleSectionCollapsed(section) {
|
|
223
239
|
this.collapsedSections[section] = !this.collapsedSections[section];
|
|
224
240
|
},
|
|
241
|
+
onButtonSectionChanged() {
|
|
242
|
+
const isExpanding = this.collapsedSections.button;
|
|
243
|
+
|
|
244
|
+
this.toggleSectionCollapsed('button');
|
|
245
|
+
|
|
246
|
+
// Expanding while the button is off would show an empty body — opening the
|
|
247
|
+
// section IS the intent to configure it, so switch the button on. Collapsing
|
|
248
|
+
// never switches it off.
|
|
249
|
+
if (isExpanding && !this.isHeaderButtonVisible) {
|
|
250
|
+
this.onHeaderButtonChange({ isVisible: true });
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
onHeaderButtonToggle(isVisible) {
|
|
254
|
+
this.onHeaderButtonChange({ isVisible });
|
|
255
|
+
|
|
256
|
+
// Enabling the button is what makes the section worth looking at — open it
|
|
257
|
+
// so the fields appear with the flick of the toggle (matches the design).
|
|
258
|
+
if (isVisible) {
|
|
259
|
+
this.collapsedSections.button = false;
|
|
260
|
+
}
|
|
261
|
+
},
|
|
225
262
|
},
|
|
226
263
|
};
|
|
227
264
|
</script>
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { shallowMount } from '@vue/test-utils';
|
|
4
|
+
import type { VueWrapper } from '@vue/test-utils';
|
|
5
|
+
import type { HeaderButton, LogoTheme } from '@brand-page';
|
|
6
|
+
import HeaderButtonSettings from './HeaderButtonSettings.vue';
|
|
7
|
+
import { useUiPageSettingsStore } from '../../store';
|
|
8
|
+
import { useUiStyleSettingsStore } from '../../../style-settings/store/style-settings-store';
|
|
9
|
+
import { availableButtonStyleConfigs } from '@brand-page-maker/models';
|
|
10
|
+
import type { ContentPage } from '../../models';
|
|
11
|
+
|
|
12
|
+
// A string selector types as WrapperLike, which hides `vm` — the fields are
|
|
13
|
+
// located by their data-test hook rather than by component identity (two of
|
|
14
|
+
// them are ColorFields).
|
|
15
|
+
const emitFrom = (
|
|
16
|
+
wrapper: VueWrapper,
|
|
17
|
+
hook: string,
|
|
18
|
+
payload: unknown,
|
|
19
|
+
): void => {
|
|
20
|
+
(wrapper.findComponent(hook) as unknown as VueWrapper).vm.$emit(
|
|
21
|
+
'update:modelValue',
|
|
22
|
+
payload,
|
|
23
|
+
);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// BP-5852: the Button panel is a toggle plus the fields it gates. Everything
|
|
27
|
+
// below the toggle renders only while it's on, and every edit leaves through
|
|
28
|
+
// one `on-change` patch (the mixin dual-writes it to both viewports).
|
|
29
|
+
describe('HeaderButtonSettings', () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
setActivePinia(createPinia());
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const mountPanel = (value?: HeaderButton) =>
|
|
35
|
+
shallowMount(HeaderButtonSettings, {
|
|
36
|
+
props: {
|
|
37
|
+
value,
|
|
38
|
+
isAdminMaker: false,
|
|
39
|
+
logoTheme: {} as LogoTheme,
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const enabled: HeaderButton = {
|
|
44
|
+
isVisible: true,
|
|
45
|
+
text: '',
|
|
46
|
+
buttonLinkType: 'url',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const fieldHooks = [
|
|
50
|
+
'[data-test-header-button-text]',
|
|
51
|
+
'[data-test-header-button-link-type]',
|
|
52
|
+
'[data-test-header-button-text-style]',
|
|
53
|
+
'[data-test-header-button-fill-color]',
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
// The show/hide toggle lives in the panel HEADER (HeaderSettingsPanel), which
|
|
57
|
+
// also gates this whole component — so every field renders unconditionally here.
|
|
58
|
+
test('every field renders', () => {
|
|
59
|
+
const wrapper = mountPanel(enabled);
|
|
60
|
+
|
|
61
|
+
fieldHooks.forEach((hook) => {
|
|
62
|
+
expect(wrapper.find(hook).exists()).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// Border colour is capability-gated on the site's selected button style, like the
|
|
67
|
+
// global Style panel: a style without a border would make the control inert.
|
|
68
|
+
test('the border colour field is hidden while the selected button style draws no border', () => {
|
|
69
|
+
// default selectedButtonStyleId is BUTTON_STYLE_1, whose options carry no border
|
|
70
|
+
const wrapper = mountPanel(enabled);
|
|
71
|
+
|
|
72
|
+
expect(
|
|
73
|
+
wrapper.find('[data-test-header-button-border-color]').exists(),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test('the border colour field renders when the selected button style has a border', () => {
|
|
78
|
+
useUiStyleSettingsStore().$patch(($state) => {
|
|
79
|
+
// availableButtonStyleConfigs starts empty until the store initialises
|
|
80
|
+
$state.availableButtonStyleConfigs = availableButtonStyleConfigs;
|
|
81
|
+
$state.buttonStyles.selectedButtonStyleId = 'BUTTON_STYLE_4';
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const wrapper = mountPanel(enabled);
|
|
85
|
+
|
|
86
|
+
expect(
|
|
87
|
+
wrapper.find('[data-test-header-button-border-color]').exists(),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('the link-value field follows the selected link kind', () => {
|
|
92
|
+
const kinds = {
|
|
93
|
+
url: '[data-test-header-button-url]',
|
|
94
|
+
page: '[data-test-header-button-page]',
|
|
95
|
+
mailTo: '[data-test-header-button-email]',
|
|
96
|
+
tel: '[data-test-header-button-tel]',
|
|
97
|
+
} as const;
|
|
98
|
+
|
|
99
|
+
Object.entries(kinds).forEach(([kind, hook]) => {
|
|
100
|
+
const wrapper = mountPanel({
|
|
101
|
+
...enabled,
|
|
102
|
+
buttonLinkType: kind as HeaderButton['buttonLinkType'],
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
Object.values(kinds).forEach((otherHook) => {
|
|
106
|
+
expect(wrapper.find(otherHook).exists()).toBe(otherHook === hook);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const linkTypeValues = (wrapper: VueWrapper): string[] => {
|
|
112
|
+
const group = wrapper.findComponent(
|
|
113
|
+
'[data-test-header-button-link-type]',
|
|
114
|
+
) as unknown as { props(name: string): unknown };
|
|
115
|
+
|
|
116
|
+
return (group.props('options') as { value: string }[]).map(
|
|
117
|
+
(option) => option.value,
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
test('the four link kinds are offered in a 2-column group', () => {
|
|
122
|
+
const wrapper = mountPanel(enabled);
|
|
123
|
+
|
|
124
|
+
expect(linkTypeValues(wrapper)).toEqual(['url', 'page', 'mailTo', 'tel']);
|
|
125
|
+
expect(
|
|
126
|
+
wrapper.find('[data-test-header-button-link-type]').attributes('columns'),
|
|
127
|
+
).toBe('2');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('a single-page site drops the Page kind (buttonLinkStyleConvertor)', () => {
|
|
131
|
+
useUiPageSettingsStore().setPages({
|
|
132
|
+
pages: [{ id: 'page-1', title: 'Home', contents: [] }] as ContentPage[],
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(linkTypeValues(mountPanel(enabled))).toEqual([
|
|
136
|
+
'url',
|
|
137
|
+
'mailTo',
|
|
138
|
+
'tel',
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a label edit emits only the text patch', () => {
|
|
143
|
+
const wrapper = mountPanel(enabled);
|
|
144
|
+
|
|
145
|
+
emitFrom(wrapper, '[data-test-header-button-text]', {
|
|
146
|
+
id: 'headerButton',
|
|
147
|
+
name: 'text',
|
|
148
|
+
value: 'Book now',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
expect(wrapper.emitted('on-change')).toEqual([[{ text: 'Book now' }]]);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test('a fill-colour pick emits only the fillColor patch', () => {
|
|
155
|
+
const wrapper = mountPanel(enabled);
|
|
156
|
+
|
|
157
|
+
emitFrom(wrapper, '[data-test-header-button-fill-color]', {
|
|
158
|
+
id: 'fillColor',
|
|
159
|
+
name: 'fillColor',
|
|
160
|
+
value: 'var(--palette-2)',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(wrapper.emitted('on-change')).toEqual([
|
|
164
|
+
[{ fillColor: 'var(--palette-2)' }],
|
|
165
|
+
]);
|
|
166
|
+
});
|
|
167
|
+
});
|