@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
package/src/shared/page-settings/components/page-settings-section-styles/HeaderButtonSettings.vue
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div data-component="header-button-settings">
|
|
3
|
+
<p class="header-button-settings_option-title tw-mb-2">
|
|
4
|
+
{{ tr('pageSettingsSection.buttonLabel') }}
|
|
5
|
+
</p>
|
|
6
|
+
<TextField
|
|
7
|
+
id="headerButton"
|
|
8
|
+
data-test-header-button-text
|
|
9
|
+
name="text"
|
|
10
|
+
:value="text"
|
|
11
|
+
:placeholder="tr('pageSettingsSection.buttonLabelPlaceholder')"
|
|
12
|
+
:max-length="BUTTON_TEXT_MAX_LENGTH"
|
|
13
|
+
:settings="validFieldSettings"
|
|
14
|
+
@update:model-value="onTextChange"
|
|
15
|
+
/>
|
|
16
|
+
|
|
17
|
+
<p class="header-button-settings_option-title tw-mt-4 tw-mb-2">
|
|
18
|
+
{{ tr('pageSettingsSection.buttonLink') }}
|
|
19
|
+
</p>
|
|
20
|
+
<ButtonGroup
|
|
21
|
+
data-test-header-button-link-type
|
|
22
|
+
:options="linkTypeOptions"
|
|
23
|
+
:columns="linkTypeColumns"
|
|
24
|
+
@on-select="onSelectLinkType"
|
|
25
|
+
/>
|
|
26
|
+
|
|
27
|
+
<div class="tw-mt-2">
|
|
28
|
+
<PageDropdownField
|
|
29
|
+
v-if="buttonLinkType === BUTTON_LINK_TYPE.page"
|
|
30
|
+
id="headerButton"
|
|
31
|
+
data-test-header-button-page
|
|
32
|
+
name="buttonPageId"
|
|
33
|
+
:value="buttonPageId"
|
|
34
|
+
:placeholder="contentSettingsTr('selectAPagePlaceholder')"
|
|
35
|
+
@update:model-value="onPageChange"
|
|
36
|
+
/>
|
|
37
|
+
<EmailField
|
|
38
|
+
v-else-if="buttonLinkType === BUTTON_LINK_TYPE.mailTo"
|
|
39
|
+
id="headerButton"
|
|
40
|
+
data-test-header-button-email
|
|
41
|
+
name="url"
|
|
42
|
+
role="email"
|
|
43
|
+
:value="url"
|
|
44
|
+
:placeholder="contentSettingsTr('emailPlaceholder')"
|
|
45
|
+
:settings="validFieldSettings"
|
|
46
|
+
@update:model-value="onUrlChange"
|
|
47
|
+
/>
|
|
48
|
+
<TelField
|
|
49
|
+
v-else-if="buttonLinkType === BUTTON_LINK_TYPE.tel"
|
|
50
|
+
id="headerButton"
|
|
51
|
+
data-test-header-button-tel
|
|
52
|
+
name="url"
|
|
53
|
+
role="tel"
|
|
54
|
+
:value="url"
|
|
55
|
+
:placeholder="tr('pageSettingsSection.buttonPhonePlaceholder')"
|
|
56
|
+
:settings="validFieldSettings"
|
|
57
|
+
@update:model-value="onUrlChange"
|
|
58
|
+
/>
|
|
59
|
+
<UrlField
|
|
60
|
+
v-else
|
|
61
|
+
id="headerButton"
|
|
62
|
+
data-test-header-button-url
|
|
63
|
+
name="url"
|
|
64
|
+
role="url"
|
|
65
|
+
:value="url"
|
|
66
|
+
:placeholder="contentSettingsTr('exampleWebsitePlaceholder')"
|
|
67
|
+
:settings="validFieldSettings"
|
|
68
|
+
@update:model-value="onUrlChange"
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
|
|
72
|
+
<TextStyleField
|
|
73
|
+
id="header-button-text-style"
|
|
74
|
+
data-test-header-button-text-style
|
|
75
|
+
name="buttonTextStyle"
|
|
76
|
+
:label="contentSettingsTr('buttonTextStyleLabel')"
|
|
77
|
+
:is-admin-maker="isAdminMaker"
|
|
78
|
+
:logo-theme="logoTheme"
|
|
79
|
+
:value="buttonTextStyle"
|
|
80
|
+
:show-text-alignment-options="false"
|
|
81
|
+
@update:model-value="onTextStyleChange"
|
|
82
|
+
/>
|
|
83
|
+
|
|
84
|
+
<ColorField
|
|
85
|
+
id="fillColor"
|
|
86
|
+
data-test-header-button-fill-color
|
|
87
|
+
name="fillColor"
|
|
88
|
+
:label="tr('pageSettingsSection.buttonFillColor')"
|
|
89
|
+
:value="fillColor"
|
|
90
|
+
:is-admin-maker="isAdminMaker"
|
|
91
|
+
:logo-theme="logoTheme"
|
|
92
|
+
@update:model-value="onFillColorChange"
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<ColorField
|
|
96
|
+
v-if="supportsBorderColor"
|
|
97
|
+
id="borderColor"
|
|
98
|
+
data-test-header-button-border-color
|
|
99
|
+
name="borderColor"
|
|
100
|
+
:label="tr('pageSettingsSection.buttonBorderColor')"
|
|
101
|
+
:value="borderColor"
|
|
102
|
+
:is-admin-maker="isAdminMaker"
|
|
103
|
+
:logo-theme="logoTheme"
|
|
104
|
+
@update:model-value="onBorderColorChange"
|
|
105
|
+
/>
|
|
106
|
+
</div>
|
|
107
|
+
</template>
|
|
108
|
+
<script setup lang="ts">
|
|
109
|
+
import { computed } from 'vue';
|
|
110
|
+
import { ButtonGroup } from '@designcrowd/fe-shared-lib';
|
|
111
|
+
import type {
|
|
112
|
+
BrandPageContext,
|
|
113
|
+
HeaderButton,
|
|
114
|
+
LogoTheme,
|
|
115
|
+
TextStyleDeclaration,
|
|
116
|
+
} from '@brand-page';
|
|
117
|
+
import { BUTTON_LINK_TYPE } from '@brand-page';
|
|
118
|
+
import type { ButtonLinkType } from '@brand-page';
|
|
119
|
+
import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
|
|
120
|
+
import { buttonLinkStyleConvertor } from '@brand-page-maker/convertors';
|
|
121
|
+
import { useUiPageSettingsStore } from '../../store';
|
|
122
|
+
import { useUiStyleSettingsStore } from '../../../style-settings/store/style-settings-store';
|
|
123
|
+
import type { ContentItemFieldSettings } from '../../../content-settings/models';
|
|
124
|
+
import ColorField from '../../../content-settings/components/color-field/ColorField.vue';
|
|
125
|
+
import EmailField from '../../../content-settings/components/email-field/EmailField.vue';
|
|
126
|
+
import PageDropdownField from '../../../content-settings/components/page-dropdown-field/PageDropdownField.vue';
|
|
127
|
+
import TelField from '../../../content-settings/components/tel-field/TelField.vue';
|
|
128
|
+
import TextField from '../../../content-settings/components/text-field/TextField.vue';
|
|
129
|
+
import TextStyleField from '../../../content-settings/components/text-style-field/TextStyleField.vue';
|
|
130
|
+
import UrlField from '../../../content-settings/components/url-field/UrlField.vue';
|
|
131
|
+
import { contentSettingsTr, tr } from '@src/composables/useTranslate';
|
|
132
|
+
|
|
133
|
+
/** Matches the section buttons' label cap (`buttonText` in the field configs). */
|
|
134
|
+
const BUTTON_TEXT_MAX_LENGTH = 60;
|
|
135
|
+
|
|
136
|
+
interface Props {
|
|
137
|
+
value?: HeaderButton;
|
|
138
|
+
isAdminMaker: boolean;
|
|
139
|
+
logoTheme: LogoTheme;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const props = defineProps<Props>();
|
|
143
|
+
|
|
144
|
+
const pageSettingsStore = useUiPageSettingsStore();
|
|
145
|
+
const styleSettingsStore = useUiStyleSettingsStore();
|
|
146
|
+
|
|
147
|
+
// The site's selected button style decides whether a border exists at all — the
|
|
148
|
+
// default and several others draw none, so a border colour would be inert. Same
|
|
149
|
+
// capability gate the global Style panel applies to this control.
|
|
150
|
+
const supportsBorderColor = computed(
|
|
151
|
+
(): boolean =>
|
|
152
|
+
styleSettingsStore.selectedButtonStyleConfig?.options.includes(
|
|
153
|
+
'buttonBorderColor',
|
|
154
|
+
) === true,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
const emit = defineEmits<{
|
|
158
|
+
(e: 'on-change', patch: Partial<HeaderButton>): void;
|
|
159
|
+
}>();
|
|
160
|
+
|
|
161
|
+
const text = computed((): string => props.value?.text || '');
|
|
162
|
+
const url = computed((): string => props.value?.url || '');
|
|
163
|
+
const buttonPageId = computed((): string => props.value?.buttonPageId || '');
|
|
164
|
+
const fillColor = computed((): string => props.value?.fillColor || '');
|
|
165
|
+
const borderColor = computed((): string => props.value?.borderColor || '');
|
|
166
|
+
const buttonTextStyle = computed(
|
|
167
|
+
(): TextStyleDeclaration => props.value?.buttonTextStyle || {},
|
|
168
|
+
);
|
|
169
|
+
const buttonLinkType = computed(
|
|
170
|
+
(): ButtonLinkType => props.value?.buttonLinkType || BUTTON_LINK_TYPE.url,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The url/email/tel inputs surface `settings.errorMessage` when `isValid` is
|
|
175
|
+
* false, and `TelField` renders its error row whenever `isValid` is not `true`.
|
|
176
|
+
* There is no validator behind these fields yet, so state them valid.
|
|
177
|
+
*/
|
|
178
|
+
const validFieldSettings: ContentItemFieldSettings = {
|
|
179
|
+
errorMessage: '',
|
|
180
|
+
isValid: true,
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const linkTypeConfig = computed(() =>
|
|
184
|
+
buttonLinkStyleConvertor.convert(
|
|
185
|
+
buttonLinkType.value,
|
|
186
|
+
// The convertor reads `pages` only, to drop "Page" on a single-page site;
|
|
187
|
+
// a settings panel has no full render context to hand it. Read off the
|
|
188
|
+
// store rather than passed in, so adding a page re-offers the option.
|
|
189
|
+
{ pages: pageSettingsStore.pages } as unknown as BrandPageContext,
|
|
190
|
+
),
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const linkTypeOptions = computed(
|
|
194
|
+
(): ButtonGroupOption[] => linkTypeConfig.value.options,
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
const linkTypeColumns = computed(
|
|
198
|
+
(): number | undefined => linkTypeConfig.value.columns,
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
const onTextChange = ({ value }: { value: string }): void => {
|
|
202
|
+
emit('on-change', { text: value });
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const onSelectLinkType = (option: ButtonGroupOption): void => {
|
|
206
|
+
emit('on-change', { buttonLinkType: option.value as ButtonLinkType });
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const onUrlChange = ({ value }: { value: string }): void => {
|
|
210
|
+
emit('on-change', { url: value });
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
const onPageChange = ({ value }: { value: string }): void => {
|
|
214
|
+
emit('on-change', { buttonPageId: value });
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const onTextStyleChange = ({
|
|
218
|
+
value,
|
|
219
|
+
}: {
|
|
220
|
+
value: TextStyleDeclaration;
|
|
221
|
+
}): void => {
|
|
222
|
+
emit('on-change', { buttonTextStyle: value });
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const onFillColorChange = ({ value }: { value: string }): void => {
|
|
226
|
+
emit('on-change', { fillColor: value });
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const onBorderColorChange = ({ value }: { value: string }): void => {
|
|
230
|
+
emit('on-change', { borderColor: value });
|
|
231
|
+
};
|
|
232
|
+
</script>
|
|
233
|
+
<style scoped>
|
|
234
|
+
.header-button-settings_option-title {
|
|
235
|
+
@apply tw-font-sans tw-font-bold tw-text-secondary-800 tw-text-sm;
|
|
236
|
+
}
|
|
237
|
+
</style>
|
package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.test.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { shallowMount } from '@vue/test-utils';
|
|
4
|
+
import { ButtonGroup } from '@designcrowd/fe-shared-lib';
|
|
5
|
+
import {
|
|
6
|
+
HEADER_DISPLAY,
|
|
7
|
+
HEADER_DISPLAY_STYLE_CONFIGS,
|
|
8
|
+
NAVIGATION_DISPLAY,
|
|
9
|
+
VIEWPORTS,
|
|
10
|
+
} from '@brand-page';
|
|
11
|
+
import type { ViewportsType } from '@brand-page';
|
|
12
|
+
import type { ButtonGroupOption } from '@brand-page-maker/models/button-group.model';
|
|
13
|
+
import HeaderDisplayStyle from './HeaderDisplayStyle.vue';
|
|
14
|
+
|
|
15
|
+
// BP-5852: enabling the header button swaps the six DESKTOP BAR tiles to the
|
|
16
|
+
// with-button artwork. No such artwork exists for the mobile bar, hamburger or
|
|
17
|
+
// no-logo sets, so those keep their plain icons; `dataAttribute` never changes
|
|
18
|
+
// (selection state and the bc-net e2e locators key off it).
|
|
19
|
+
describe('HeaderDisplayStyle — header-button icon swap', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
setActivePinia(createPinia());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const mountTiles = ({
|
|
25
|
+
displayStyle,
|
|
26
|
+
navigationDisplayType,
|
|
27
|
+
viewport = VIEWPORTS.desktop as ViewportsType,
|
|
28
|
+
isLogoVisible = true,
|
|
29
|
+
showHeaderButton = false,
|
|
30
|
+
}: {
|
|
31
|
+
displayStyle: string;
|
|
32
|
+
navigationDisplayType: string;
|
|
33
|
+
viewport?: ViewportsType;
|
|
34
|
+
isLogoVisible?: boolean;
|
|
35
|
+
showHeaderButton?: boolean;
|
|
36
|
+
}): ButtonGroupOption[] => {
|
|
37
|
+
const wrapper = shallowMount(HeaderDisplayStyle, {
|
|
38
|
+
props: {
|
|
39
|
+
displayStyle,
|
|
40
|
+
navigationDisplayType,
|
|
41
|
+
viewport,
|
|
42
|
+
isLogoVisible,
|
|
43
|
+
showHeaderButton,
|
|
44
|
+
onSelect: () => undefined,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return wrapper
|
|
49
|
+
.findComponent(ButtonGroup)
|
|
50
|
+
.props('options') as ButtonGroupOption[];
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const desktopBar = (showHeaderButton: boolean) =>
|
|
54
|
+
mountTiles({
|
|
55
|
+
displayStyle: HEADER_DISPLAY.DESKTOP_TAB_1,
|
|
56
|
+
navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
|
|
57
|
+
showHeaderButton,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const iconNames = (options: ButtonGroupOption[]) =>
|
|
61
|
+
options.map((option) => ('iconName' in option ? option.iconName : ''));
|
|
62
|
+
|
|
63
|
+
test('off: the six desktop bar tiles keep their plain icons', () => {
|
|
64
|
+
expect(iconNames(desktopBar(false))).toEqual([
|
|
65
|
+
'header-desktop-nav-style-tab-display-style-1',
|
|
66
|
+
'header-desktop-nav-style-tab-display-style-2',
|
|
67
|
+
'header-desktop-nav-style-tab-display-style-3',
|
|
68
|
+
'header-desktop-nav-style-tab-display-style-4',
|
|
69
|
+
'header-desktop-nav-style-tab-display-style-5',
|
|
70
|
+
'header-desktop-nav-style-tab-display-style-6',
|
|
71
|
+
]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('on: the six desktop bar tiles swap to the with-button icons', () => {
|
|
75
|
+
expect(iconNames(desktopBar(true))).toEqual([
|
|
76
|
+
'header-desktop-nav-style-tab-display-style-with-button-1',
|
|
77
|
+
'header-desktop-nav-style-tab-display-style-with-button-2',
|
|
78
|
+
'header-desktop-nav-style-tab-display-style-with-button-3',
|
|
79
|
+
'header-desktop-nav-style-tab-display-style-with-button-4',
|
|
80
|
+
'header-desktop-nav-style-tab-display-style-with-button-5',
|
|
81
|
+
'header-desktop-nav-style-tab-display-style-with-button-6',
|
|
82
|
+
]);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('value, dataAttribute and iconViewBox survive the swap', () => {
|
|
86
|
+
const off = desktopBar(false);
|
|
87
|
+
const on = desktopBar(true);
|
|
88
|
+
|
|
89
|
+
expect(on.map((option) => option.value)).toEqual(
|
|
90
|
+
off.map((option) => option.value),
|
|
91
|
+
);
|
|
92
|
+
expect(on.map((option) => option.dataAttribute)).toEqual(
|
|
93
|
+
off.map((option) => option.dataAttribute),
|
|
94
|
+
);
|
|
95
|
+
expect(
|
|
96
|
+
on.map((option) => ('iconViewBox' in option ? option.iconViewBox : '')),
|
|
97
|
+
).toEqual([
|
|
98
|
+
'0 0 160 56',
|
|
99
|
+
'0 0 160 56',
|
|
100
|
+
'0 0 160 56',
|
|
101
|
+
'0 0 160 56',
|
|
102
|
+
'0 0 160 56',
|
|
103
|
+
'0 0 160 56',
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('the mobile bar tiles are left alone', () => {
|
|
108
|
+
const options = mountTiles({
|
|
109
|
+
displayStyle: HEADER_DISPLAY.MOBILE_TAB_1,
|
|
110
|
+
navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
|
|
111
|
+
viewport: VIEWPORTS.mobile,
|
|
112
|
+
showHeaderButton: true,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
iconNames(options).every((name) => !name.includes('with-button')),
|
|
117
|
+
).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('the desktop hamburger tiles are left alone', () => {
|
|
121
|
+
const options = mountTiles({
|
|
122
|
+
displayStyle: HEADER_DISPLAY.DESKTOP_HAMBURGER_1,
|
|
123
|
+
navigationDisplayType: NAVIGATION_DISPLAY.HAMBURGER_1,
|
|
124
|
+
showHeaderButton: true,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
expect(
|
|
128
|
+
iconNames(options).every((name) => !name.includes('with-button')),
|
|
129
|
+
).toBe(true);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('the desktop no-logo bar tiles are left alone', () => {
|
|
133
|
+
const options = mountTiles({
|
|
134
|
+
displayStyle: HEADER_DISPLAY.DESKTOP_TAB_NO_LOGO_1,
|
|
135
|
+
navigationDisplayType: NAVIGATION_DISPLAY.TAB_1,
|
|
136
|
+
isLogoVisible: false,
|
|
137
|
+
showHeaderButton: true,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
expect(
|
|
141
|
+
iconNames(options).every((name) => !name.includes('with-button')),
|
|
142
|
+
).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('the shared display-style configs are never mutated in place', () => {
|
|
146
|
+
desktopBar(true);
|
|
147
|
+
|
|
148
|
+
// Absolute assertions, not a before/after diff: the configs are authored with
|
|
149
|
+
// isSelected: false and plain icon names, so any in-place write (selection or
|
|
150
|
+
// the with-button swap) leaves a value these checks catch — a snapshot taken
|
|
151
|
+
// after a mutation would not.
|
|
152
|
+
HEADER_DISPLAY_STYLE_CONFIGS.forEach((config) => {
|
|
153
|
+
expect(config.option.isSelected).toBe(false);
|
|
154
|
+
|
|
155
|
+
if ('iconName' in config.option) {
|
|
156
|
+
expect(config.option.iconName).not.toContain('with-button');
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
});
|
package/src/shared/page-settings/components/page-settings-section-styles/HeaderDisplayStyle.vue
CHANGED
|
@@ -30,14 +30,41 @@ import { useBrandPageMakerStore } from '@brand-page-maker/stores';
|
|
|
30
30
|
import { useUiPageSettingsStore } from '../../store';
|
|
31
31
|
import { tr } from '@src/composables/useTranslate';
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Only the six desktop BAR tiles have with-button artwork; every other icon
|
|
35
|
+
* name (mobile bar, hamburger, no-logo) keeps its plain tile.
|
|
36
|
+
*/
|
|
37
|
+
const DESKTOP_BAR_ICON_PREFIX = 'header-desktop-nav-style-tab-display-style-';
|
|
38
|
+
|
|
33
39
|
interface Props {
|
|
34
40
|
displayStyle: string;
|
|
35
41
|
onSelect: (displayStyle: string) => void;
|
|
36
42
|
navigationDisplayType: string;
|
|
37
43
|
isLogoVisible: boolean;
|
|
38
44
|
viewport: ViewportsType;
|
|
45
|
+
showHeaderButton?: boolean;
|
|
39
46
|
}
|
|
40
|
-
const props = defineProps<Props>()
|
|
47
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
48
|
+
showHeaderButton: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const withHeaderButtonIcon = (option: ButtonGroupOption): ButtonGroupOption => {
|
|
52
|
+
if (
|
|
53
|
+
!props.showHeaderButton ||
|
|
54
|
+
!('iconName' in option) ||
|
|
55
|
+
!option.iconName.startsWith(DESKTOP_BAR_ICON_PREFIX)
|
|
56
|
+
) {
|
|
57
|
+
return option;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
...option,
|
|
62
|
+
iconName: option.iconName.replace(
|
|
63
|
+
DESKTOP_BAR_ICON_PREFIX,
|
|
64
|
+
`${DESKTOP_BAR_ICON_PREFIX}with-button-`,
|
|
65
|
+
),
|
|
66
|
+
};
|
|
67
|
+
};
|
|
41
68
|
|
|
42
69
|
const brandPageMakerStore = useBrandPageMakerStore();
|
|
43
70
|
const pageSettingsStore = useUiPageSettingsStore();
|
|
@@ -72,13 +99,15 @@ const options = computed(() => {
|
|
|
72
99
|
}
|
|
73
100
|
}
|
|
74
101
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
102
|
+
// Copied, never mutated in place: `config.option` belongs to the shared
|
|
103
|
+
// HEADER_DISPLAY_STYLE_CONFIGS module constant, so writing `isSelected` onto
|
|
104
|
+
// it leaked this panel's selection into every other consumer.
|
|
105
|
+
const filteredOptions: ButtonGroupOption[] = filteredConfigs.map((config) =>
|
|
106
|
+
withHeaderButtonIcon({
|
|
107
|
+
...config.option,
|
|
108
|
+
isSelected: props.displayStyle === config.option.value,
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
82
111
|
|
|
83
112
|
const selectedOption = filteredOptions.find((x) => x.isSelected);
|
|
84
113
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { createPinia, setActivePinia } from 'pinia';
|
|
3
|
+
import { VIEWPORTS } from '@brand-page';
|
|
4
|
+
import type { HeaderButton } from '@brand-page';
|
|
5
|
+
import mixin from './PageSettingsSectionStyles.mixin';
|
|
6
|
+
import { useUiPageSettingsStore } from '@shared/page-settings/store';
|
|
7
|
+
|
|
8
|
+
// BP-5852 AC: the header button is configured once but must apply to BOTH
|
|
9
|
+
// viewports. There is no generic sync mechanism — the handler dual-writes, the
|
|
10
|
+
// way onCopyrightTextChange / onSocialLinksChange do. The method is pure over
|
|
11
|
+
// `this`, so it's invoked against a hand-built context wired to the real store
|
|
12
|
+
// (the PageSettings.mixin testing pattern) rather than mounting the panel.
|
|
13
|
+
const { onHeaderButtonChange } = (
|
|
14
|
+
mixin as unknown as {
|
|
15
|
+
methods: {
|
|
16
|
+
onHeaderButtonChange(this: unknown, patch: Partial<HeaderButton>): void;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
).methods;
|
|
20
|
+
|
|
21
|
+
describe('PageSettingsSectionStyles.mixin onHeaderButtonChange', () => {
|
|
22
|
+
let pageSettingsStore: ReturnType<typeof useUiPageSettingsStore>;
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
setActivePinia(createPinia());
|
|
26
|
+
pageSettingsStore = useUiPageSettingsStore();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const headers = () => pageSettingsStore.sections.header;
|
|
30
|
+
|
|
31
|
+
// `headerButton` on the CURRENT viewport is what the panel reads and the
|
|
32
|
+
// handler patches — desktop here, as the panel opens on desktop.
|
|
33
|
+
const context = () => ({
|
|
34
|
+
pageSettingsStore,
|
|
35
|
+
headerButton: headers()[VIEWPORTS.desktop].headerButton,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const change = (patch: Partial<HeaderButton>) =>
|
|
39
|
+
onHeaderButtonChange.call(context(), patch);
|
|
40
|
+
|
|
41
|
+
test('the first write seeds a full button on both viewports', () => {
|
|
42
|
+
change({ isVisible: true });
|
|
43
|
+
|
|
44
|
+
expect(headers()[VIEWPORTS.desktop].headerButton).toEqual({
|
|
45
|
+
isVisible: true,
|
|
46
|
+
text: '',
|
|
47
|
+
buttonLinkType: 'url',
|
|
48
|
+
});
|
|
49
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).toEqual(
|
|
50
|
+
headers()[VIEWPORTS.desktop].headerButton,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('off by default — nothing is seeded until something is written', () => {
|
|
55
|
+
expect(headers()[VIEWPORTS.desktop].headerButton).toBeUndefined();
|
|
56
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('later writes patch one key and still land on both viewports', () => {
|
|
60
|
+
change({ isVisible: true });
|
|
61
|
+
change({ text: 'Book now' });
|
|
62
|
+
change({ buttonLinkType: 'tel', url: '12345-6789' });
|
|
63
|
+
|
|
64
|
+
const expected: HeaderButton = {
|
|
65
|
+
isVisible: true,
|
|
66
|
+
text: 'Book now',
|
|
67
|
+
buttonLinkType: 'tel',
|
|
68
|
+
url: '12345-6789',
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
expect(headers()[VIEWPORTS.desktop].headerButton).toEqual(expected);
|
|
72
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).toEqual(expected);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('each viewport holds its own object, not a shared reference', () => {
|
|
76
|
+
change({ isVisible: true });
|
|
77
|
+
|
|
78
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).not.toBe(
|
|
79
|
+
headers()[VIEWPORTS.desktop].headerButton,
|
|
80
|
+
);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a viewport that somehow diverged is reconciled by the next write', () => {
|
|
84
|
+
change({ isVisible: true, text: 'Contact' });
|
|
85
|
+
|
|
86
|
+
headers()[VIEWPORTS.mobile].headerButton = {
|
|
87
|
+
isVisible: false,
|
|
88
|
+
text: 'stale',
|
|
89
|
+
buttonLinkType: 'url',
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
change({ text: 'Contact us' });
|
|
93
|
+
|
|
94
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).toEqual(
|
|
95
|
+
headers()[VIEWPORTS.desktop].headerButton,
|
|
96
|
+
);
|
|
97
|
+
expect(headers()[VIEWPORTS.mobile].headerButton?.text).toBe('Contact us');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('toggling off keeps the config, only flipping isVisible', () => {
|
|
101
|
+
change({ isVisible: true, text: 'Contact us' });
|
|
102
|
+
change({ isVisible: false });
|
|
103
|
+
|
|
104
|
+
expect(headers()[VIEWPORTS.desktop].headerButton).toEqual({
|
|
105
|
+
isVisible: false,
|
|
106
|
+
text: 'Contact us',
|
|
107
|
+
buttonLinkType: 'url',
|
|
108
|
+
});
|
|
109
|
+
expect(headers()[VIEWPORTS.mobile].headerButton).toEqual(
|
|
110
|
+
headers()[VIEWPORTS.desktop].headerButton,
|
|
111
|
+
);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
PageSettingsFontSettingsOption,
|
|
10
10
|
} from '../../models';
|
|
11
11
|
import type {
|
|
12
|
+
HeaderButton,
|
|
12
13
|
LogoTheme,
|
|
13
14
|
NavigationDisplayType,
|
|
14
15
|
NavigationType,
|
|
@@ -27,6 +28,7 @@ import type {
|
|
|
27
28
|
BackgroundColorStyle,
|
|
28
29
|
} from '@brand-page';
|
|
29
30
|
import {
|
|
31
|
+
BUTTON_LINK_TYPE,
|
|
30
32
|
LOGO_SLIDER_DESKTOP_MAX,
|
|
31
33
|
LOGO_SLIDER_DESKTOP_MIN,
|
|
32
34
|
LOGO_SLIDER_MOBILE_MAX,
|
|
@@ -62,6 +64,17 @@ import { FabricRenderClient } from '../../../../clients/fabric-render-client';
|
|
|
62
64
|
import { parseLogoThemeToCssStyles } from '@brand-page/utils';
|
|
63
65
|
import { commonTr } from '@src/composables/useTranslate';
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Seed for the first write to `headerButton` (the model deliberately holds no
|
|
69
|
+
* default — absent means "no button"). Url is the link kind the Figma shows
|
|
70
|
+
* pre-selected.
|
|
71
|
+
*/
|
|
72
|
+
const NEW_HEADER_BUTTON: HeaderButton = {
|
|
73
|
+
isVisible: false,
|
|
74
|
+
text: '',
|
|
75
|
+
buttonLinkType: BUTTON_LINK_TYPE.url,
|
|
76
|
+
};
|
|
77
|
+
|
|
65
78
|
const viewportsWithTr = () => [
|
|
66
79
|
{
|
|
67
80
|
key: VIEWPORTS.desktop,
|
|
@@ -162,6 +175,12 @@ export default defineComponent({
|
|
|
162
175
|
header(): PageHeaderDesktop | PageHeaderMobile {
|
|
163
176
|
return this.sections[PAGE_SECTIONS.header][this.viewport];
|
|
164
177
|
},
|
|
178
|
+
headerButton(): HeaderButton | undefined {
|
|
179
|
+
return this.header.headerButton;
|
|
180
|
+
},
|
|
181
|
+
isHeaderButtonVisible(): boolean {
|
|
182
|
+
return this.headerButton?.isVisible === true;
|
|
183
|
+
},
|
|
165
184
|
navigationType(): NavigationType {
|
|
166
185
|
return getNavigationType(this.navigation.displayType);
|
|
167
186
|
},
|
|
@@ -451,6 +470,34 @@ export default defineComponent({
|
|
|
451
470
|
});
|
|
452
471
|
},
|
|
453
472
|
|
|
473
|
+
onHeaderButtonChange(patch: Partial<HeaderButton>) {
|
|
474
|
+
const value: HeaderButton = {
|
|
475
|
+
...NEW_HEADER_BUTTON,
|
|
476
|
+
...this.headerButton,
|
|
477
|
+
...patch,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// Desktop and mobile must hold the SAME button config (BP-5852 AC). The
|
|
481
|
+
// whole object is written to both from one source rather than merged
|
|
482
|
+
// per-viewport, so a pair that somehow diverged is reconciled instead of
|
|
483
|
+
// each side keeping its own leftovers.
|
|
484
|
+
//
|
|
485
|
+
// Each viewport gets its own copy: one shared object would alias the two
|
|
486
|
+
// branches of store state to a single instance.
|
|
487
|
+
this.pageSettingsStore.setPageSettingsSectionValue({
|
|
488
|
+
sectionName: PAGE_SECTIONS.header,
|
|
489
|
+
propName: 'headerButton',
|
|
490
|
+
value: { ...value },
|
|
491
|
+
viewport: VIEWPORTS.mobile,
|
|
492
|
+
});
|
|
493
|
+
this.pageSettingsStore.setPageSettingsSectionValue({
|
|
494
|
+
sectionName: PAGE_SECTIONS.header,
|
|
495
|
+
propName: 'headerButton',
|
|
496
|
+
value: { ...value },
|
|
497
|
+
viewport: VIEWPORTS.desktop,
|
|
498
|
+
});
|
|
499
|
+
},
|
|
500
|
+
|
|
454
501
|
onItemListIntentImageReplace(
|
|
455
502
|
name: PageSectionStyleOption,
|
|
456
503
|
{ index }: { index: number },
|
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
"iconStyle": "Icon style",
|
|
9
9
|
"logoSize": "Logo Size",
|
|
10
10
|
"socialLinks": "Social Links",
|
|
11
|
-
"copyright": "Copyright"
|
|
11
|
+
"copyright": "Copyright",
|
|
12
|
+
"buttonLabel": "Label",
|
|
13
|
+
"buttonLabelPlaceholder": "Contact Us",
|
|
14
|
+
"buttonLink": "Link",
|
|
15
|
+
"buttonPhonePlaceholder": "12345-6789",
|
|
16
|
+
"buttonFillColor": "Fill color",
|
|
17
|
+
"buttonBorderColor": "Border color"
|
|
12
18
|
}
|
|
13
19
|
}
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import type { NavigationType } from '@brand-page';
|
|
13
13
|
import type {
|
|
14
14
|
Content,
|
|
15
|
+
HeaderButton,
|
|
15
16
|
ImageGalleryItem,
|
|
16
17
|
LinksItem,
|
|
17
18
|
LogoImageModel,
|
|
@@ -242,6 +243,7 @@ export const useUiPageSettingsStore = defineStore('uiPageSettings', {
|
|
|
242
243
|
| LinksItem[]
|
|
243
244
|
| BrandPageCssStyle
|
|
244
245
|
| Partial<ImageGalleryItem>
|
|
246
|
+
| HeaderButton
|
|
245
247
|
| number
|
|
246
248
|
| null
|
|
247
249
|
| undefined;
|