@designcrowd/library.brand-page 5.19.1-id-based-items-2 → 5.19.1
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-BmeMc9oz.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DGUJtvld.js → CreatedOnBrandCrowd-en-us-CQNdm_6P.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dg-1uOfV.js → CreatedOnBrandCrowd-es-es-BrmHWeAM.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CRKraZl8.js → CreatedOnBrandCrowd-fr-ca-CaIyjTXl.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CRKraZl8.js → CreatedOnBrandCrowd-fr-fr-CaIyjTXl.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-br-BClE0Nd9.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-bZ9VlEAu.js → CreatedOnBrandCrowd-pt-pt-BClE0Nd9.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-CJYfwZx8.js → CreatedOnDcom-de-de-CkrdlPqK.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-rzA1lVxH.js → CreatedOnDcom-en-us-D33J9qky.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CNwGPjik.js → CreatedOnDcom-es-es-BsmInngw.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-DKw7OGIz.js → CreatedOnDcom-fr-ca-DOXlsRea.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-WzOEaAZG.js → CreatedOnDcom-fr-fr-SfFYLtyn.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-DkualVhm.js → CreatedOnDcom-pt-br-DcWeuZUy.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-w4Nnsdzq.js → CreatedOnDcom-pt-pt-DZsZZ4iX.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 +0 -12
- 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 +0 -18
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-Dy7MYyqz.js → lib-C5vGkjfs.js} +17577 -17407
- 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/page-settings/components/page-settings/PageSettings.mixin.d.ts +0 -2
- package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +0 -2
- 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 +0 -12
- package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +0 -6
- 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/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
|
@@ -18,24 +18,12 @@ import {
|
|
|
18
18
|
} from '../section-field-utils';
|
|
19
19
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
20
20
|
import { validateImagesSection } from '../section-rules/images-section.rules';
|
|
21
|
-
|
|
22
|
-
ensureItemIds,
|
|
23
|
-
generateItemId,
|
|
24
|
-
removeItemsByIds,
|
|
25
|
-
repositionItemByAnchor,
|
|
26
|
-
} from '../item-id-utils';
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Full replacement: discard existing items and rebuild from the payload.
|
|
30
|
-
* Assigns a stable `id` to each item; `itemsId` is set positionally here
|
|
31
|
-
* but will be re-indexed at the end of buildAsync.
|
|
32
|
-
*/
|
|
21
|
+
|
|
33
22
|
const applyImageItems = (
|
|
34
23
|
content: ImagesSectionContent,
|
|
35
24
|
items: ImagesSectionItem[],
|
|
36
25
|
): void => {
|
|
37
26
|
content.items = items.map((item, index) => ({
|
|
38
|
-
id: item.item_id || generateItemId(),
|
|
39
27
|
imageUrl: isValidUrl(item.image_url) ? item.image_url! : '',
|
|
40
28
|
imageTitle: '',
|
|
41
29
|
width: 0,
|
|
@@ -50,33 +38,25 @@ const applyImageItems = (
|
|
|
50
38
|
}));
|
|
51
39
|
};
|
|
52
40
|
|
|
53
|
-
/**
|
|
54
|
-
* Apply a batch of item operations by id (BP-5603), mirroring the
|
|
55
|
-
* section-level id-based contract one level deeper:
|
|
56
|
-
* - item with `item_id` matching an existing item → update in place (and
|
|
57
|
-
* reposition if `move_before_item_id` is set);
|
|
58
|
-
* - item without `item_id` → add a new item,
|
|
59
|
-
* positioned by `move_before_item_id`;
|
|
60
|
-
* - `_remove_item_ids` → delete by id.
|
|
61
|
-
*
|
|
62
|
-
* Existing items are id-backfilled first so legacy sections become
|
|
63
|
-
* referenceable the moment the metadata builder touches them.
|
|
64
|
-
* Order-independent: there are no positional indices to shift between ops.
|
|
65
|
-
*/
|
|
66
41
|
const mergeImageItems = (
|
|
67
42
|
content: ImagesSectionContent,
|
|
68
43
|
items: ImagesSectionItem[],
|
|
69
|
-
|
|
44
|
+
replaceAll: boolean,
|
|
70
45
|
): void => {
|
|
71
|
-
|
|
72
|
-
content.items = removeItemsByIds(content.items, removeIds);
|
|
46
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
73
47
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const
|
|
48
|
+
if (hasIndexedItems) {
|
|
49
|
+
for (const item of items) {
|
|
50
|
+
const targetIndex = item.item_index;
|
|
77
51
|
|
|
78
|
-
|
|
79
|
-
|
|
52
|
+
if (
|
|
53
|
+
targetIndex === undefined ||
|
|
54
|
+
targetIndex < 0 ||
|
|
55
|
+
!content.items?.[targetIndex]
|
|
56
|
+
)
|
|
57
|
+
continue;
|
|
58
|
+
|
|
59
|
+
const existing = content.items[targetIndex];
|
|
80
60
|
|
|
81
61
|
if (isValidUrl(item.image_url)) existing.imageUrl = item.image_url!;
|
|
82
62
|
|
|
@@ -86,27 +66,47 @@ const mergeImageItems = (
|
|
|
86
66
|
|
|
87
67
|
if (item.button_text !== undefined)
|
|
88
68
|
existing.buttonText = item.button_text || false;
|
|
89
|
-
|
|
90
|
-
repositionItemByAnchor(content.items, existing, item.move_before_item_id);
|
|
91
|
-
} else {
|
|
92
|
-
const newItem = {
|
|
93
|
-
id: generateItemId(),
|
|
94
|
-
imageUrl: isValidUrl(item.image_url) ? item.image_url! : '',
|
|
95
|
-
imageTitle: '',
|
|
96
|
-
width: 0,
|
|
97
|
-
height: 0,
|
|
98
|
-
mediaType: 'image',
|
|
99
|
-
imageLink: '',
|
|
100
|
-
title: item.heading ?? '',
|
|
101
|
-
paragraph: item.text ?? '',
|
|
102
|
-
buttonText: item.button_text || false,
|
|
103
|
-
itemsId: 0, // re-indexed at the end of buildAsync
|
|
104
|
-
colorReplacements: [],
|
|
105
|
-
} as ImagesSectionContent['items'][number];
|
|
106
|
-
|
|
107
|
-
repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
|
|
108
69
|
}
|
|
70
|
+
|
|
71
|
+
return;
|
|
109
72
|
}
|
|
73
|
+
|
|
74
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
75
|
+
|
|
76
|
+
const updatedItems = itemsToMerge.map((item, index) => {
|
|
77
|
+
const existing = content.items?.[index];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
...(existing || {}),
|
|
81
|
+
imageUrl: isValidUrl(item.image_url)
|
|
82
|
+
? item.image_url!
|
|
83
|
+
: existing?.imageUrl ?? '',
|
|
84
|
+
imageTitle: existing?.imageTitle ?? '',
|
|
85
|
+
width: existing?.width ?? 0,
|
|
86
|
+
height: existing?.height ?? 0,
|
|
87
|
+
mediaType: existing?.mediaType ?? 'image',
|
|
88
|
+
imageLink: existing?.imageLink ?? '',
|
|
89
|
+
title: item.heading ?? existing?.title ?? '',
|
|
90
|
+
paragraph: item.text ?? existing?.paragraph ?? '',
|
|
91
|
+
buttonText: item.button_text ?? existing?.buttonText ?? '',
|
|
92
|
+
itemsId: existing?.itemsId ?? index,
|
|
93
|
+
colorReplacements: existing?.colorReplacements ?? [],
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (
|
|
98
|
+
!replaceAll &&
|
|
99
|
+
content.items &&
|
|
100
|
+
content.items.length > itemsToMerge.length
|
|
101
|
+
) {
|
|
102
|
+
const remaining = content.items.slice(
|
|
103
|
+
itemsToMerge.length,
|
|
104
|
+
) as typeof updatedItems;
|
|
105
|
+
|
|
106
|
+
updatedItems.push(...remaining);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
content.items = updatedItems;
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
export class ImagesSectionBuilder extends BaseSectionBuilder<ImagesSectionContent> {
|
|
@@ -192,23 +192,86 @@ export class ImagesSectionBuilder extends BaseSectionBuilder<ImagesSectionConten
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
-
if (args.
|
|
196
|
-
|
|
197
|
-
applyImageItems(content, args.items);
|
|
198
|
-
} else if (this.mode === 'create') {
|
|
199
|
-
if (args.items && Array.isArray(args.items)) {
|
|
195
|
+
if (args.items && Array.isArray(args.items)) {
|
|
196
|
+
if (this.mode === 'create') {
|
|
200
197
|
applyImageItems(content, args.items);
|
|
198
|
+
} else {
|
|
199
|
+
mergeImageItems(content, args.items, !!args._replace_items);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Phase 2: Item operations (remove, insert, reorder)
|
|
204
|
+
if (args._remove_item_indices?.length && content.items) {
|
|
205
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
206
|
+
|
|
207
|
+
for (const idx of sorted) {
|
|
208
|
+
if (idx >= 0 && idx < content.items.length) {
|
|
209
|
+
content.items.splice(idx, 1);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (args.items && Array.isArray(args.items)) {
|
|
215
|
+
const insertItems = args.items.filter(
|
|
216
|
+
(item) => item.insert_at !== undefined && item.item_index === undefined,
|
|
217
|
+
);
|
|
218
|
+
// Process inserts in reverse order of insert_at to preserve indices
|
|
219
|
+
const sortedInserts = [...insertItems].sort(
|
|
220
|
+
(a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0),
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
for (const item of sortedInserts) {
|
|
224
|
+
if (!isValidUrl(item.image_url)) continue;
|
|
225
|
+
|
|
226
|
+
// Skip if this image_url already exists — defensive against duplicate
|
|
227
|
+
// tool call application
|
|
228
|
+
if (
|
|
229
|
+
content.items?.some(
|
|
230
|
+
(existing: any) => existing.imageUrl === item.image_url,
|
|
231
|
+
)
|
|
232
|
+
) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const pos = Math.max(
|
|
237
|
+
0,
|
|
238
|
+
Math.min(item.insert_at ?? 0, content.items?.length ?? 0),
|
|
239
|
+
);
|
|
240
|
+
const newItem = {
|
|
241
|
+
imageUrl: item.image_url,
|
|
242
|
+
imageTitle: '',
|
|
243
|
+
width: 0,
|
|
244
|
+
height: 0,
|
|
245
|
+
mediaType: 'image',
|
|
246
|
+
imageLink: '',
|
|
247
|
+
title: item.heading ?? '',
|
|
248
|
+
paragraph: item.text ?? '',
|
|
249
|
+
buttonText: item.button_text || '',
|
|
250
|
+
itemsId: pos,
|
|
251
|
+
colorReplacements: [],
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
content.items = content.items ?? [];
|
|
255
|
+
content.items.splice(pos, 0, newItem as any);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (args._reorder_items?.length && content.items) {
|
|
260
|
+
const newOrder = args._reorder_items;
|
|
261
|
+
const unique = new Set(newOrder);
|
|
262
|
+
|
|
263
|
+
if (unique.size === newOrder.length) {
|
|
264
|
+
const reordered = newOrder
|
|
265
|
+
.filter((idx) => idx >= 0 && idx < content.items!.length)
|
|
266
|
+
.map((idx) => content.items![idx]);
|
|
267
|
+
|
|
268
|
+
if (reordered.length === content.items.length) {
|
|
269
|
+
content.items = reordered;
|
|
270
|
+
}
|
|
201
271
|
}
|
|
202
|
-
} else if (
|
|
203
|
-
(args.items && Array.isArray(args.items)) ||
|
|
204
|
-
args._remove_item_ids?.length
|
|
205
|
-
) {
|
|
206
|
-
// Update: id-based add / edit / move / remove in a single pass.
|
|
207
|
-
mergeImageItems(content, args.items ?? [], args._remove_item_ids);
|
|
208
272
|
}
|
|
209
273
|
|
|
210
|
-
// Re-index itemsId after
|
|
211
|
-
// separate from the stable `id` used for targeting).
|
|
274
|
+
// Re-index itemsId after any item operations
|
|
212
275
|
if (content.items) {
|
|
213
276
|
content.items.forEach((item, i) => {
|
|
214
277
|
item.itemsId = i;
|
|
@@ -23,12 +23,6 @@ import {
|
|
|
23
23
|
} from '../shape-color-utils';
|
|
24
24
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
25
25
|
import { validateShapesSection } from '../section-rules/shapes-section.rules';
|
|
26
|
-
import {
|
|
27
|
-
ensureItemIds,
|
|
28
|
-
generateItemId,
|
|
29
|
-
removeItemsByIds,
|
|
30
|
-
repositionItemByAnchor,
|
|
31
|
-
} from '../item-id-utils';
|
|
32
26
|
|
|
33
27
|
const resolveItemColorsAsync = async (
|
|
34
28
|
item: ShapesSectionItem,
|
|
@@ -49,117 +43,111 @@ const resolveItemColorsAsync = async (
|
|
|
49
43
|
return colors;
|
|
50
44
|
};
|
|
51
45
|
|
|
52
|
-
/**
|
|
53
|
-
* Full replacement: discard existing items and rebuild from the payload.
|
|
54
|
-
* Used for create mode and _replace_items: true.
|
|
55
|
-
*/
|
|
56
46
|
const applyShapeItemsAsync = async (
|
|
57
47
|
content: ShapesSectionContent,
|
|
58
48
|
items: ShapesSectionItem[],
|
|
59
49
|
): Promise<void> => {
|
|
60
50
|
content.items = await Promise.all(
|
|
61
|
-
items.map(async (item) => ({
|
|
62
|
-
id: item.item_id || generateItemId(),
|
|
51
|
+
items.map(async (item, index) => ({
|
|
63
52
|
imageUrl: item.shape_url || item.icon_image_url || '',
|
|
64
53
|
imageTitle: '',
|
|
65
54
|
width: 0,
|
|
66
55
|
height: 0,
|
|
67
|
-
mediaType: 'shape'
|
|
68
|
-
imageLink: ''
|
|
56
|
+
mediaType: 'shape',
|
|
57
|
+
imageLink: '',
|
|
69
58
|
title: item.heading || '',
|
|
70
59
|
paragraph: item.text || '',
|
|
71
|
-
itemsId:
|
|
60
|
+
itemsId: index,
|
|
72
61
|
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
73
62
|
})),
|
|
74
63
|
);
|
|
75
|
-
|
|
76
|
-
// Re-index itemsId after build
|
|
77
|
-
content.items.forEach((item, i) => {
|
|
78
|
-
(item as TextShapeItem).itemsId = i;
|
|
79
|
-
});
|
|
80
64
|
};
|
|
81
65
|
|
|
82
|
-
/**
|
|
83
|
-
* Apply a batch of item operations by id (BP-5603), mirroring the section-level
|
|
84
|
-
* id-based contract one level deeper:
|
|
85
|
-
* - item with `item_id` matching an existing item → update in place (and
|
|
86
|
-
* reposition if `move_before_item_id` is set);
|
|
87
|
-
* - item without `item_id` → add a new item,
|
|
88
|
-
* positioned by `move_before_item_id`;
|
|
89
|
-
* - `_remove_item_ids` → delete by id.
|
|
90
|
-
*
|
|
91
|
-
* Existing items are id-backfilled first so legacy sections become referenceable
|
|
92
|
-
* the moment the metadata builder touches them. Order-independent: there are no
|
|
93
|
-
* positional indices to shift between operations.
|
|
94
|
-
*/
|
|
95
66
|
const mergeShapeItemsAsync = async (
|
|
96
67
|
content: ShapesSectionContent,
|
|
97
68
|
items: ShapesSectionItem[],
|
|
98
|
-
|
|
69
|
+
replaceAll: boolean,
|
|
99
70
|
): Promise<void> => {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
existing.imageUrl
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
71
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
72
|
+
|
|
73
|
+
if (hasIndexedItems) {
|
|
74
|
+
await Promise.all(
|
|
75
|
+
items.map(async (item) => {
|
|
76
|
+
const targetIndex = item.item_index;
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
targetIndex === undefined ||
|
|
80
|
+
targetIndex < 0 ||
|
|
81
|
+
!content.items?.[targetIndex]
|
|
82
|
+
)
|
|
83
|
+
return;
|
|
84
|
+
|
|
85
|
+
const existing = content.items[targetIndex] as TextShapeItem;
|
|
86
|
+
const prevImageUrl = existing.imageUrl;
|
|
87
|
+
|
|
88
|
+
if (item.shape_url !== undefined || item.icon_image_url !== undefined) {
|
|
89
|
+
existing.imageUrl =
|
|
90
|
+
item.shape_url ?? item.icon_image_url ?? existing.imageUrl;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (item.heading !== undefined) existing.title = item.heading;
|
|
94
|
+
|
|
95
|
+
if (item.text !== undefined) existing.paragraph = item.text;
|
|
96
|
+
|
|
97
|
+
if (item.shape_url !== undefined || item.color_theme !== undefined) {
|
|
98
|
+
existing.colorReplacements = await resolveItemColorsAsync(
|
|
99
|
+
item,
|
|
100
|
+
existing.colorReplacements,
|
|
101
|
+
prevImageUrl,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
120
106
|
|
|
121
|
-
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
122
109
|
|
|
123
|
-
|
|
124
|
-
|
|
110
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
111
|
+
|
|
112
|
+
const updatedItems = await Promise.all(
|
|
113
|
+
itemsToMerge.map(async (item, index) => {
|
|
114
|
+
const existing = content.items?.[index];
|
|
115
|
+
const imageUrl =
|
|
116
|
+
item.shape_url ?? item.icon_image_url ?? existing?.imageUrl ?? '';
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
...(existing || {}),
|
|
120
|
+
imageUrl,
|
|
121
|
+
imageTitle: existing?.imageTitle ?? '',
|
|
122
|
+
width: existing?.width ?? 0,
|
|
123
|
+
height: existing?.height ?? 0,
|
|
124
|
+
mediaType: existing?.mediaType ?? 'shape',
|
|
125
|
+
imageLink: existing?.imageLink ?? '',
|
|
126
|
+
title: item.heading ?? existing?.title ?? '',
|
|
127
|
+
paragraph: item.text ?? existing?.paragraph ?? '',
|
|
128
|
+
itemsId: existing?.itemsId ?? index,
|
|
129
|
+
colorReplacements: await resolveItemColorsAsync(
|
|
125
130
|
item,
|
|
126
|
-
existing
|
|
127
|
-
|
|
128
|
-
)
|
|
129
|
-
}
|
|
131
|
+
existing?.colorReplacements,
|
|
132
|
+
existing?.imageUrl,
|
|
133
|
+
),
|
|
134
|
+
};
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
130
137
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
width: 0,
|
|
142
|
-
height: 0,
|
|
143
|
-
mediaType: 'shape' as const,
|
|
144
|
-
imageLink: '' as const,
|
|
145
|
-
title: item.heading ?? '',
|
|
146
|
-
paragraph: item.text ?? '',
|
|
147
|
-
itemsId: 0,
|
|
148
|
-
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
149
|
-
} as TextShapeItem;
|
|
150
|
-
|
|
151
|
-
repositionItemByAnchor(
|
|
152
|
-
content.items as TextShapeItem[],
|
|
153
|
-
newItem,
|
|
154
|
-
item.move_before_item_id,
|
|
155
|
-
);
|
|
156
|
-
}
|
|
138
|
+
if (
|
|
139
|
+
!replaceAll &&
|
|
140
|
+
content.items &&
|
|
141
|
+
content.items.length > itemsToMerge.length
|
|
142
|
+
) {
|
|
143
|
+
const remaining = content.items.slice(
|
|
144
|
+
itemsToMerge.length,
|
|
145
|
+
) as typeof updatedItems;
|
|
146
|
+
|
|
147
|
+
updatedItems.push(...remaining);
|
|
157
148
|
}
|
|
158
149
|
|
|
159
|
-
|
|
160
|
-
(content.items as TextShapeItem[]).forEach((item, i) => {
|
|
161
|
-
item.itemsId = i;
|
|
162
|
-
});
|
|
150
|
+
content.items = updatedItems;
|
|
163
151
|
};
|
|
164
152
|
|
|
165
153
|
export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionContent> {
|
|
@@ -245,23 +233,90 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
|
|
|
245
233
|
};
|
|
246
234
|
}
|
|
247
235
|
|
|
248
|
-
if (args.
|
|
249
|
-
|
|
250
|
-
await applyShapeItemsAsync(content, args.items);
|
|
251
|
-
} else if (this.mode === 'create') {
|
|
252
|
-
if (args.items && Array.isArray(args.items)) {
|
|
236
|
+
if (args.items && Array.isArray(args.items)) {
|
|
237
|
+
if (this.mode === 'create') {
|
|
253
238
|
await applyShapeItemsAsync(content, args.items);
|
|
239
|
+
} else {
|
|
240
|
+
await mergeShapeItemsAsync(content, args.items, !!args._replace_items);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Phase 2: Item operations (remove, insert, reorder)
|
|
245
|
+
if (args._remove_item_indices?.length && content.items) {
|
|
246
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
247
|
+
|
|
248
|
+
for (const idx of sorted) {
|
|
249
|
+
if (idx >= 0 && idx < content.items.length) {
|
|
250
|
+
content.items.splice(idx, 1);
|
|
251
|
+
}
|
|
254
252
|
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
await mergeShapeItemsAsync(
|
|
261
|
-
content,
|
|
262
|
-
args.items ?? [],
|
|
263
|
-
args._remove_item_ids,
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (args.items && Array.isArray(args.items)) {
|
|
256
|
+
const insertItems = args.items.filter(
|
|
257
|
+
(item) => item.insert_at !== undefined && item.item_index === undefined,
|
|
264
258
|
);
|
|
259
|
+
// Process inserts in reverse order of insert_at to preserve indices
|
|
260
|
+
const sortedInserts = [...insertItems].sort(
|
|
261
|
+
(a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0),
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
for (const item of sortedInserts) {
|
|
265
|
+
const insertUrl = item.shape_url ?? item.icon_image_url ?? '';
|
|
266
|
+
|
|
267
|
+
// Skip if this shape/icon already exists — defensive against duplicate
|
|
268
|
+
// tool call application
|
|
269
|
+
if (
|
|
270
|
+
insertUrl &&
|
|
271
|
+
content.items?.some(
|
|
272
|
+
(existing: any) => existing.imageUrl === insertUrl,
|
|
273
|
+
)
|
|
274
|
+
) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const pos = Math.max(
|
|
279
|
+
0,
|
|
280
|
+
Math.min(item.insert_at ?? 0, content.items?.length ?? 0),
|
|
281
|
+
);
|
|
282
|
+
const newItem = {
|
|
283
|
+
imageUrl: insertUrl,
|
|
284
|
+
imageTitle: '',
|
|
285
|
+
width: 0,
|
|
286
|
+
height: 0,
|
|
287
|
+
mediaType: 'shape',
|
|
288
|
+
imageLink: '',
|
|
289
|
+
title: item.heading ?? '',
|
|
290
|
+
paragraph: item.text ?? '',
|
|
291
|
+
itemsId: pos,
|
|
292
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
content.items = content.items ?? [];
|
|
296
|
+
content.items.splice(pos, 0, newItem as any);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (args._reorder_items?.length && content.items) {
|
|
301
|
+
const newOrder = args._reorder_items;
|
|
302
|
+
const unique = new Set(newOrder);
|
|
303
|
+
|
|
304
|
+
if (unique.size === newOrder.length) {
|
|
305
|
+
const reordered = newOrder
|
|
306
|
+
.filter((idx) => idx >= 0 && idx < content.items!.length)
|
|
307
|
+
.map((idx) => content.items![idx]);
|
|
308
|
+
|
|
309
|
+
if (reordered.length === content.items.length) {
|
|
310
|
+
content.items = reordered;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Re-index itemsId after any item operations
|
|
316
|
+
if (content.items) {
|
|
317
|
+
content.items.forEach((item, i) => {
|
|
318
|
+
item.itemsId = i;
|
|
319
|
+
});
|
|
265
320
|
}
|
|
266
321
|
|
|
267
322
|
return { request: this.request, model };
|
|
@@ -10,24 +10,12 @@ import type { BuilderResult } from '../../models';
|
|
|
10
10
|
import { applyScalarFields, applyStyleFields } from '../section-field-utils';
|
|
11
11
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
12
12
|
import { validateSocialLinks } from '../section-rules/social-links.rules';
|
|
13
|
-
|
|
14
|
-
ensureItemIds,
|
|
15
|
-
generateItemId,
|
|
16
|
-
removeItemsByIds,
|
|
17
|
-
repositionItemByAnchor,
|
|
18
|
-
} from '../item-id-utils';
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Create path: build the links array from scratch, assigning stable ids.
|
|
22
|
-
* `link.platform` is stored as `imageUrl` — the renderer resolves it to the
|
|
23
|
-
* actual SVG at display time.
|
|
24
|
-
*/
|
|
13
|
+
|
|
25
14
|
const applySocialLinks = (
|
|
26
15
|
content: SocialLinksContent,
|
|
27
16
|
links: SocialLink[],
|
|
28
17
|
): void => {
|
|
29
18
|
content.links = links.map((link) => ({
|
|
30
|
-
id: link.item_id || generateItemId(),
|
|
31
19
|
imageUrl: link.platform || '',
|
|
32
20
|
imageTitle: '',
|
|
33
21
|
width: 0,
|
|
@@ -37,52 +25,26 @@ const applySocialLinks = (
|
|
|
37
25
|
}));
|
|
38
26
|
};
|
|
39
27
|
|
|
40
|
-
/**
|
|
41
|
-
* Merge path: id-based add / edit / move / remove in a single pass (BP-5603).
|
|
42
|
-
*
|
|
43
|
-
* - Existing items are id-backfilled first so legacy sections gain stable ids
|
|
44
|
-
* the moment the metadata builder touches them.
|
|
45
|
-
* - `_remove_item_ids` deletes by id before the edit/add loop.
|
|
46
|
-
* - An item with `item_id` matching an existing link is updated in place and
|
|
47
|
-
* optionally repositioned via `move_before_item_id`.
|
|
48
|
-
* - An item without `item_id` is added as a new link, positioned by
|
|
49
|
-
* `move_before_item_id` (or appended when omitted).
|
|
50
|
-
* - An item with an unknown `item_id` is skipped rather than guessing position.
|
|
51
|
-
*/
|
|
52
28
|
const mergeSocialLinks = (
|
|
53
29
|
content: SocialLinksContent,
|
|
54
30
|
links: SocialLink[],
|
|
55
|
-
|
|
31
|
+
replaceAll: boolean,
|
|
56
32
|
): void => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
if (link.url !== undefined) existing.url = link.url;
|
|
70
|
-
|
|
71
|
-
repositionItemByAnchor(content.links, existing, link.move_before_item_id);
|
|
72
|
-
} else {
|
|
73
|
-
const newLink = {
|
|
74
|
-
id: generateItemId(),
|
|
75
|
-
imageUrl: link.platform ?? '',
|
|
76
|
-
imageTitle: '',
|
|
77
|
-
width: 0,
|
|
78
|
-
height: 0,
|
|
79
|
-
imageLink: '' as const,
|
|
80
|
-
url: link.url ?? '',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
repositionItemByAnchor(content.links, newLink, link.move_before_item_id);
|
|
84
|
-
}
|
|
33
|
+
const updatedLinks = links.map((link, index) => {
|
|
34
|
+
const existing = content.links?.[index];
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
...(existing || {}),
|
|
38
|
+
imageUrl: link.platform ?? existing?.imageUrl ?? '',
|
|
39
|
+
url: link.url ?? existing?.url ?? '',
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
if (!replaceAll && content.links && content.links.length > links.length) {
|
|
44
|
+
updatedLinks.push(...content.links.slice(links.length));
|
|
85
45
|
}
|
|
46
|
+
|
|
47
|
+
content.links = updatedLinks;
|
|
86
48
|
};
|
|
87
49
|
|
|
88
50
|
export class SocialLinksBuilder extends BaseSectionBuilder<SocialLinksContent> {
|
|
@@ -115,19 +77,12 @@ export class SocialLinksBuilder extends BaseSectionBuilder<SocialLinksContent> {
|
|
|
115
77
|
|
|
116
78
|
const args = this.args as SocialLinksSectionArguments;
|
|
117
79
|
|
|
118
|
-
if (args.
|
|
119
|
-
|
|
120
|
-
applySocialLinks(content, args.links);
|
|
121
|
-
} else if (this.mode === 'create') {
|
|
122
|
-
if (args.links && Array.isArray(args.links)) {
|
|
80
|
+
if (args.links && Array.isArray(args.links)) {
|
|
81
|
+
if (this.mode === 'create') {
|
|
123
82
|
applySocialLinks(content, args.links);
|
|
83
|
+
} else {
|
|
84
|
+
mergeSocialLinks(content, args.links, !!args._replace_items);
|
|
124
85
|
}
|
|
125
|
-
} else if (
|
|
126
|
-
(args.links && Array.isArray(args.links)) ||
|
|
127
|
-
args._remove_item_ids?.length
|
|
128
|
-
) {
|
|
129
|
-
// Update: id-based add / edit / move / remove in a single pass.
|
|
130
|
-
mergeSocialLinks(content, args.links ?? [], args._remove_item_ids);
|
|
131
86
|
}
|
|
132
87
|
|
|
133
88
|
return { request: this.request, model };
|