@designcrowd/library.brand-page 6.2.8 → 6.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-Bi71mUjJ.js → CreatedOnBrandCrowd-de-de-CTEj6D-p.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-C2kl91cc.js → CreatedOnBrandCrowd-en-us-ByFNuMoA.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CNK-VpMH.js → CreatedOnBrandCrowd-es-es-lA_3mWIj.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-B_WTCv5k.js → CreatedOnBrandCrowd-fr-ca-CS_YkUz3.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-B_WTCv5k.js → CreatedOnBrandCrowd-fr-fr-CS_YkUz3.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-Dk-k5-Nh.js → CreatedOnBrandCrowd-pt-br-BoduQ7zJ.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-Dk-k5-Nh.js → CreatedOnBrandCrowd-pt-pt-BoduQ7zJ.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-BVaJyfAR.js → CreatedOnDcom-de-de-Co1OO4_u.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-DVzzynRf.js → CreatedOnDcom-en-us-DqFyqxOJ.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BS2u-QHG.js → CreatedOnDcom-es-es-DhQWyb7C.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BavFpbyC.js → CreatedOnDcom-fr-ca-DIxstLTf.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DRchqDHL.js → CreatedOnDcom-fr-fr-B-z8V1cH.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-N7XAWRj_.js → CreatedOnDcom-pt-br-B7AMR1sc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-Bct14us6.js → CreatedOnDcom-pt-pt-Cdci4yr3.js} +1 -1
  15. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +6 -5
  16. package/lib/src/brand-page-maker/components/brandpage-maker-page/BrandPageMakerPage.mixin.d.ts +6 -5
  17. package/lib/src/brand-page-maker/components/color-picker/BpColorPicker.stories.d.ts +3 -0
  18. package/lib/src/brand-page-maker/components/color-picker/BpColorPicker.stories.js +10 -0
  19. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.d.ts +2 -2
  20. package/lib/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.js +2 -2
  21. package/lib/src/brand-page-maker/components/quick-link-bar/QuickLinkBar.mixin.d.ts +6 -5
  22. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +7 -5
  23. package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.js +5 -0
  24. package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +6 -5
  25. package/lib/src/brand-page-maker/components/seo-settings/UseExistingDomainModal.mixin.d.ts +6 -5
  26. package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +6 -5
  27. package/lib/src/brand-page-maker/components/upgrade-to-premium-button/UpgradeToPremiumButton.mixin.d.ts +6 -5
  28. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +7 -6
  29. package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +52 -5
  30. package/lib/src/{bundled-translations-BNA5h0Dx.js → bundled-translations-zfiA5Tsw.js} +1 -1
  31. package/lib/src/{bundled-translations.de-DE-fyTHrRxT.js → bundled-translations.de-DE-ChsdSyAP.js} +13 -13
  32. package/lib/src/{bundled-translations.es-ES-BMmvJGGC.js → bundled-translations.es-ES-DflsWQK0.js} +17 -17
  33. package/lib/src/{bundled-translations.fr-CA-Bo69L_BP.js → bundled-translations.fr-CA-fpDRuZ52.js} +1 -1
  34. package/lib/src/{bundled-translations.fr-FR-C4yTVFOW.js → bundled-translations.fr-FR-Bub73Zt-.js} +17 -17
  35. package/lib/src/{bundled-translations.pt-BR-CPMPfny0.js → bundled-translations.pt-BR-BlnWE9TB.js} +13 -13
  36. package/lib/src/{bundled-translations.pt-PT-C6ZwYs37.js → bundled-translations.pt-PT-DmzXlnaS.js} +30 -30
  37. package/lib/src/bundles/bundled-translations.de-DE.json +7 -1
  38. package/lib/src/bundles/bundled-translations.es-ES.json +7 -1
  39. package/lib/src/bundles/bundled-translations.fr-CA.json +7 -1
  40. package/lib/src/bundles/bundled-translations.fr-FR.json +7 -1
  41. package/lib/src/bundles/bundled-translations.json +7 -1
  42. package/lib/src/bundles/bundled-translations.pt-BR.json +7 -1
  43. package/lib/src/bundles/bundled-translations.pt-PT.json +7 -1
  44. package/lib/src/css/library.brand-page-brandCrowd.min.css +4 -4
  45. package/lib/src/css/library.brand-page-brandPage.min.css +4 -4
  46. package/lib/src/css/library.brand-page-designCom.min.css +4 -4
  47. package/lib/src/css/library.brand-page-preview.min.css +4 -4
  48. package/lib/src/index.mjs +1 -1
  49. package/lib/src/{lib-BE9sCFvu.js → lib-Y-hakSU_.js} +5456 -5414
  50. package/lib/src/shared/content-settings/components/payment-config-field/PaymentConfigField.mixin.d.ts +6 -5
  51. package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +6 -5
  52. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +6 -5
  53. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +6 -5
  54. package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +6 -5
  55. package/lib/src/shared/side-bar/components/SideBar.mixin.d.ts +6 -5
  56. package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +6 -5
  57. package/package.json +1 -1
  58. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.test.ts +323 -0
  59. package/src/brand-page-maker/components/brandpage-settings/BrandPageSettings.vue +3 -3
  60. package/src/brand-page-maker/components/color-picker/BpColorPicker.stories.ts +10 -0
  61. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.mixin.ts +2 -2
  62. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.test.ts +116 -0
  63. package/src/brand-page-maker/components/color-picker/components/ColorModeSelect.vue +7 -1
  64. package/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.test.ts +157 -0
  65. package/src/brand-page-maker/components/color-picker/components/GradientTypeSelector.vue +10 -4
  66. package/src/brand-page-maker/components/sensitive-content/SensitiveContent.vue +23 -3
  67. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.ts +5 -0
  68. package/src/brand-page-maker/components/seo-settings/SeoPageSettings.vue +2 -0
  69. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.save.test.ts +332 -1
  70. package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +83 -5
  71. package/src/bundles/bundled-translations.de-DE.json +7 -1
  72. package/src/bundles/bundled-translations.es-ES.json +7 -1
  73. package/src/bundles/bundled-translations.fr-CA.json +7 -1
  74. package/src/bundles/bundled-translations.fr-FR.json +7 -1
  75. package/src/bundles/bundled-translations.json +7 -1
  76. package/src/bundles/bundled-translations.pt-BR.json +7 -1
  77. package/src/bundles/bundled-translations.pt-PT.json +7 -1
  78. package/src/shared/color-picker/components/i18n/color-picker.de-DE.json +8 -1
  79. package/src/shared/color-picker/components/i18n/color-picker.es-ES.json +8 -1
  80. package/src/shared/color-picker/components/i18n/color-picker.fr-CA.json +8 -1
  81. package/src/shared/color-picker/components/i18n/color-picker.fr-FR.json +8 -1
  82. package/src/shared/color-picker/components/i18n/color-picker.json +8 -1
  83. package/src/shared/color-picker/components/i18n/color-picker.pt-BR.json +8 -1
  84. package/src/shared/color-picker/components/i18n/color-picker.pt-PT.json +8 -1
