@designcrowd/library.brand-page 5.19.1-id-based-items-2 → 5.19.2
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-Ruu5Az32.js → CreatedOnBrandCrowd-de-de-Ekmn0O3s.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DGUJtvld.js → CreatedOnBrandCrowd-en-us-BjeXlKTS.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dg-1uOfV.js → CreatedOnBrandCrowd-es-es-rpNINAMY.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CRKraZl8.js → CreatedOnBrandCrowd-fr-ca-BL4nSaLj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CRKraZl8.js → CreatedOnBrandCrowd-fr-fr-BL4nSaLj.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-br-K0r4FyYq.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-pt-K0r4FyYq.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CJYfwZx8.js → CreatedOnDcom-de-de-0XYyEKYc.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-rzA1lVxH.js → CreatedOnDcom-en-us-E8hvwiMd.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CNwGPjik.js → CreatedOnDcom-es-es-pRuCd7xX.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-DKw7OGIz.js → CreatedOnDcom-fr-ca-Bu4V3jpn.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-WzOEaAZG.js → CreatedOnDcom-fr-fr-CCcrGNz2.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DkualVhm.js → CreatedOnDcom-pt-br-1GTvaLKd.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-w4Nnsdzq.js → CreatedOnDcom-pt-pt-CTIYW10X.js} +1 -1
- package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
- package/lib/src/brand-page/models/index.d.ts +1 -12
- package/lib/src/brand-page/models/index.js +3 -0
- package/lib/src/brand-page/utils/personalisation.util.js +72 -14
- package/lib/src/brand-page-maker/components/action-bar/ActionBar.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/admin-background-settings/AdminBackgroundSettings.mixin.d.ts +6 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.d.ts +12 -1
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.js +2 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +1 -0
- package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMakerTemplateSettings.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/components/brandpage-settings/ReplaceImageSettingsForBrandPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/SeoPageSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/seo-settings/UrlSettings.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/social-share/SocialShare.mixin.d.ts +1 -1
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.d.ts +2 -3
- package/lib/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.js +16 -5
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +0 -5
- package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +6 -18
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.d.ts +1 -0
- package/lib/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.js +3 -0
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.d.ts +3 -1
- package/lib/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.js +5 -1
- package/lib/src/brand-page-maker/stores/ui/ui-store.d.ts +6 -0
- package/lib/src/brand-page-maker/stores/ui/ui-store.js +8 -0
- package/lib/src/clients/design-template-client.d.ts +3 -0
- package/lib/src/clients/models.d.ts +2 -0
- package/lib/src/css/library.brand-page-brandCrowd.min.css +19 -7
- package/lib/src/css/library.brand-page-brandPage.min.css +19 -7
- package/lib/src/css/library.brand-page-designCom.min.css +19 -7
- package/lib/src/css/library.brand-page-preview.min.css +19 -7
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-Dy7MYyqz.js → lib-BfgUqu0B.js} +25060 -24791
- package/lib/src/metadata-builder/builders/footer-builder.js +44 -34
- package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +68 -57
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +93 -58
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +99 -66
- package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +17 -57
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +85 -51
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +74 -49
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +4 -13
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +48 -66
- 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 +7 -1
- package/lib/src/shared/logo-settings/components/EditLogoSettings.mixin.d.ts +9 -0
- package/lib/src/shared/logo-settings/components/LogoSettings.mixin.d.ts +1 -1
- package/lib/src/shared/logo-versions/LogoVersions.mixin.d.ts +6 -0
- package/lib/src/shared/models.d.ts +2 -0
- package/lib/src/shared/my-designs/MyDesigns.mixin.d.ts +6 -0
- package/lib/src/shared/my-uploads/MyMedia.mixin.d.ts +6 -0
- package/lib/src/shared/my-uploads/MyUploads.mixin.d.ts +6 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +7 -3
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +7 -3
- package/lib/src/shared/publish-brand-page-with-state/PublishBrandPageModalWithState.mixin.d.ts +1 -1
- package/lib/src/shared/style-settings/components/StyleSettings.mixin.d.ts +7 -1
- package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.d.ts +10 -439
- package/lib/src/shared/template-settings/components/TemplatesSettings.mixin.js +2 -2
- package/lib/src/shared/template-settings/models.d.ts +1 -0
- package/lib/src/shared/template-settings/store/template-settings-store.d.ts +4 -0
- package/lib/src/shared/template-settings/store/template-settings-store.js +79 -11
- package/lib/src/shared/unsplash-library/UnsplashLibrary.mixin.d.ts +6 -0
- package/lib/src/utils/minify-brand-page-metadata.d.ts +0 -2
- package/lib/src/utils/minify-brand-page-metadata.js +1 -8
- package/package.json +1 -1
- package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +1 -2
- package/src/brand-page/models/index.ts +4 -12
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +3 -0
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +1 -0
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +2 -0
- package/src/brand-page/utils/personalisation.util.test.ts +35 -0
- package/src/brand-page/utils/personalisation.util.ts +119 -23
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.mixin.ts +4 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
- package/src/brand-page-maker/components/template-preview/TemplatePreview.mixin.ts +22 -8
- package/src/brand-page-maker/components/template-preview/TemplatePreview.vue +5 -10
- package/src/brand-page-maker/components/template-preview/TemplatePreviewAiBadge.vue +12 -0
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +0 -6
- package/src/brand-page-maker/stores/brandpage-container/brand-page-container-store.ts +5 -0
- package/src/brand-page-maker/stores/brandpage-maker/brandpage-maker-store.ts +10 -0
- package/src/brand-page-maker/stores/ui/ui-store.ts +16 -0
- package/src/clients/design-template-client.ts +3 -0
- package/src/clients/models.ts +2 -0
- package/src/metadata-builder/builders/footer-builder.test.ts +0 -298
- package/src/metadata-builder/builders/footer-builder.ts +65 -56
- package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +88 -62
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +129 -66
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +160 -105
- package/src/metadata-builder/builders/section-builders/social-links-builder.ts +20 -65
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +146 -83
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +122 -70
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +4 -13
- package/src/metadata-builder/parsers/section-tool-arguments.ts +48 -66
- package/src/shared/models.ts +6 -0
- package/src/shared/template-settings/components/TemplatesSettings.mixin.ts +2 -2
- package/src/shared/template-settings/models.ts +1 -0
- package/src/shared/template-settings/store/template-settings-store.ts +105 -18
- package/src/utils/minify-brand-page-metadata.ts +2 -14
- package/lib/src/metadata-builder/builders/item-id-utils.d.ts +0 -54
- package/lib/src/metadata-builder/builders/item-id-utils.js +0 -96
- package/src/metadata-builder/builders/item-id-utils.test.ts +0 -184
- package/src/metadata-builder/builders/item-id-utils.ts +0 -117
package/src/clients/models.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface Paginated<T> {
|
|
|
4
4
|
pageSize: number;
|
|
5
5
|
pageCount: number;
|
|
6
6
|
totalResultCount: number;
|
|
7
|
+
abTestValuesToBePushed?: Record<string, string>;
|
|
7
8
|
}
|
|
8
9
|
|
|
9
10
|
export interface DesignTemplate {
|
|
@@ -14,6 +15,7 @@ export interface DesignTemplate {
|
|
|
14
15
|
|
|
15
16
|
export interface PaginatedDesignTemplateItem {
|
|
16
17
|
designTemplateToken: string;
|
|
18
|
+
isPersonalisable?: boolean;
|
|
17
19
|
previewImageUrl?: string;
|
|
18
20
|
}
|
|
19
21
|
|
|
@@ -14,10 +14,6 @@ import { FooterBuilder } from './footer-builder';
|
|
|
14
14
|
* The fix routes copyright style writes through `applyDetachedStyleOverride`,
|
|
15
15
|
* which resolves the preset in and clears `textStyleType` so the per-element
|
|
16
16
|
* override sticks.
|
|
17
|
-
*
|
|
18
|
-
* BP-5603: social link mutations converted from index-based to id-based
|
|
19
|
-
* (item_id / move_before_item_id / _remove_social_link_ids), mirroring the
|
|
20
|
-
* section-level move_before_content_id contract one level deeper.
|
|
21
17
|
*/
|
|
22
18
|
|
|
23
19
|
const textStyleOptions = {
|
|
@@ -46,37 +42,6 @@ const makeModel = (copyrightStyle: Record<string, unknown>): BrandPageModel =>
|
|
|
46
42
|
pageFooterMobile: { copyrightStyle: { ...copyrightStyle } },
|
|
47
43
|
}) as unknown as BrandPageModel;
|
|
48
44
|
|
|
49
|
-
const makeModelWithLinks = (
|
|
50
|
-
links: { id?: string; imageUrl: string; url: string }[],
|
|
51
|
-
): BrandPageModel =>
|
|
52
|
-
({
|
|
53
|
-
styles: { textStyles: { textStyleOptions } },
|
|
54
|
-
pageFooterDesktop: {
|
|
55
|
-
copyrightStyle: {},
|
|
56
|
-
socialLinks: links.map((l) => ({
|
|
57
|
-
id: l.id,
|
|
58
|
-
imageUrl: l.imageUrl,
|
|
59
|
-
url: l.url,
|
|
60
|
-
imageTitle: '',
|
|
61
|
-
width: 0,
|
|
62
|
-
height: 0,
|
|
63
|
-
imageLink: '',
|
|
64
|
-
})),
|
|
65
|
-
},
|
|
66
|
-
pageFooterMobile: {
|
|
67
|
-
copyrightStyle: {},
|
|
68
|
-
socialLinks: links.map((l) => ({
|
|
69
|
-
id: l.id,
|
|
70
|
-
imageUrl: l.imageUrl,
|
|
71
|
-
url: l.url,
|
|
72
|
-
imageTitle: '',
|
|
73
|
-
width: 0,
|
|
74
|
-
height: 0,
|
|
75
|
-
imageLink: '',
|
|
76
|
-
})),
|
|
77
|
-
},
|
|
78
|
-
}) as unknown as BrandPageModel;
|
|
79
|
-
|
|
80
45
|
const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
|
|
81
46
|
new FooterBuilder({
|
|
82
47
|
id: 'f1',
|
|
@@ -84,10 +49,6 @@ const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
|
|
|
84
49
|
payload: args,
|
|
85
50
|
}).buildAsync(model);
|
|
86
51
|
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
// Copyright font (BP-5420)
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
|
|
91
52
|
describe('FooterBuilder — copyright font vs global style', () => {
|
|
92
53
|
it('detaches from the global preset: applies the font, clears textStyleType, retains preset values', async () => {
|
|
93
54
|
const model = makeModel({
|
|
@@ -113,262 +74,3 @@ describe('FooterBuilder — copyright font vs global style', () => {
|
|
|
113
74
|
expect(style.textAlign).toBe('center');
|
|
114
75
|
});
|
|
115
76
|
});
|
|
116
|
-
|
|
117
|
-
// ---------------------------------------------------------------------------
|
|
118
|
-
// Social links — id-based mutations (BP-5603)
|
|
119
|
-
// ---------------------------------------------------------------------------
|
|
120
|
-
|
|
121
|
-
describe('FooterBuilder — social links id-based mutations', () => {
|
|
122
|
-
it('backfills ids on legacy links that have none', async () => {
|
|
123
|
-
// Links without ids (legacy persisted data)
|
|
124
|
-
const model = makeModelWithLinks([
|
|
125
|
-
{ imageUrl: 'facebook', url: 'https://fb.com' },
|
|
126
|
-
{ imageUrl: 'instagram', url: 'https://ig.com' },
|
|
127
|
-
]);
|
|
128
|
-
|
|
129
|
-
const { model: out } = await run({}, model);
|
|
130
|
-
|
|
131
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
132
|
-
|
|
133
|
-
expect(links[0].id).toBeTruthy();
|
|
134
|
-
expect(links[1].id).toBeTruthy();
|
|
135
|
-
expect(links[0].id).not.toBe(links[1].id);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('updates an existing link in place by item_id', async () => {
|
|
139
|
-
const model = makeModelWithLinks([
|
|
140
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
141
|
-
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
142
|
-
]);
|
|
143
|
-
|
|
144
|
-
const { model: out } = await run(
|
|
145
|
-
{
|
|
146
|
-
social_links: [
|
|
147
|
-
{ item_id: 'link-1', url: 'https://facebook.com/mybrand' },
|
|
148
|
-
],
|
|
149
|
-
},
|
|
150
|
-
model,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
154
|
-
|
|
155
|
-
expect(links).toHaveLength(2);
|
|
156
|
-
expect(links[0].url).toBe('https://facebook.com/mybrand');
|
|
157
|
-
expect(links[0].id).toBe('link-1');
|
|
158
|
-
// untouched link unchanged
|
|
159
|
-
expect(links[1].id).toBe('link-2');
|
|
160
|
-
expect(links[1].url).toBe('https://ig.com');
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it('updates social_link_icon on an existing link', async () => {
|
|
164
|
-
const model = makeModelWithLinks([
|
|
165
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
166
|
-
]);
|
|
167
|
-
|
|
168
|
-
const { model: out } = await run(
|
|
169
|
-
{
|
|
170
|
-
social_links: [{ item_id: 'link-1', social_link_icon: 'facebook-v2' }],
|
|
171
|
-
},
|
|
172
|
-
model,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
expect(out.pageFooterDesktop!.socialLinks[0].imageUrl).toBe('facebook-v2');
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('skips unknown item_id silently', async () => {
|
|
179
|
-
const model = makeModelWithLinks([
|
|
180
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
181
|
-
]);
|
|
182
|
-
|
|
183
|
-
const { model: out } = await run(
|
|
184
|
-
{
|
|
185
|
-
social_links: [{ item_id: 'no-such-id', url: 'https://nowhere.com' }],
|
|
186
|
-
},
|
|
187
|
-
model,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
191
|
-
|
|
192
|
-
expect(links).toHaveLength(1);
|
|
193
|
-
expect(links[0].url).toBe('https://fb.com');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('inserts a new link (no item_id) appended by default', async () => {
|
|
197
|
-
const model = makeModelWithLinks([
|
|
198
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
199
|
-
]);
|
|
200
|
-
|
|
201
|
-
const { model: out } = await run(
|
|
202
|
-
{
|
|
203
|
-
social_links: [
|
|
204
|
-
{ social_link_icon: 'instagram', url: 'https://ig.com' },
|
|
205
|
-
],
|
|
206
|
-
},
|
|
207
|
-
model,
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
211
|
-
|
|
212
|
-
expect(links).toHaveLength(2);
|
|
213
|
-
expect(links[1].imageUrl).toBe('instagram');
|
|
214
|
-
expect(links[1].id).toBeTruthy();
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
it('inserts a new link before an anchor via move_before_item_id', async () => {
|
|
218
|
-
const model = makeModelWithLinks([
|
|
219
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
220
|
-
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
221
|
-
]);
|
|
222
|
-
|
|
223
|
-
const { model: out } = await run(
|
|
224
|
-
{
|
|
225
|
-
social_links: [
|
|
226
|
-
{
|
|
227
|
-
social_link_icon: 'x',
|
|
228
|
-
url: 'https://x.com',
|
|
229
|
-
move_before_item_id: 'link-2',
|
|
230
|
-
},
|
|
231
|
-
],
|
|
232
|
-
},
|
|
233
|
-
model,
|
|
234
|
-
);
|
|
235
|
-
|
|
236
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
237
|
-
|
|
238
|
-
expect(links).toHaveLength(3);
|
|
239
|
-
expect(links[0].id).toBe('link-1');
|
|
240
|
-
expect(links[1].imageUrl).toBe('x');
|
|
241
|
-
expect(links[2].id).toBe('link-2');
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('inserts a new link at end via move_before_item_id "__end__"', async () => {
|
|
245
|
-
const model = makeModelWithLinks([
|
|
246
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
247
|
-
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
248
|
-
]);
|
|
249
|
-
|
|
250
|
-
const { model: out } = await run(
|
|
251
|
-
{
|
|
252
|
-
social_links: [
|
|
253
|
-
{
|
|
254
|
-
social_link_icon: 'x',
|
|
255
|
-
url: 'https://x.com',
|
|
256
|
-
move_before_item_id: '__end__',
|
|
257
|
-
},
|
|
258
|
-
],
|
|
259
|
-
},
|
|
260
|
-
model,
|
|
261
|
-
);
|
|
262
|
-
|
|
263
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
264
|
-
|
|
265
|
-
expect(links).toHaveLength(3);
|
|
266
|
-
expect(links[2].imageUrl).toBe('x');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('moves an existing link before an anchor via move_before_item_id', async () => {
|
|
270
|
-
const model = makeModelWithLinks([
|
|
271
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
272
|
-
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
273
|
-
{ id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
|
|
274
|
-
]);
|
|
275
|
-
|
|
276
|
-
const { model: out } = await run(
|
|
277
|
-
{
|
|
278
|
-
social_links: [{ item_id: 'link-3', move_before_item_id: 'link-1' }],
|
|
279
|
-
},
|
|
280
|
-
model,
|
|
281
|
-
);
|
|
282
|
-
|
|
283
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
284
|
-
|
|
285
|
-
expect(links.map((l) => l.id)).toEqual(['link-3', 'link-1', 'link-2']);
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
it('removes links by id', async () => {
|
|
289
|
-
const model = makeModelWithLinks([
|
|
290
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
291
|
-
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
292
|
-
{ id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
|
|
293
|
-
]);
|
|
294
|
-
|
|
295
|
-
const { model: out } = await run(
|
|
296
|
-
{ _remove_social_link_ids: ['link-1', 'link-3'] },
|
|
297
|
-
model,
|
|
298
|
-
);
|
|
299
|
-
|
|
300
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
301
|
-
|
|
302
|
-
expect(links).toHaveLength(1);
|
|
303
|
-
expect(links[0].id).toBe('link-2');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
it('_replace_social_links rebuilds the entire list and assigns fresh ids', async () => {
|
|
307
|
-
const model = makeModelWithLinks([
|
|
308
|
-
{ id: 'old-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
309
|
-
]);
|
|
310
|
-
|
|
311
|
-
const { model: out } = await run(
|
|
312
|
-
{
|
|
313
|
-
_replace_social_links: true,
|
|
314
|
-
social_links: [
|
|
315
|
-
{ social_link_icon: 'instagram', url: 'https://ig.com' },
|
|
316
|
-
{ social_link_icon: 'x', url: 'https://x.com' },
|
|
317
|
-
],
|
|
318
|
-
},
|
|
319
|
-
model,
|
|
320
|
-
);
|
|
321
|
-
|
|
322
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
323
|
-
|
|
324
|
-
expect(links).toHaveLength(2);
|
|
325
|
-
expect(links[0].imageUrl).toBe('instagram');
|
|
326
|
-
expect(links[1].imageUrl).toBe('x');
|
|
327
|
-
// fresh ids assigned, not the old one
|
|
328
|
-
expect(links[0].id).toBeTruthy();
|
|
329
|
-
expect(links[0].id).not.toBe('old-1');
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('_replace_social_links with empty social_links clears all', async () => {
|
|
333
|
-
const model = makeModelWithLinks([
|
|
334
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
335
|
-
]);
|
|
336
|
-
|
|
337
|
-
const { model: out } = await run({ _replace_social_links: true }, model);
|
|
338
|
-
|
|
339
|
-
expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('normalises mailto: and tel: prefixes on new links', async () => {
|
|
343
|
-
const model = makeModelWithLinks([]);
|
|
344
|
-
|
|
345
|
-
const { model: out } = await run(
|
|
346
|
-
{
|
|
347
|
-
social_links: [
|
|
348
|
-
{ social_link_icon: 'email', url: 'mailto:me@example.com' },
|
|
349
|
-
{ social_link_icon: 'phone', url: 'tel:+1234567890' },
|
|
350
|
-
],
|
|
351
|
-
},
|
|
352
|
-
model,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
|
-
const links = out.pageFooterDesktop!.socialLinks;
|
|
356
|
-
|
|
357
|
-
expect(links[0].url).toBe('me@example.com');
|
|
358
|
-
expect(links[1].url).toBe('+1234567890');
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
it('applies mutations to both desktop and mobile footers', async () => {
|
|
362
|
-
const model = makeModelWithLinks([
|
|
363
|
-
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
364
|
-
]);
|
|
365
|
-
|
|
366
|
-
const { model: out } = await run(
|
|
367
|
-
{ _remove_social_link_ids: ['link-1'] },
|
|
368
|
-
model,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
|
|
372
|
-
expect(out.pageFooterMobile!.socialLinks).toHaveLength(0);
|
|
373
|
-
});
|
|
374
|
-
});
|
|
@@ -10,17 +10,12 @@ import {
|
|
|
10
10
|
import type {
|
|
11
11
|
UpdateFooterArguments,
|
|
12
12
|
ViewportFooterOverrides,
|
|
13
|
+
FooterSocialLinkItem,
|
|
13
14
|
} from '../parsers/ai-toolcall-parser';
|
|
14
15
|
import type { Logger } from '../logger';
|
|
15
16
|
import type { TextStyleOptions } from '../../brand-page/models/styles.model';
|
|
16
17
|
import { FOOTER_CHECKS } from './section-rules/footer.rules';
|
|
17
18
|
import { applyDetachedStyleOverride } from './section-field-utils';
|
|
18
|
-
import {
|
|
19
|
-
ensureItemIds,
|
|
20
|
-
generateItemId,
|
|
21
|
-
removeItemsByIds,
|
|
22
|
-
repositionItemByAnchor,
|
|
23
|
-
} from './item-id-utils';
|
|
24
19
|
|
|
25
20
|
const normaliseSocialUrl = (url: string): string => {
|
|
26
21
|
for (const prefix of ['mailto:', 'tel:']) {
|
|
@@ -52,7 +47,6 @@ const applyGlobalFooterChanges = (
|
|
|
52
47
|
// they are superseded by the replacement and running them would cause duplicates.
|
|
53
48
|
if (args._replace_social_links) {
|
|
54
49
|
footer.socialLinks = (links ?? []).map((l) => ({
|
|
55
|
-
id: generateItemId(),
|
|
56
50
|
imageUrl: l.social_link_icon || '',
|
|
57
51
|
url: normaliseSocialUrl(l.url || ''),
|
|
58
52
|
imageTitle: '',
|
|
@@ -62,59 +56,74 @@ const applyGlobalFooterChanges = (
|
|
|
62
56
|
}));
|
|
63
57
|
} else {
|
|
64
58
|
// Targeted mutations — only run when NOT replacing the entire list.
|
|
65
|
-
// BP-5603: id-based contract (item_id / move_before_item_id / _remove_social_link_ids).
|
|
66
|
-
// Backfill ids on legacy links so they become referenceable after this call.
|
|
67
|
-
ensureItemIds(footer.socialLinks);
|
|
68
|
-
|
|
69
|
-
// Remove by id (order-independent — no index shifting needed).
|
|
70
|
-
footer.socialLinks = removeItemsByIds(
|
|
71
|
-
footer.socialLinks,
|
|
72
|
-
args._remove_social_link_ids,
|
|
73
|
-
);
|
|
74
59
|
|
|
75
|
-
//
|
|
60
|
+
// item_index-based updates for existing links
|
|
76
61
|
if (links?.length) {
|
|
62
|
+
for (const link of links) {
|
|
63
|
+
if (link.item_index === undefined) continue;
|
|
64
|
+
|
|
65
|
+
const existing = footer.socialLinks?.[link.item_index];
|
|
66
|
+
|
|
67
|
+
if (!existing) continue;
|
|
68
|
+
|
|
69
|
+
if (link.social_link_icon !== undefined)
|
|
70
|
+
existing.imageUrl = link.social_link_icon;
|
|
71
|
+
|
|
72
|
+
if (link.url !== undefined) existing.url = normaliseSocialUrl(link.url);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Remove by index (descending order to preserve indices during splice)
|
|
77
|
+
if (args._remove_social_link_indices?.length && footer.socialLinks) {
|
|
78
|
+
const sorted = [...args._remove_social_link_indices].sort(
|
|
79
|
+
(a, b) => b - a,
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
for (const idx of sorted) {
|
|
83
|
+
if (idx >= 0 && idx < footer.socialLinks.length)
|
|
84
|
+
footer.socialLinks.splice(idx, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Insert new links (descending insert_at order to preserve positions during splice)
|
|
89
|
+
const insertLinks = (links ?? []).filter(
|
|
90
|
+
(l): l is FooterSocialLinkItem & { insert_at: number } =>
|
|
91
|
+
l.insert_at !== undefined && l.item_index === undefined,
|
|
92
|
+
);
|
|
93
|
+
const sortedInserts = [...insertLinks].sort(
|
|
94
|
+
(a, b) => b.insert_at - a.insert_at,
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
for (const link of sortedInserts) {
|
|
98
|
+
const pos = Math.max(
|
|
99
|
+
0,
|
|
100
|
+
Math.min(link.insert_at, footer.socialLinks?.length ?? 0),
|
|
101
|
+
);
|
|
102
|
+
|
|
77
103
|
footer.socialLinks = footer.socialLinks ?? [];
|
|
104
|
+
footer.socialLinks.splice(pos, 0, {
|
|
105
|
+
imageUrl: link.social_link_icon || '',
|
|
106
|
+
url: normaliseSocialUrl(link.url || ''),
|
|
107
|
+
imageTitle: '',
|
|
108
|
+
width: 0,
|
|
109
|
+
height: 0,
|
|
110
|
+
imageLink: '',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
78
113
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (link.url !== undefined)
|
|
93
|
-
existing.url = normaliseSocialUrl(link.url);
|
|
94
|
-
|
|
95
|
-
repositionItemByAnchor(
|
|
96
|
-
footer.socialLinks,
|
|
97
|
-
existing,
|
|
98
|
-
link.move_before_item_id,
|
|
99
|
-
);
|
|
100
|
-
} else {
|
|
101
|
-
// New link — assign a stable id and position via move_before_item_id.
|
|
102
|
-
const newLink = {
|
|
103
|
-
id: generateItemId(),
|
|
104
|
-
imageUrl: link.social_link_icon || '',
|
|
105
|
-
url: normaliseSocialUrl(link.url || ''),
|
|
106
|
-
imageTitle: '',
|
|
107
|
-
width: 0,
|
|
108
|
-
height: 0,
|
|
109
|
-
imageLink: '' as string | false,
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
repositionItemByAnchor(
|
|
113
|
-
footer.socialLinks,
|
|
114
|
-
newLink,
|
|
115
|
-
link.move_before_item_id,
|
|
116
|
-
);
|
|
117
|
-
}
|
|
114
|
+
// Reorder (requires ALL indices — partial reorder is not supported)
|
|
115
|
+
if (args._reorder_social_links?.length && footer.socialLinks) {
|
|
116
|
+
const newOrder = args._reorder_social_links;
|
|
117
|
+
const unique = new Set(newOrder);
|
|
118
|
+
|
|
119
|
+
if (
|
|
120
|
+
unique.size === newOrder.length &&
|
|
121
|
+
newOrder.every((idx) => idx >= 0 && idx < footer.socialLinks!.length)
|
|
122
|
+
) {
|
|
123
|
+
const reordered = newOrder.map((idx) => footer.socialLinks![idx]);
|
|
124
|
+
|
|
125
|
+
if (reordered.length === footer.socialLinks.length)
|
|
126
|
+
footer.socialLinks = reordered;
|
|
118
127
|
}
|
|
119
128
|
}
|
|
120
129
|
}
|
|
@@ -10,12 +10,6 @@ import type { BuilderResult } from '../../models';
|
|
|
10
10
|
import { applyScalarFields, isValidUrl } from '../section-field-utils';
|
|
11
11
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
12
12
|
import { validateImageGallery } from '../section-rules/image-gallery.rules';
|
|
13
|
-
import {
|
|
14
|
-
ensureItemIds,
|
|
15
|
-
generateItemId,
|
|
16
|
-
removeItemsByIds,
|
|
17
|
-
repositionItemByAnchor,
|
|
18
|
-
} from '../item-id-utils';
|
|
19
13
|
|
|
20
14
|
const applyGalleryType = (
|
|
21
15
|
content: ImageGalleryContent,
|
|
@@ -27,17 +21,11 @@ const applyGalleryType = (
|
|
|
27
21
|
};
|
|
28
22
|
};
|
|
29
23
|
|
|
30
|
-
/**
|
|
31
|
-
* Full rebuild: discard existing images and map the payload directly.
|
|
32
|
-
* Used on create and when `_replace_items` is set. Each item gets a fresh id
|
|
33
|
-
* (or keeps the caller-supplied `item_id` when provided).
|
|
34
|
-
*/
|
|
35
24
|
const applyGalleryImages = (
|
|
36
25
|
content: ImageGalleryContent,
|
|
37
26
|
images: ImageGalleryItem[],
|
|
38
27
|
): void => {
|
|
39
28
|
content.images = images.map((img) => ({
|
|
40
|
-
id: img.item_id || generateItemId(),
|
|
41
29
|
imageUrl: isValidUrl(img.image_url) ? img.image_url! : '',
|
|
42
30
|
imageTitle: img.caption || '',
|
|
43
31
|
width: 0,
|
|
@@ -47,53 +35,84 @@ const applyGalleryImages = (
|
|
|
47
35
|
}));
|
|
48
36
|
};
|
|
49
37
|
|
|
50
|
-
/**
|
|
51
|
-
* Id-based merge (BP-5603), mirroring testimonial-section-builder one level
|
|
52
|
-
* deeper into `content.images`:
|
|
53
|
-
* - `item_id` matches existing item → update in place; reposition if
|
|
54
|
-
* `move_before_item_id` is set.
|
|
55
|
-
* - no `item_id` → add new item, positioned by
|
|
56
|
-
* `move_before_item_id`.
|
|
57
|
-
* - `removeIds` → delete by id (order-independent).
|
|
58
|
-
*
|
|
59
|
-
* Existing items are id-backfilled first so legacy galleries become
|
|
60
|
-
* referenceable as soon as the metadata builder touches them.
|
|
61
|
-
*/
|
|
62
38
|
const mergeGalleryImages = (
|
|
63
39
|
content: ImageGalleryContent,
|
|
64
40
|
images: ImageGalleryItem[],
|
|
65
|
-
|
|
41
|
+
replaceAll: boolean,
|
|
66
42
|
): void => {
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
const itemsToMerge = images.filter((img) => img.insert_at === undefined);
|
|
44
|
+
|
|
45
|
+
const updatedImages = itemsToMerge.map((img, index) => {
|
|
46
|
+
const existing = content.images?.[index];
|
|
47
|
+
const imageUrl = isValidUrl(img.image_url)
|
|
48
|
+
? img.image_url!
|
|
49
|
+
: existing?.imageUrl ?? '';
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...(existing || {}),
|
|
53
|
+
imageUrl,
|
|
54
|
+
imageTitle: img.caption ?? existing?.imageTitle ?? '',
|
|
55
|
+
width: existing?.width ?? 0,
|
|
56
|
+
height: existing?.height ?? 0,
|
|
57
|
+
mediaType: existing?.mediaType ?? 'image',
|
|
58
|
+
imageLink: existing?.imageLink ?? '',
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!replaceAll &&
|
|
64
|
+
content.images &&
|
|
65
|
+
content.images.length > itemsToMerge.length
|
|
66
|
+
) {
|
|
67
|
+
const extras = content.images.slice(itemsToMerge.length).map((img) => ({
|
|
68
|
+
imageUrl: img.imageUrl ?? '',
|
|
69
|
+
imageTitle: img.imageTitle ?? '',
|
|
70
|
+
width: img.width ?? 0,
|
|
71
|
+
height: img.height ?? 0,
|
|
72
|
+
mediaType: img.mediaType ?? ('image' as const),
|
|
73
|
+
imageLink: img.imageLink ?? '',
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
updatedImages.push(...extras);
|
|
77
|
+
}
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const existing = content.images.find((i) => i.id === img.item_id);
|
|
79
|
+
content.images = updatedImages;
|
|
80
|
+
};
|
|
73
81
|
|
|
74
|
-
|
|
75
|
-
|
|
82
|
+
const insertGalleryImages = (
|
|
83
|
+
content: ImageGalleryContent,
|
|
84
|
+
images: ImageGalleryItem[],
|
|
85
|
+
): void => {
|
|
86
|
+
const inserts = images.filter((img) => img.insert_at !== undefined);
|
|
76
87
|
|
|
77
|
-
|
|
78
|
-
existing.imageUrl = isValidUrl(img.image_url) ? img.image_url! : '';
|
|
79
|
-
}
|
|
88
|
+
if (!inserts.length) return;
|
|
80
89
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
90
|
+
const sorted = [...inserts].sort(
|
|
91
|
+
(a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0),
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
for (const img of sorted) {
|
|
95
|
+
if (!isValidUrl(img.image_url)) continue;
|
|
96
|
+
|
|
97
|
+
if (content.images?.some((existing) => existing.imageUrl === img.image_url))
|
|
98
|
+
continue;
|
|
99
|
+
|
|
100
|
+
content.images = content.images ?? [];
|
|
101
|
+
|
|
102
|
+
const pos = Math.max(
|
|
103
|
+
0,
|
|
104
|
+
Math.min(img.insert_at ?? 0, content.images.length),
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
content.images.splice(pos, 0, {
|
|
108
|
+
imageUrl: img.image_url!,
|
|
109
|
+
imageTitle: img.caption ?? '',
|
|
110
|
+
width: 0,
|
|
111
|
+
height: 0,
|
|
112
|
+
mediaType: 'image',
|
|
113
|
+
imageLink: '',
|
|
114
|
+
colorReplacements: [],
|
|
115
|
+
});
|
|
97
116
|
}
|
|
98
117
|
};
|
|
99
118
|
|
|
@@ -125,19 +144,26 @@ export class ImageGalleryBuilder extends BaseSectionBuilder<ImageGalleryContent>
|
|
|
125
144
|
applyGalleryType(content, args.gallery_type);
|
|
126
145
|
}
|
|
127
146
|
|
|
128
|
-
if (args.
|
|
129
|
-
|
|
130
|
-
applyGalleryImages(content, args.images);
|
|
131
|
-
} else if (this.mode === 'create') {
|
|
132
|
-
if (args.images && Array.isArray(args.images)) {
|
|
147
|
+
if (args.images && Array.isArray(args.images)) {
|
|
148
|
+
if (this.mode === 'create') {
|
|
133
149
|
applyGalleryImages(content, args.images);
|
|
150
|
+
} else {
|
|
151
|
+
mergeGalleryImages(content, args.images, !!args._replace_items);
|
|
134
152
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (args._remove_item_indices?.length && content.images) {
|
|
156
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
157
|
+
|
|
158
|
+
for (const idx of sorted) {
|
|
159
|
+
if (idx >= 0 && idx < content.images.length) {
|
|
160
|
+
content.images.splice(idx, 1);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (args.images && Array.isArray(args.images) && this.mode === 'update') {
|
|
166
|
+
insertGalleryImages(content, args.images);
|
|
141
167
|
}
|
|
142
168
|
|
|
143
169
|
return { request: this.request, model };
|