@designcrowd/library.brand-page 5.19.1-id-based-items-1 → 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-0PcGqy0N.js → CreatedOnBrandCrowd-de-de-BmeMc9oz.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-DB8wcFMI.js → CreatedOnBrandCrowd-en-us-CQNdm_6P.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-DVWQ243C.js → CreatedOnBrandCrowd-es-es-BrmHWeAM.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-D-M_L0Du.js → CreatedOnBrandCrowd-fr-ca-CaIyjTXl.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-D-M_L0Du.js → CreatedOnBrandCrowd-fr-fr-CaIyjTXl.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CNpzknxU.js → CreatedOnBrandCrowd-pt-br-BClE0Nd9.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-CNpzknxU.js → CreatedOnBrandCrowd-pt-pt-BClE0Nd9.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-DMJoD5wE.js → CreatedOnDcom-de-de-CkrdlPqK.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-B-F3Pmjv.js → CreatedOnDcom-en-us-D33J9qky.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-DmLk-cA2.js → CreatedOnDcom-es-es-BsmInngw.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-ntnsCzvA.js → CreatedOnDcom-fr-ca-DOXlsRea.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-DFcO0gHL.js → CreatedOnDcom-fr-fr-SfFYLtyn.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-CtuJoVXW.js → CreatedOnDcom-pt-br-DcWeuZUy.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-B7shZVVn.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/stores/ai-checkpoint/ai-checkpoint-store.d.ts +0 -18
- package/lib/src/index.mjs +1 -1
- package/lib/src/{lib-BQmJBVbS.js → lib-C5vGkjfs.js} +17561 -17370
- 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/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/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/lib/src/metadata-builder/builders/item-id-utils.d.ts +0 -41
- package/lib/src/metadata-builder/builders/item-id-utils.js +0 -68
- package/src/metadata-builder/builders/item-id-utils.test.ts +0 -118
- package/src/metadata-builder/builders/item-id-utils.ts +0 -85
|
@@ -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 };
|
|
@@ -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;
|