@@ -351,13 +351,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
351
351
  startBrandPageSlugEditMode(): void;
352
352
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
353
353
  unpublishBrandPage(): Promise<void>;
354
- updateBrandPageSettings({ description, title }: {
355
- description: any;
356
- title: any;
354
+ updateBrandPageSettings(payload: {
355
+ description: string;
356
+ title: string;
357
357
  }): Promise<void>;
358
- updateSensitiveContent({ sensitiveContent }: {
359
- sensitiveContent: any;
358
+ updateSensitiveContent(payload: {
359
+ sensitiveContent: import("@brand-page").SensitiveContent;
360
360
  }): Promise<void>;
361
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
361
362
  createBrandPageCustomDomain({ domainName }: {
362
363
  domainName: any;
363
364
  }): Promise<false | undefined>;
@@ -647,13 +647,14 @@ declare const _default: import("vue").DefineComponent<{}, {
647
647
  startBrandPageSlugEditMode(): void;
648
648
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
649
649
  unpublishBrandPage(): Promise<void>;
650
- updateBrandPageSettings({ description, title }: {
651
- description: any;
652
- title: any;
650
+ updateBrandPageSettings(payload: {
651
+ description: string;
652
+ title: string;
653
653
  }): Promise<void>;
654
- updateSensitiveContent({ sensitiveContent }: {
655
- sensitiveContent: any;
654
+ updateSensitiveContent(payload: {
655
+ sensitiveContent: import("../../../brand-page").SensitiveContent;
656
656
  }): Promise<void>;
657
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
657
658
  createBrandPageCustomDomain({ domainName }: {
658
659
  domainName: any;
659
660
  }): Promise<false | undefined>;
@@ -5083,13 +5083,14 @@ declare const _default: import("vue").DefineComponent<{}, {
5083
5083
  startBrandPageSlugEditMode(): void;
5084
5084
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
5085
5085
  unpublishBrandPage(): Promise<void>;
5086
- updateBrandPageSettings({ description, title }: {
5087
- description: any;
5088
- title: any;
5086
+ updateBrandPageSettings(payload: {
5087
+ description: string;
5088
+ title: string;
5089
5089
  }): Promise<void>;
5090
- updateSensitiveContent({ sensitiveContent }: {
5091
- sensitiveContent: any;
5090
+ updateSensitiveContent(payload: {
5091
+ sensitiveContent: import("@brand-page").SensitiveContent;
5092
5092
  }): Promise<void>;
5093
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
5093
5094
  createBrandPageCustomDomain({ domainName }: {
5094
5095
  domainName: any;
5095
5096
  }): Promise<false | undefined>;
@@ -4434,13 +4434,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
4434
4434
  startBrandPageSlugEditMode(): void;
4435
4435
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
4436
4436
  unpublishBrandPage(): Promise<void>;
4437
- updateBrandPageSettings({ description, title }: {
4438
- description: any;
4439
- title: any;
4437
+ updateBrandPageSettings(payload: {
4438
+ description: string;
4439
+ title: string;
4440
4440
  }): Promise<void>;
4441
- updateSensitiveContent({ sensitiveContent }: {
4442
- sensitiveContent: any;
4441
+ updateSensitiveContent(payload: {
4442
+ sensitiveContent: import("@brand-page").SensitiveContent;
4443
4443
  }): Promise<void>;
4444
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
4444
4445
  createBrandPageCustomDomain({ domainName }: {
4445
4446
  domainName: any;
4446
4447
  }): Promise<false | undefined>;
@@ -554,13 +554,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
554
554
  startBrandPageSlugEditMode(): void;
555
555
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
556
556
  unpublishBrandPage(): Promise<void>;
557
- updateBrandPageSettings({ description, title }: {
558
- description: any;
559
- title: any;
557
+ updateBrandPageSettings(payload: {
558
+ description: string;
559
+ title: string;
560
560
  }): Promise<void>;
561
- updateSensitiveContent({ sensitiveContent }: {
562
- sensitiveContent: any;
561
+ updateSensitiveContent(payload: {
562
+ sensitiveContent: import("../../brand-page").SensitiveContent;
563
563
  }): Promise<void>;
564
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
564
565
  createBrandPageCustomDomain({ domainName }: {
565
566
  domainName: any;
566
567
  }): Promise<false | undefined>;
@@ -344,13 +344,14 @@ declare const _default: import("vue").DefineComponent<import("vue").ExtractPropT
344
344
  startBrandPageSlugEditMode(): void;
345
345
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
346
346
  unpublishBrandPage(): Promise<void>;
347
- updateBrandPageSettings({ description, title }: {
348
- description: any;
349
- title: any;
347
+ updateBrandPageSettings(payload: {
348
+ description: string;
349
+ title: string;
350
350
  }): Promise<void>;
351
- updateSensitiveContent({ sensitiveContent }: {
352
- sensitiveContent: any;
351
+ updateSensitiveContent(payload: {
352
+ sensitiveContent: import("../../../brand-page").SensitiveContent;
353
353
  }): Promise<void>;
354
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
354
355
  createBrandPageCustomDomain({ domainName }: {
355
356
  domainName: any;
356
357
  }): Promise<false | undefined>;
@@ -852,13 +852,14 @@ declare const _default: import("vue").DefineComponent<{}, {
852
852
  startBrandPageSlugEditMode(): void;
853
853
  triggerLockedContentClickedEvent(contentType: string): Promise<void>;
854
854
  unpublishBrandPage(): Promise<void>;
855
- updateBrandPageSettings({ description, title }: {
856
- description: any;
857
- title: any;
855
+ updateBrandPageSettings(payload: {
856
+ description: string;
857
+ title: string;
858
858
  }): Promise<void>;
859
- updateSensitiveContent({ sensitiveContent }: {
860
- sensitiveContent: any;
859
+ updateSensitiveContent(payload: {
860
+ sensitiveContent: import("../../../brand-page").SensitiveContent;
861
861
  }): Promise<void>;
862
+ saveSiteBeforeFirstSettingsWrite(): Promise<boolean>;
862
863
  createBrandPageCustomDomain({ domainName }: {
863
864
  domainName: any;
864
865
  }): Promise<false | undefined>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@designcrowd/library.brand-page",
3
3
  "description": "A collection of Maker/Designer applications",
4
- "version": "6.2.8",
4
+ "version": "6.2.10",
5
5
  "author": "Design.com Eng",
6
6
  "license": "UNLICENSED",
7
7
  "type": "module",
@@ -0,0 +1,323 @@
1
+ import { mount, DOMWrapper } from '@vue/test-utils';
2
+ import { createPinia, setActivePinia } from 'pinia';
3
+ import { describe, test, expect, beforeEach, vi } from 'vitest';
4
+ import { TextInput, Textarea } from '@designcrowd/fe-shared-lib';
5
+ import BrandPageSettings from './BrandPageSettings.vue';
6
+ import SeoPageSettings from '../seo-settings/SeoPageSettings.vue';
7
+ import SensitiveContent from '../sensitive-content/SensitiveContent.vue';
8
+ import ImageField from '@shared/content-settings/components/image-field/ImageField.vue';
9
+ import {
10
+ useBrandPageContainerStore,
11
+ useBrandPageMakerStore,
12
+ } from '@brand-page-maker/stores';
13
+ import { SENSITIVE_CONTENT_TYPES } from '@brand-page';
14
+
15
+ // CollapsiblePanel is imported directly in a `<script setup>` template, so
16
+ // `global.stubs` can't intercept it by name (no `resolveComponent` call is
17
+ // emitted for it). It starts collapsed and unmounts its slot when collapsed
18
+ // (maker-settings-panels.md), so open every panel for real by clicking its
19
+ // header, the same way ColorStrategyPanel.test.ts does.
20
+ const mountSettings = async () => {
21
+ const wrapper = mount(BrandPageSettings);
22
+
23
+ for (const header of wrapper.findAll(
24
+ '[data-component="collapsible-panel"] > div',
25
+ )) {
26
+ await header.trigger('click');
27
+ }
28
+
29
+ return wrapper;
30
+ };
31
+
32
+ type Wrapper = Awaited<ReturnType<typeof mountSettings>>;
33
+
34
+ const radios = (wrapper: Wrapper) =>
35
+ wrapper.findAll('input[type="radio"][name="sensitiveContent"]');
36
+
37
+ // GoogleAnalytics and SocialShare each render their own TextInput in a
38
+ // DIFFERENT collapsible panel, so scope to the panel holding the SEO
39
+ // Textarea (unique to SeoPageSettings) to find the SEO title input alone.
40
+ const seoTitleInput = (wrapper: Wrapper) => {
41
+ const textarea = wrapper.findComponent(Textarea);
42
+
43
+ if (!textarea.exists()) return undefined;
44
+
45
+ const panel = textarea.element.closest(
46
+ '[data-component="collapsible-panel"]',
47
+ ) as HTMLElement;
48
+
49
+ const input = new DOMWrapper(panel).findComponent(TextInput);
50
+
51
+ return input.exists() ? input : undefined;
52
+ };
53
+
54
+ // The Sensitive Content panel is the only rendered panel with these radios;
55
+ // its Update button is the nearest one in the same collapsible panel.
56
+ const sensitiveContentUpdateButton = (wrapper: Wrapper) => {
57
+ const panel = radios(wrapper)[0].element.closest(
58
+ '[data-component="collapsible-panel"]',
59
+ ) as HTMLElement;
60
+
61
+ return new DOMWrapper(panel).find('button');
62
+ };
63
+
64
+ const faviconField = (wrapper: Wrapper) =>
65
+ wrapper
66
+ .findAllComponents(ImageField)
67
+ .find((field) => field.props('name') === 'favicon');
68
+
69
+ describe('BrandPageSettings — SEO and Sensitive Content before the first save', () => {
70
+ let containerStore: ReturnType<typeof useBrandPageContainerStore>;
71
+ let brandPageMakerStore: ReturnType<typeof useBrandPageMakerStore>;
72
+
73
+ beforeEach(() => {
74
+ setActivePinia(createPinia());
75
+
76
+ containerStore = useBrandPageContainerStore();
77
+ brandPageMakerStore = useBrandPageMakerStore();
78
+ });
79
+
80
+ test.each([
81
+ ['post-purchase, never saved', {}],
82
+ ['pre-purchase, never saved', { isPrePurchase: true }],
83
+ ['saved site', { designDraftToken: 'draft-123' }],
84
+ ])(
85
+ 'shows both settings (%s)',
86
+ async (_case: string, flags: Record<string, boolean | string>) => {
87
+ Object.assign(containerStore, flags);
88
+
89
+ const wrapper = await mountSettings();
90
+
91
+ expect(seoTitleInput(wrapper)).toBeDefined();
92
+ expect(wrapper.findComponent(Textarea).exists()).toBe(true);
93
+ expect(wrapper.find('[data-test-btn-update-seo-settings]').exists()).toBe(
94
+ true,
95
+ );
96
+ expect(radios(wrapper)).toHaveLength(4);
97
+ expect(faviconField(wrapper)).toBeDefined();
98
+ },
99
+ );
100
+
101
+ test('disabled while saving', async () => {
102
+ // isPublishing disables both panels' Update buttons and inputs, and blocks
103
+ // a row click from changing the choice already made — the same as isSaving.
104
+ const publishingWrapper = await mountSettings();
105
+
106
+ // Dirty both panels first — an already-disabled (clean) button can't
107
+ // distinguish "isPublishing disables it" from "it was never enabled".
108
+ await seoTitleInput(publishingWrapper)!.find('input').setValue('A title');
109
+ await new DOMWrapper(
110
+ radios(publishingWrapper)[1].element.parentElement!,
111
+ ).trigger('click');
112
+
113
+ containerStore.isPublishing = true;
114
+ await publishingWrapper.vm.$nextTick();
115
+
116
+ expect(
117
+ sensitiveContentUpdateButton(publishingWrapper).attributes('disabled'),
118
+ ).toBeDefined();
119
+ expect(radios(publishingWrapper)[0].attributes('disabled')).toBeDefined();
120
+ expect(
121
+ publishingWrapper
122
+ .find('[data-test-btn-update-seo-settings]')
123
+ .attributes('disabled'),
124
+ ).toBeDefined();
125
+ expect(seoTitleInput(publishingWrapper)!.props('disabled')).toBe(true);
126
+ expect(publishingWrapper.findComponent(Textarea).props('disabled')).toBe(
127
+ true,
128
+ );
129
+
130
+ // A click on a different row must not change the choice already made
131
+ // above while a publish is in flight.
132
+ await new DOMWrapper(
133
+ radios(publishingWrapper)[2].element.parentElement!,
134
+ ).trigger('click');
135
+ expect(
136
+ (radios(publishingWrapper)[1].element as HTMLInputElement).checked,
137
+ ).toBe(true);
138
+
139
+ containerStore.isPublishing = false;
140
+
141
+ // isSaving disables both panels' Update buttons and inputs, and blocks a
142
+ // row click from changing the choice already made.
143
+ const wrapper = await mountSettings();
144
+
145
+ // Dirty both panels first — an already-disabled (clean) button can't
146
+ // distinguish "isSaving disables it" from "it was never enabled". The
147
+ // radio's own state changes only through the row's `@click` handler, not
148
+ // a native `change`, so click the row rather than `setValue` the input.
149
+ await seoTitleInput(wrapper)!.find('input').setValue('A title');
150
+ await new DOMWrapper(radios(wrapper)[1].element.parentElement!).trigger(
151
+ 'click',
152
+ );
153
+
154
+ expect(
155
+ sensitiveContentUpdateButton(wrapper).attributes('disabled'),
156
+ ).toBeUndefined();
157
+ expect(
158
+ wrapper
159
+ .find('[data-test-btn-update-seo-settings]')
160
+ .attributes('disabled'),
161
+ ).toBeUndefined();
162
+
163
+ containerStore.isSaving = true;
164
+ await wrapper.vm.$nextTick();
165
+
166
+ expect(
167
+ sensitiveContentUpdateButton(wrapper).attributes('disabled'),
168
+ ).toBeDefined();
169
+ expect(radios(wrapper)[0].attributes('disabled')).toBeDefined();
170
+ expect(
171
+ wrapper
172
+ .find('[data-test-btn-update-seo-settings]')
173
+ .attributes('disabled'),
174
+ ).toBeDefined();
175
+ expect(seoTitleInput(wrapper)!.props('disabled')).toBe(true);
176
+ expect(wrapper.findComponent(Textarea).props('disabled')).toBe(true);
177
+
178
+ // A click on a different row must not change the choice already made
179
+ // above while a save is in flight.
180
+ await new DOMWrapper(radios(wrapper)[2].element.parentElement!).trigger(
181
+ 'click',
182
+ );
183
+ expect((radios(wrapper)[1].element as HTMLInputElement).checked).toBe(true);
184
+
185
+ containerStore.isSaving = false;
186
+ await wrapper.vm.$nextTick();
187
+
188
+ expect(
189
+ sensitiveContentUpdateButton(wrapper).attributes('disabled'),
190
+ ).toBeUndefined();
191
+ expect(radios(wrapper)[0].attributes('disabled')).toBeUndefined();
192
+ expect(
193
+ wrapper
194
+ .find('[data-test-btn-update-seo-settings]')
195
+ .attributes('disabled'),
196
+ ).toBeUndefined();
197
+ expect(seoTitleInput(wrapper)!.props('disabled')).toBe(false);
198
+ expect(wrapper.findComponent(Textarea).props('disabled')).toBe(false);
199
+ });
200
+
201
+ test('Manage page', async () => {
202
+ containerStore.isSaving = true;
203
+ vi.spyOn(brandPageMakerStore, 'updateBrandPageSettings').mockResolvedValue(
204
+ undefined,
205
+ );
206
+ vi.spyOn(
207
+ brandPageMakerStore,
208
+ 'updateSensitiveContentSettings',
209
+ ).mockResolvedValue(undefined);
210
+
211
+ const seoWrapper = mount(SeoPageSettings, { props: { isMaker: false } });
212
+
213
+ await seoWrapper.find('input').setValue('New title');
214
+ const seoUpdateButton = seoWrapper.find(
215
+ '[data-test-btn-update-seo-settings]',
216
+ );
217
+
218
+ expect(seoUpdateButton.attributes('disabled')).toBeUndefined();
219
+
220
+ await seoUpdateButton.trigger('click');
221
+ await seoWrapper.vm.$nextTick();
222
+
223
+ expect(brandPageMakerStore.updateBrandPageSettings).toHaveBeenCalledWith(
224
+ expect.objectContaining({ title: 'New title' }),
225
+ );
226
+
227
+ const sensitiveWrapper = mount(SensitiveContent, {
228
+ props: { isMaker: false, value: SENSITIVE_CONTENT_TYPES.off },
229
+ });
230
+ const sensitiveRadios = sensitiveWrapper.findAll(
231
+ 'input[type="radio"][name="sensitiveContent"]',
232
+ );
233
+
234
+ expect(sensitiveRadios[1].attributes('disabled')).toBeUndefined();
235
+
236
+ // The radio has no v-model/@change of its own — a real click lands on
237
+ // the row's wrapping div, same as SensitiveContent's own click handler.
238
+ await new DOMWrapper(sensitiveRadios[1].element.parentElement!).trigger(
239
+ 'click',
240
+ );
241
+ await sensitiveWrapper.vm.$nextTick();
242
+
243
+ const sensitiveUpdateButton = sensitiveWrapper.find('button');
244
+
245
+ expect(sensitiveUpdateButton.attributes('disabled')).toBeUndefined();
246
+
247
+ await sensitiveUpdateButton.trigger('click');
248
+ await sensitiveWrapper.vm.$nextTick();
249
+
250
+ expect(
251
+ brandPageMakerStore.updateSensitiveContentSettings,
252
+ ).toHaveBeenCalledWith({
253
+ sensitiveContent: SENSITIVE_CONTENT_TYPES.plus18,
254
+ shouldRefresh: true,
255
+ });
256
+
257
+ // isPublishing is the maker's own in-flight state (set by
258
+ // ManageBrandPageContainer's onPublishClicked) — it must not lock the
259
+ // Manage page's radios either.
260
+ containerStore.isPublishing = true;
261
+ await sensitiveWrapper.vm.$nextTick();
262
+
263
+ expect(sensitiveRadios[2].attributes('disabled')).toBeUndefined();
264
+
265
+ await new DOMWrapper(sensitiveRadios[2].element.parentElement!).trigger(
266
+ 'click',
267
+ );
268
+ await sensitiveWrapper.vm.$nextTick();
269
+
270
+ expect((sensitiveRadios[2].element as HTMLInputElement).checked).toBe(true);
271
+ });
272
+
273
+ test('SEO inputs stay editable on the Manage page while publishing', async () => {
274
+ // The Manage form is dirty when the store's title or description differs
275
+ // from the props, so match them to start clean.
276
+ containerStore.brandPageTitle = 'Old title';
277
+ containerStore.brandPageDescription = 'Old description';
278
+ containerStore.isPublishing = true;
279
+
280
+ const wrapper = mount(SeoPageSettings, {
281
+ props: {
282
+ isMaker: false,
283
+ pageTitle: 'Old title',
284
+ pageDescription: 'Old description',
285
+ },
286
+ });
287
+ const titleInput = wrapper.findComponent(TextInput);
288
+ const descriptionInput = wrapper.findComponent(Textarea);
289
+
290
+ expect(titleInput.props('disabled')).toBe(false);
291
+ expect(descriptionInput.props('disabled')).toBe(false);
292
+
293
+ await titleInput.find('input').setValue('New title');
294
+ await descriptionInput.find('textarea').setValue('New description');
295
+
296
+ expect(titleInput.props('modelValue')).toBe('New title');
297
+ expect(descriptionInput.props('modelValue')).toBe('New description');
298
+
299
+ const updateButton = wrapper.find('[data-test-btn-update-seo-settings]');
300
+
301
+ // The Update button keeps its unscoped isPublishing gate from develop;
302
+ // the form started clean, so releasing it shows the typing made it dirty.
303
+ expect(updateButton.attributes('disabled')).toBeDefined();
304
+
305
+ containerStore.isPublishing = false;
306
+ await wrapper.vm.$nextTick();
307
+
308
+ expect(updateButton.attributes('disabled')).toBeUndefined();
309
+ });
310
+
311
+ test('admin template editor', async () => {
312
+ containerStore.isAdminMaker = true;
313
+
314
+ const wrapper = await mountSettings();
315
+
316
+ expect(wrapper.find('[data-test-btn-update-seo-settings]').exists()).toBe(
317
+ false,
318
+ );
319
+ expect(wrapper.findComponent(Textarea).exists()).toBe(false);
320
+ expect(radios(wrapper)).toHaveLength(0);
321
+ expect(faviconField(wrapper)).toBeDefined();
322
+ });
323
+ });
@@ -31,14 +31,14 @@
31
31
  :is-collapsed="true"
32
32
  >
33
33
  <PageSettings
34
- v-if="designDraftToken"
34
+ v-if="!isAdminMaker"
35
35
  :is-maker="true"
36
36
  @update-settings="onUpdateSettings"
37
37
  />
38
38
  <div
39
39
  class="tw-border-0 tw-border-solid tw-border-grayscale-400 tw-mt-6"
40
40
  :class="{
41
- 'tw-border-t tw-py-8': designDraftToken,
41
+ 'tw-border-t tw-py-8': !isAdminMaker,
42
42
  }"
43
43
  >
44
44
  <h3 class="tw-text-left tw-font-bold tw-mb-2">
@@ -117,7 +117,7 @@
117
117
  </div>
118
118
  </CollapsiblePanel>
119
119
  <CollapsiblePanel
120
- v-if="designDraftToken"
120
+ v-if="!isAdminMaker"
121
121
  data-component="collapsible-panel"
122
122
  :title="commonTr('sensitiveContent')"
123
123
  :is-collapsed="true"
@@ -1,4 +1,5 @@
1
1
  import { defineComponent } from 'vue';
2
+ import { setLocaleAsync } from '@src/composables/useTranslate';
2
3
  import ColorPickerV2 from './BpColorPickerV2.vue';
3
4
  import ColorModeSelect from './components/ColorModeSelect.vue';
4
5
  import GradientTypeSelector from './components/GradientTypeSelector.vue';
@@ -7,6 +8,15 @@ import GradientColorSlider from './components/GradientColorSlider.vue';
7
8
  export default {
8
9
  title: 'Components/BpColorPicker',
9
10
  component: ColorPickerV2,
11
+ // All of this file's stories render labels looked up via colorPickerTr; with
12
+ // i18next 25, t() returns undefined before setLocaleAsync runs. A meta-level
13
+ // loader awaits it once for every story, including BpColorPickerV2Sample's
14
+ // ColorModeSelect/GradientTypeSelector rendered inside the popup.
15
+ loaders: [
16
+ async ({ globals }) => {
17
+ await setLocaleAsync(globals.locale);
18
+ },
19
+ ],
10
20
  };
11
21
 
12
22
  export const ColorModeSelectComponent = () => {
@@ -7,11 +7,11 @@ export const COLOR_PICKER_MODE = {
7
7
 
8
8
  export const COLOR_PICKER_MODE_OPTIONS = [
9
9
  {
10
- displayName: 'Solid Color',
10
+ labelKey: 'colorModeSolid',
11
11
  value: COLOR_PICKER_MODE.SOLID,
12
12
  },
13
13
  {
14
- displayName: 'Gradient Color',
14
+ labelKey: 'colorModeGradient',
15
15
  value: COLOR_PICKER_MODE.GRADIENT,
16
16
  },
17
17
  ];
@@ -0,0 +1,116 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import i18next from 'i18next';
3
+ import { describe, expect, test } from 'vitest';
4
+ import {
5
+ setLocaleAsync,
6
+ supportedLocales,
7
+ type LocaleTypes,
8
+ } from '@src/composables/useTranslate';
9
+ import ColorModeSelect from './ColorModeSelect.vue';
10
+ import en from '@src/shared/color-picker/components/i18n/color-picker.json';
11
+ import deDE from '@src/shared/color-picker/components/i18n/color-picker.de-DE.json';
12
+ import esES from '@src/shared/color-picker/components/i18n/color-picker.es-ES.json';
13
+ import frCA from '@src/shared/color-picker/components/i18n/color-picker.fr-CA.json';
14
+ import frFR from '@src/shared/color-picker/components/i18n/color-picker.fr-FR.json';
15
+ import ptBR from '@src/shared/color-picker/components/i18n/color-picker.pt-BR.json';
16
+ import ptPT from '@src/shared/color-picker/components/i18n/color-picker.pt-PT.json';
17
+
18
+ // Expected values come from the locale files, not literals, so Smartling's
19
+ // rewrite of the wording can't turn these tests red.
20
+ const localeFiles: Record<LocaleTypes, typeof en> = {
21
+ 'en-US': en,
22
+ 'de-DE': deDE,
23
+ 'es-ES': esES,
24
+ 'fr-CA': frCA,
25
+ 'fr-FR': frFR,
26
+ 'pt-BR': ptBR,
27
+ 'pt-PT': ptPT,
28
+ };
29
+
30
+ const stringsFor = (locale: LocaleTypes): (typeof en)['colorPicker'] =>
31
+ localeFiles[locale].colorPicker;
32
+
33
+ const NON_ENGLISH_LOCALES = supportedLocales.filter(
34
+ (locale) => locale !== 'en-US',
35
+ );
36
+
37
+ // See RecaptchaDisclosure.vue.test.ts: the shared i18next singleton only takes
38
+ // the language from `setLocaleAsync` on its first call.
39
+ const useLocale = async (locale: string): Promise<void> => {
40
+ await setLocaleAsync(locale as LocaleTypes);
41
+ await i18next.changeLanguage(locale);
42
+ };
43
+
44
+ const optionTexts = (wrapper: ReturnType<typeof mount>): string[] =>
45
+ wrapper.findAll('option').map((option) => option.text());
46
+
47
+ const mountSelect = (modelValue?: string): ReturnType<typeof mount> =>
48
+ mount(ColorModeSelect, { props: { modelValue } });
49
+
50
+ describe('ColorModeSelect', () => {
51
+ test.each(NON_ENGLISH_LOCALES)(
52
+ 'dropdown options use the %s locale file',
53
+ async (locale) => {
54
+ await useLocale(locale);
55
+
56
+ const wrapper = mountSelect();
57
+
58
+ expect(optionTexts(wrapper)).toEqual([
59
+ stringsFor(locale).colorModeSolid,
60
+ stringsFor(locale).colorModeGradient,
61
+ ]);
62
+ optionTexts(wrapper).forEach((text) => {
63
+ expect(text).not.toMatch(/^colorPicker\./);
64
+ });
65
+ },
66
+ );
67
+
68
+ test('English dropdown options read word for word', async () => {
69
+ await useLocale('en-US');
70
+
71
+ const wrapper = mountSelect();
72
+
73
+ expect(optionTexts(wrapper)).toEqual(['Solid Color', 'Gradient Color']);
74
+ });
75
+
76
+ // A key missing from one locale file renders as its key path on the site;
77
+ // the shared test singleton would hide it behind the en-US fallback.
78
+ test.each(supportedLocales)(
79
+ 'every locale file defines the colour-mode keys: %s',
80
+ (locale) => {
81
+ (['colorModeSolid', 'colorModeGradient'] as const).forEach((key) => {
82
+ const value = stringsFor(locale)[key];
83
+
84
+ expect(typeof value, `${locale} › ${key}`).toBe('string');
85
+ expect(value.trim(), `${locale} › ${key}`).not.toBe('');
86
+ });
87
+ },
88
+ );
89
+
90
+ test('a gradient colour opens on the translated gradient option', async () => {
91
+ await useLocale('de-DE');
92
+
93
+ const wrapper = mountSelect('gradient');
94
+ const select = wrapper.find('select');
95
+
96
+ expect((select.element as HTMLSelectElement).value).toBe('gradient');
97
+ expect(wrapper.find('option:checked').text()).toBe(
98
+ stringsFor('de-DE').colorModeGradient,
99
+ );
100
+ });
101
+
102
+ test('picking an option emits the same value as today', async () => {
103
+ await useLocale('de-DE');
104
+
105
+ const wrapper = mountSelect('solid');
106
+ const select = wrapper.find('select');
107
+
108
+ await select.setValue('gradient');
109
+ await select.setValue('solid');
110
+
111
+ expect(wrapper.emitted('update:modelValue')).toEqual([
112
+ ['gradient'],
113
+ ['solid'],
114
+ ]);
115
+ });
116
+ });