@designcrowd/library.brand-page 6.0.12 → 6.0.13
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-BfQcnhof.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-B86JfNzV.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dy70BV7X.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-c257u4q-.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-c257u4q-.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-cRZ_Lx3_.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CrfHKR-1.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BFjhMPTR.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CZimjPcn.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-V1z3-WzE.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-NrmWKrvi.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CKLASXM9.js → CreatedOnDcom-pt-pt-DFsncYax.js} +1 -1
- package/lib/src/brand-page/components/image-gallery/ImageGallery.stories.js +3 -0
- package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
- package/lib/src/brand-page/components/images-section/ImagesSection.stories.js +2 -1
- package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +3 -0
- package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +3 -0
- package/lib/src/brand-page/components/text/Text.stories.js +2 -0
- package/lib/src/brand-page/models/index.d.ts +13 -0
- package/lib/src/brand-page/models/index.js +4 -0
- package/lib/src/brand-page/utils/migration/migration-01.js +2 -0
- package/lib/src/brand-page/utils/migration/migration-18.d.ts +14 -0
- package/lib/src/brand-page/utils/migration/migration-18.js +17 -0
- package/lib/src/brand-page/utils/migration.util.js +6 -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/__fixtures__/brand-page-examples.js +1 -0
- package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +5 -0
- package/lib/src/brand-page-maker/constants/config-settings-constant.js +4 -0
- package/lib/src/brand-page-maker/models/content-settings.js +4 -0
- package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +18 -0
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-CHfUKgt-.js → lib-BG-pyXi9.js} +19091 -18974
- package/lib/src/metadata-builder/ai-metadata-executor.js +24 -1
- package/lib/src/metadata-builder/builders/footer-builder.js +32 -41
- package/lib/src/metadata-builder/builders/item-id-utils.d.ts +54 -0
- package/lib/src/metadata-builder/builders/item-id-utils.js +96 -0
- package/lib/src/metadata-builder/builders/move-page-builder.d.ts +32 -0
- package/lib/src/metadata-builder/builders/move-page-builder.js +137 -0
- package/lib/src/metadata-builder/builders/nav-utils.d.ts +26 -0
- package/lib/src/metadata-builder/builders/nav-utils.js +95 -0
- package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +57 -68
- package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +58 -93
- package/lib/src/metadata-builder/builders/section-builders/move-section-builder.d.ts +13 -1
- package/lib/src/metadata-builder/builders/section-builders/move-section-builder.js +40 -30
- package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +66 -99
- package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +57 -17
- package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +51 -85
- package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +50 -74
- package/lib/src/metadata-builder/builders/section-field-maps.js +17 -0
- package/lib/src/metadata-builder/builders/set-page-visibility-builder.d.ts +23 -0
- package/lib/src/metadata-builder/builders/set-page-visibility-builder.js +83 -0
- package/lib/src/metadata-builder/metadata-executor.js +4 -0
- package/lib/src/metadata-builder/models.d.ts +2 -0
- package/lib/src/metadata-builder/models.js +2 -0
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +31 -7
- package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +2 -0
- package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +84 -48
- package/lib/src/shared/content-settings/components/links-field/LinksField.mixin.js +2 -0
- package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -0
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
- package/lib/src/utils/minify-brand-page-metadata.d.ts +2 -0
- package/lib/src/utils/minify-brand-page-metadata.js +8 -1
- package/package.json +1 -1
- package/src/brand-page/components/image-gallery/ImageGallery.stories.ts +3 -0
- package/src/brand-page/components/image-gallery/ImageGallery.test.ts +1 -0
- package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +2 -1
- package/src/brand-page/components/images-section/ImagesSection.stories.ts +2 -1
- package/src/brand-page/components/social-links/SocialLinks.stories.ts +3 -0
- package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +5 -0
- package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +3 -0
- package/src/brand-page/components/text/Text.stories.ts +2 -0
- package/src/brand-page/models/index.ts +17 -0
- package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +23 -3
- package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -0
- package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -0
- package/src/brand-page/utils/migration/migration-01.ts +2 -0
- package/src/brand-page/utils/migration/migration-18.test.ts +131 -0
- package/src/brand-page/utils/migration/migration-18.ts +22 -0
- package/src/brand-page/utils/migration.util.ts +7 -0
- package/src/brand-page/utils/personalisation.util.test.ts +4 -0
- package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
- package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -0
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +6 -0
- package/src/brand-page-maker/constants/config-settings-constant.ts +4 -0
- package/src/brand-page-maker/models/content-settings.ts +4 -0
- package/src/metadata-builder/ai-metadata-executor.ts +31 -1
- package/src/metadata-builder/builders/footer-builder.test.ts +335 -28
- package/src/metadata-builder/builders/footer-builder.ts +40 -57
- package/src/metadata-builder/builders/item-id-utils.test.ts +184 -0
- package/src/metadata-builder/builders/item-id-utils.ts +117 -0
- package/src/metadata-builder/builders/move-page-builder.ts +227 -0
- package/src/metadata-builder/builders/nav-utils.ts +141 -0
- package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +62 -88
- package/src/metadata-builder/builders/section-builders/images-section-builder.ts +66 -129
- package/src/metadata-builder/builders/section-builders/move-section-builder.ts +58 -35
- package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +105 -160
- package/src/metadata-builder/builders/section-builders/social-links-builder.ts +65 -20
- package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +83 -146
- package/src/metadata-builder/builders/section-builders/text-section-builder.ts +71 -122
- package/src/metadata-builder/builders/section-field-maps.ts +18 -0
- package/src/metadata-builder/builders/set-page-visibility-builder.ts +122 -0
- package/src/metadata-builder/metadata-executor.ts +4 -0
- package/src/metadata-builder/models.ts +2 -0
- package/src/metadata-builder/parsers/ai-toolcall-parser.ts +32 -7
- package/src/metadata-builder/parsers/section-tool-arguments.ts +84 -48
- package/src/shared/content-settings/components/links-field/LinksField.mixin.ts +2 -0
- package/src/utils/minify-brand-page-metadata.ts +14 -2
|
@@ -22,6 +22,8 @@ import {
|
|
|
22
22
|
type AddPageArguments,
|
|
23
23
|
type RemovePageArguments,
|
|
24
24
|
type RenamePageArguments,
|
|
25
|
+
type MovePageArguments,
|
|
26
|
+
type SetPageVisibilityArguments,
|
|
25
27
|
type UpdateFooterArguments,
|
|
26
28
|
type UpdateGlobalStylesArguments,
|
|
27
29
|
type UpdateHeaderArguments,
|
|
@@ -101,7 +103,7 @@ export const executeToolCallsAsync = async (
|
|
|
101
103
|
type: BUILDER_TYPES.MOVE_SECTION as BuilderType,
|
|
102
104
|
payload: {
|
|
103
105
|
contentId: arg.content_id,
|
|
104
|
-
|
|
106
|
+
moveBeforeContentId: arg.move_before_content_id,
|
|
105
107
|
targetPageId: arg.target_page_id,
|
|
106
108
|
},
|
|
107
109
|
};
|
|
@@ -145,6 +147,34 @@ export const executeToolCallsAsync = async (
|
|
|
145
147
|
};
|
|
146
148
|
}
|
|
147
149
|
|
|
150
|
+
case AI_ASSISTANT_TOOL_NAMES.MOVE_PAGE: {
|
|
151
|
+
const arg = toolcall.arguments as unknown as MovePageArguments;
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
id: toolcall.toolId,
|
|
155
|
+
type: BUILDER_TYPES.MOVE_PAGE as BuilderType,
|
|
156
|
+
payload: {
|
|
157
|
+
pageId: arg.page_id,
|
|
158
|
+
parentPageId: arg.parent_page_id,
|
|
159
|
+
moveBeforePageId: arg.move_before_page_id,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
case AI_ASSISTANT_TOOL_NAMES.SET_PAGE_VISIBILITY: {
|
|
165
|
+
const arg =
|
|
166
|
+
toolcall.arguments as unknown as SetPageVisibilityArguments;
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
id: toolcall.toolId,
|
|
170
|
+
type: BUILDER_TYPES.SET_PAGE_VISIBILITY as BuilderType,
|
|
171
|
+
payload: {
|
|
172
|
+
pageId: arg.page_id,
|
|
173
|
+
isHidden: arg.is_hidden,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
148
178
|
case AI_ASSISTANT_TOOL_NAMES.UPDATE_FOOTER: {
|
|
149
179
|
const arg = toolcall.arguments as unknown as UpdateFooterArguments;
|
|
150
180
|
|
|
@@ -14,6 +14,10 @@ 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.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
const textStyleOptions = {
|
|
@@ -42,6 +46,37 @@ const makeModel = (copyrightStyle: Record<string, unknown>): BrandPageModel =>
|
|
|
42
46
|
pageFooterMobile: { copyrightStyle: { ...copyrightStyle } },
|
|
43
47
|
}) as unknown as BrandPageModel;
|
|
44
48
|
|
|
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
|
+
|
|
45
80
|
const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
|
|
46
81
|
new FooterBuilder({
|
|
47
82
|
id: 'f1',
|
|
@@ -49,6 +84,10 @@ const run = (args: UpdateFooterArguments, model: BrandPageModel) =>
|
|
|
49
84
|
payload: args,
|
|
50
85
|
}).buildAsync(model);
|
|
51
86
|
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Copyright font (BP-5420)
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
52
91
|
describe('FooterBuilder — copyright font vs global style', () => {
|
|
53
92
|
it('detaches from the global preset: applies the font, clears textStyleType, retains preset values', async () => {
|
|
54
93
|
const model = makeModel({
|
|
@@ -137,9 +176,10 @@ describe('FooterBuilder — copyright font vs global style', () => {
|
|
|
137
176
|
});
|
|
138
177
|
});
|
|
139
178
|
|
|
140
|
-
type Link = { imageUrl: string; url: string };
|
|
179
|
+
type Link = { id?: string; imageUrl: string; url: string };
|
|
141
180
|
|
|
142
|
-
const link = (imageUrl: string): Link => ({
|
|
181
|
+
const link = (imageUrl: string, id?: string): Link => ({
|
|
182
|
+
...(id && { id }),
|
|
143
183
|
imageUrl,
|
|
144
184
|
url: `https://${imageUrl}.com`,
|
|
145
185
|
});
|
|
@@ -173,18 +213,15 @@ const socialIcons = (footer: { socialLinks?: Link[] } | undefined) =>
|
|
|
173
213
|
describe('FooterBuilder — social link removal (shared-array regression)', () => {
|
|
174
214
|
// Reproduces the bug: a single requested removal index dropped TWO links
|
|
175
215
|
// because both viewports shared one array and the builder spliced it twice.
|
|
176
|
-
it('removes exactly one link per requested
|
|
216
|
+
it('removes exactly one link per requested id even when viewports share the array', async () => {
|
|
177
217
|
const model = makeSharedSocialModel([
|
|
178
|
-
link('facebook'),
|
|
179
|
-
link('instagram'),
|
|
180
|
-
link('x'),
|
|
181
|
-
link('youtube'),
|
|
218
|
+
link('facebook', 'fb'),
|
|
219
|
+
link('instagram', 'ig'),
|
|
220
|
+
link('x', 'x'),
|
|
221
|
+
link('youtube', 'yt'),
|
|
182
222
|
]);
|
|
183
223
|
|
|
184
|
-
const { model: out } = await run(
|
|
185
|
-
{ _remove_social_link_indices: [2] },
|
|
186
|
-
model,
|
|
187
|
-
);
|
|
224
|
+
const { model: out } = await run({ _remove_social_link_ids: ['x'] }, model);
|
|
188
225
|
|
|
189
226
|
expect(socialIcons(out.pageFooterDesktop)).toEqual([
|
|
190
227
|
'facebook',
|
|
@@ -200,13 +237,13 @@ describe('FooterBuilder — social link removal (shared-array regression)', () =
|
|
|
200
237
|
|
|
201
238
|
it('un-shares the viewport arrays so later in-place edits cannot cross-corrupt', async () => {
|
|
202
239
|
const model = makeSharedSocialModel([
|
|
203
|
-
link('facebook'),
|
|
204
|
-
link('instagram'),
|
|
205
|
-
link('x'),
|
|
240
|
+
link('facebook', 'fb'),
|
|
241
|
+
link('instagram', 'ig'),
|
|
242
|
+
link('x', 'x'),
|
|
206
243
|
]);
|
|
207
244
|
|
|
208
245
|
const { model: out } = await run(
|
|
209
|
-
{
|
|
246
|
+
{ _remove_social_link_ids: ['fb'] },
|
|
210
247
|
model,
|
|
211
248
|
);
|
|
212
249
|
|
|
@@ -215,16 +252,16 @@ describe('FooterBuilder — social link removal (shared-array regression)', () =
|
|
|
215
252
|
);
|
|
216
253
|
});
|
|
217
254
|
|
|
218
|
-
it('handles multiple removal
|
|
255
|
+
it('handles multiple removal ids on a shared array (order-independent)', async () => {
|
|
219
256
|
const model = makeSharedSocialModel([
|
|
220
|
-
link('facebook'),
|
|
221
|
-
link('instagram'),
|
|
222
|
-
link('x'),
|
|
223
|
-
link('youtube'),
|
|
257
|
+
link('facebook', 'fb'),
|
|
258
|
+
link('instagram', 'ig'),
|
|
259
|
+
link('x', 'x'),
|
|
260
|
+
link('youtube', 'yt'),
|
|
224
261
|
]);
|
|
225
262
|
|
|
226
263
|
const { model: out } = await run(
|
|
227
|
-
{
|
|
264
|
+
{ _remove_social_link_ids: ['fb', 'x'] },
|
|
228
265
|
model,
|
|
229
266
|
);
|
|
230
267
|
|
|
@@ -235,15 +272,15 @@ describe('FooterBuilder — social link removal (shared-array regression)', () =
|
|
|
235
272
|
expect(socialIcons(out.pageFooterMobile)).toEqual(['instagram', 'youtube']);
|
|
236
273
|
});
|
|
237
274
|
|
|
238
|
-
it('removes one link per
|
|
275
|
+
it('removes one link per id for independent viewport arrays (no regression)', async () => {
|
|
239
276
|
const model = makeIndependentSocialModel([
|
|
240
|
-
link('facebook'),
|
|
241
|
-
link('instagram'),
|
|
242
|
-
link('x'),
|
|
277
|
+
link('facebook', 'fb'),
|
|
278
|
+
link('instagram', 'ig'),
|
|
279
|
+
link('x', 'x'),
|
|
243
280
|
]);
|
|
244
281
|
|
|
245
282
|
const { model: out } = await run(
|
|
246
|
-
{
|
|
283
|
+
{ _remove_social_link_ids: ['ig'] },
|
|
247
284
|
model,
|
|
248
285
|
);
|
|
249
286
|
|
|
@@ -252,11 +289,16 @@ describe('FooterBuilder — social link removal (shared-array regression)', () =
|
|
|
252
289
|
});
|
|
253
290
|
|
|
254
291
|
it('inserts exactly one link on a shared array (symmetric add guard)', async () => {
|
|
255
|
-
const model = makeSharedSocialModel([
|
|
292
|
+
const model = makeSharedSocialModel([
|
|
293
|
+
link('facebook', 'fb'),
|
|
294
|
+
link('instagram', 'ig'),
|
|
295
|
+
]);
|
|
256
296
|
|
|
257
297
|
const { model: out } = await run(
|
|
258
298
|
{
|
|
259
|
-
social_links: [
|
|
299
|
+
social_links: [
|
|
300
|
+
{ social_link_icon: 'x', url: 'x.com', move_before_item_id: 'ig' },
|
|
301
|
+
],
|
|
260
302
|
},
|
|
261
303
|
model,
|
|
262
304
|
);
|
|
@@ -273,3 +315,268 @@ describe('FooterBuilder — social link removal (shared-array regression)', () =
|
|
|
273
315
|
]);
|
|
274
316
|
});
|
|
275
317
|
});
|
|
318
|
+
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// Social links — id-based mutations (BP-5603)
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
describe('FooterBuilder — social links id-based mutations', () => {
|
|
324
|
+
it('backfills ids on legacy links when a social mutation runs', async () => {
|
|
325
|
+
// Links without ids (legacy persisted data). The executor recomputes social
|
|
326
|
+
// links only when a social-link mutation is present (a copyright-only update
|
|
327
|
+
// must not rewrite them); read-time backfill lives in ensureBrandPageItemIds.
|
|
328
|
+
const model = makeModelWithLinks([
|
|
329
|
+
{ imageUrl: 'facebook', url: 'https://fb.com' },
|
|
330
|
+
{ imageUrl: 'instagram', url: 'https://ig.com' },
|
|
331
|
+
]);
|
|
332
|
+
|
|
333
|
+
const { model: out } = await run(
|
|
334
|
+
{ social_links: [{ social_link_icon: 'x', url: 'https://x.com' }] },
|
|
335
|
+
model,
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
const links = out.pageFooterDesktop!.socialLinks!;
|
|
339
|
+
|
|
340
|
+
// Pre-existing legacy links are backfilled with distinct stable ids.
|
|
341
|
+
expect(links[0].id).toBeTruthy();
|
|
342
|
+
expect(links[1].id).toBeTruthy();
|
|
343
|
+
expect(links[0].id).not.toBe(links[1].id);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('updates an existing link in place by item_id', async () => {
|
|
347
|
+
const model = makeModelWithLinks([
|
|
348
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
349
|
+
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
350
|
+
]);
|
|
351
|
+
|
|
352
|
+
const { model: out } = await run(
|
|
353
|
+
{
|
|
354
|
+
social_links: [
|
|
355
|
+
{ item_id: 'link-1', url: 'https://facebook.com/mybrand' },
|
|
356
|
+
],
|
|
357
|
+
},
|
|
358
|
+
model,
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
362
|
+
|
|
363
|
+
expect(links).toHaveLength(2);
|
|
364
|
+
expect(links[0].url).toBe('https://facebook.com/mybrand');
|
|
365
|
+
expect(links[0].id).toBe('link-1');
|
|
366
|
+
// untouched link unchanged
|
|
367
|
+
expect(links[1].id).toBe('link-2');
|
|
368
|
+
expect(links[1].url).toBe('https://ig.com');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('updates social_link_icon on an existing link', async () => {
|
|
372
|
+
const model = makeModelWithLinks([
|
|
373
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
374
|
+
]);
|
|
375
|
+
|
|
376
|
+
const { model: out } = await run(
|
|
377
|
+
{
|
|
378
|
+
social_links: [{ item_id: 'link-1', social_link_icon: 'facebook-v2' }],
|
|
379
|
+
},
|
|
380
|
+
model,
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
expect(out.pageFooterDesktop!.socialLinks[0].imageUrl).toBe('facebook-v2');
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
it('skips unknown item_id silently', async () => {
|
|
387
|
+
const model = makeModelWithLinks([
|
|
388
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
389
|
+
]);
|
|
390
|
+
|
|
391
|
+
const { model: out } = await run(
|
|
392
|
+
{
|
|
393
|
+
social_links: [{ item_id: 'no-such-id', url: 'https://nowhere.com' }],
|
|
394
|
+
},
|
|
395
|
+
model,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
399
|
+
|
|
400
|
+
expect(links).toHaveLength(1);
|
|
401
|
+
expect(links[0].url).toBe('https://fb.com');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('inserts a new link (no item_id) appended by default', async () => {
|
|
405
|
+
const model = makeModelWithLinks([
|
|
406
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
407
|
+
]);
|
|
408
|
+
|
|
409
|
+
const { model: out } = await run(
|
|
410
|
+
{
|
|
411
|
+
social_links: [
|
|
412
|
+
{ social_link_icon: 'instagram', url: 'https://ig.com' },
|
|
413
|
+
],
|
|
414
|
+
},
|
|
415
|
+
model,
|
|
416
|
+
);
|
|
417
|
+
|
|
418
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
419
|
+
|
|
420
|
+
expect(links).toHaveLength(2);
|
|
421
|
+
expect(links[1].imageUrl).toBe('instagram');
|
|
422
|
+
expect(links[1].id).toBeTruthy();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('inserts a new link before an anchor via move_before_item_id', async () => {
|
|
426
|
+
const model = makeModelWithLinks([
|
|
427
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
428
|
+
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
429
|
+
]);
|
|
430
|
+
|
|
431
|
+
const { model: out } = await run(
|
|
432
|
+
{
|
|
433
|
+
social_links: [
|
|
434
|
+
{
|
|
435
|
+
social_link_icon: 'x',
|
|
436
|
+
url: 'https://x.com',
|
|
437
|
+
move_before_item_id: 'link-2',
|
|
438
|
+
},
|
|
439
|
+
],
|
|
440
|
+
},
|
|
441
|
+
model,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
445
|
+
|
|
446
|
+
expect(links).toHaveLength(3);
|
|
447
|
+
expect(links[0].id).toBe('link-1');
|
|
448
|
+
expect(links[1].imageUrl).toBe('x');
|
|
449
|
+
expect(links[2].id).toBe('link-2');
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('inserts a new link at end via move_before_item_id "__end__"', async () => {
|
|
453
|
+
const model = makeModelWithLinks([
|
|
454
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
455
|
+
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
456
|
+
]);
|
|
457
|
+
|
|
458
|
+
const { model: out } = await run(
|
|
459
|
+
{
|
|
460
|
+
social_links: [
|
|
461
|
+
{
|
|
462
|
+
social_link_icon: 'x',
|
|
463
|
+
url: 'https://x.com',
|
|
464
|
+
move_before_item_id: '__end__',
|
|
465
|
+
},
|
|
466
|
+
],
|
|
467
|
+
},
|
|
468
|
+
model,
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
472
|
+
|
|
473
|
+
expect(links).toHaveLength(3);
|
|
474
|
+
expect(links[2].imageUrl).toBe('x');
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it('moves an existing link before an anchor via move_before_item_id', async () => {
|
|
478
|
+
const model = makeModelWithLinks([
|
|
479
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
480
|
+
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
481
|
+
{ id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
|
|
482
|
+
]);
|
|
483
|
+
|
|
484
|
+
const { model: out } = await run(
|
|
485
|
+
{
|
|
486
|
+
social_links: [{ item_id: 'link-3', move_before_item_id: 'link-1' }],
|
|
487
|
+
},
|
|
488
|
+
model,
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
492
|
+
|
|
493
|
+
expect(links.map((l) => l.id)).toEqual(['link-3', 'link-1', 'link-2']);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it('removes links by id', async () => {
|
|
497
|
+
const model = makeModelWithLinks([
|
|
498
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
499
|
+
{ id: 'link-2', imageUrl: 'instagram', url: 'https://ig.com' },
|
|
500
|
+
{ id: 'link-3', imageUrl: 'x', url: 'https://x.com' },
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
const { model: out } = await run(
|
|
504
|
+
{ _remove_social_link_ids: ['link-1', 'link-3'] },
|
|
505
|
+
model,
|
|
506
|
+
);
|
|
507
|
+
|
|
508
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
509
|
+
|
|
510
|
+
expect(links).toHaveLength(1);
|
|
511
|
+
expect(links[0].id).toBe('link-2');
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
it('_replace_social_links rebuilds the entire list and assigns fresh ids', async () => {
|
|
515
|
+
const model = makeModelWithLinks([
|
|
516
|
+
{ id: 'old-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
517
|
+
]);
|
|
518
|
+
|
|
519
|
+
const { model: out } = await run(
|
|
520
|
+
{
|
|
521
|
+
_replace_social_links: true,
|
|
522
|
+
social_links: [
|
|
523
|
+
{ social_link_icon: 'instagram', url: 'https://ig.com' },
|
|
524
|
+
{ social_link_icon: 'x', url: 'https://x.com' },
|
|
525
|
+
],
|
|
526
|
+
},
|
|
527
|
+
model,
|
|
528
|
+
);
|
|
529
|
+
|
|
530
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
531
|
+
|
|
532
|
+
expect(links).toHaveLength(2);
|
|
533
|
+
expect(links[0].imageUrl).toBe('instagram');
|
|
534
|
+
expect(links[1].imageUrl).toBe('x');
|
|
535
|
+
// fresh ids assigned, not the old one
|
|
536
|
+
expect(links[0].id).toBeTruthy();
|
|
537
|
+
expect(links[0].id).not.toBe('old-1');
|
|
538
|
+
});
|
|
539
|
+
|
|
540
|
+
it('_replace_social_links with empty social_links clears all', async () => {
|
|
541
|
+
const model = makeModelWithLinks([
|
|
542
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
543
|
+
]);
|
|
544
|
+
|
|
545
|
+
const { model: out } = await run({ _replace_social_links: true }, model);
|
|
546
|
+
|
|
547
|
+
expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
it('normalises mailto: and tel: prefixes on new links', async () => {
|
|
551
|
+
const model = makeModelWithLinks([]);
|
|
552
|
+
|
|
553
|
+
const { model: out } = await run(
|
|
554
|
+
{
|
|
555
|
+
social_links: [
|
|
556
|
+
{ social_link_icon: 'email', url: 'mailto:me@example.com' },
|
|
557
|
+
{ social_link_icon: 'phone', url: 'tel:+1234567890' },
|
|
558
|
+
],
|
|
559
|
+
},
|
|
560
|
+
model,
|
|
561
|
+
);
|
|
562
|
+
|
|
563
|
+
const links = out.pageFooterDesktop!.socialLinks;
|
|
564
|
+
|
|
565
|
+
expect(links[0].url).toBe('me@example.com');
|
|
566
|
+
expect(links[1].url).toBe('+1234567890');
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('applies mutations to both desktop and mobile footers', async () => {
|
|
570
|
+
const model = makeModelWithLinks([
|
|
571
|
+
{ id: 'link-1', imageUrl: 'facebook', url: 'https://fb.com' },
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
const { model: out } = await run(
|
|
575
|
+
{ _remove_social_link_ids: ['link-1'] },
|
|
576
|
+
model,
|
|
577
|
+
);
|
|
578
|
+
|
|
579
|
+
expect(out.pageFooterDesktop!.socialLinks).toHaveLength(0);
|
|
580
|
+
expect(out.pageFooterMobile!.socialLinks).toHaveLength(0);
|
|
581
|
+
});
|
|
582
|
+
});
|
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
import type {
|
|
11
11
|
UpdateFooterArguments,
|
|
12
12
|
ViewportFooterOverrides,
|
|
13
|
-
FooterSocialLinkItem,
|
|
14
13
|
} from '../parsers/ai-toolcall-parser';
|
|
15
14
|
import type { Logger } from '../logger';
|
|
16
15
|
import type {
|
|
@@ -22,6 +21,11 @@ import {
|
|
|
22
21
|
applyDetachedStyleOverride,
|
|
23
22
|
normalizeStyleValue,
|
|
24
23
|
} from './section-field-utils';
|
|
24
|
+
import {
|
|
25
|
+
generateItemId,
|
|
26
|
+
removeItemsByIds,
|
|
27
|
+
repositionItemByAnchor,
|
|
28
|
+
} from './item-id-utils';
|
|
25
29
|
|
|
26
30
|
type FooterSocialLink = NonNullable<
|
|
27
31
|
AbstractPageFooterStyles['socialLinks']
|
|
@@ -40,6 +44,7 @@ const makeSocialLink = (
|
|
|
40
44
|
url: string | undefined,
|
|
41
45
|
): FooterSocialLink =>
|
|
42
46
|
({
|
|
47
|
+
id: generateItemId(),
|
|
43
48
|
imageUrl: icon || '',
|
|
44
49
|
url: normaliseSocialUrl(url || ''),
|
|
45
50
|
imageTitle: '',
|
|
@@ -50,8 +55,7 @@ const makeSocialLink = (
|
|
|
50
55
|
|
|
51
56
|
const hasSocialLinkMutation = (args: UpdateFooterArguments): boolean =>
|
|
52
57
|
args._replace_social_links === true ||
|
|
53
|
-
(args.
|
|
54
|
-
(args._reorder_social_links?.length ?? 0) > 0 ||
|
|
58
|
+
(args._remove_social_link_ids?.length ?? 0) > 0 ||
|
|
55
59
|
(args.social_links?.length ?? 0) > 0;
|
|
56
60
|
|
|
57
61
|
/**
|
|
@@ -80,62 +84,41 @@ const computeSocialLinks = (
|
|
|
80
84
|
);
|
|
81
85
|
}
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const existing = links[link.item_index];
|
|
91
|
-
|
|
92
|
-
if (!existing) continue;
|
|
93
|
-
|
|
94
|
-
links[link.item_index] = {
|
|
95
|
-
...existing,
|
|
96
|
-
...(link.social_link_icon !== undefined
|
|
97
|
-
? { imageUrl: link.social_link_icon }
|
|
98
|
-
: {}),
|
|
99
|
-
...(link.url !== undefined ? { url: normaliseSocialUrl(link.url) } : {}),
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Remove by index (descending order to preserve indices during splice).
|
|
104
|
-
if (args._remove_social_link_indices?.length) {
|
|
105
|
-
const sorted = [...args._remove_social_link_indices].sort((a, b) => b - a);
|
|
87
|
+
// Work on fresh copies so `current` (which may be shared across viewports)
|
|
88
|
+
// is never mutated. Backfill ids on the copies so legacy links become
|
|
89
|
+
// referenceable by the id-based contract (BP-5603).
|
|
90
|
+
let links: FooterSocialLink[] = (current ?? []).map((l) => ({
|
|
91
|
+
...l,
|
|
92
|
+
id: l.id || generateItemId(),
|
|
93
|
+
}));
|
|
106
94
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Insert new links (descending insert_at order to preserve positions).
|
|
113
|
-
const insertLinks = (args.social_links ?? []).filter(
|
|
114
|
-
(l): l is FooterSocialLinkItem & { insert_at: number } =>
|
|
115
|
-
l.insert_at !== undefined && l.item_index === undefined,
|
|
116
|
-
);
|
|
117
|
-
const sortedInserts = [...insertLinks].sort(
|
|
118
|
-
(a, b) => b.insert_at - a.insert_at,
|
|
119
|
-
);
|
|
95
|
+
// Remove by id — order-independent, no index shifting.
|
|
96
|
+
links = removeItemsByIds(links, args._remove_social_link_ids);
|
|
120
97
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
98
|
+
// Update existing links (item_id present) or insert new ones (no item_id);
|
|
99
|
+
// position/move via move_before_item_id ("__end__" or a missing anchor => end).
|
|
100
|
+
for (const link of args.social_links ?? []) {
|
|
101
|
+
if (link.item_id) {
|
|
102
|
+
const index = links.findIndex((l) => l.id === link.item_id);
|
|
103
|
+
|
|
104
|
+
// Unknown id — skip rather than guess at a position.
|
|
105
|
+
if (index === -1) continue;
|
|
106
|
+
|
|
107
|
+
links[index] = {
|
|
108
|
+
...links[index],
|
|
109
|
+
...(link.social_link_icon !== undefined
|
|
110
|
+
? { imageUrl: link.social_link_icon }
|
|
111
|
+
: {}),
|
|
112
|
+
...(link.url !== undefined
|
|
113
|
+
? { url: normaliseSocialUrl(link.url) }
|
|
114
|
+
: {}),
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
repositionItemByAnchor(links, links[index], link.move_before_item_id);
|
|
118
|
+
} else {
|
|
119
|
+
const newLink = makeSocialLink(link.social_link_icon, link.url);
|
|
120
|
+
|
|
121
|
+
repositionItemByAnchor(links, newLink, link.move_before_item_id);
|
|
139
122
|
}
|
|
140
123
|
}
|
|
141
124
|
|