@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
|
@@ -2,7 +2,6 @@ import {} from '../../brand-page/models';
|
|
|
2
2
|
import { BaseMetadataBuilder, } from '../models';
|
|
3
3
|
import { FOOTER_CHECKS } from './section-rules/footer.rules';
|
|
4
4
|
import { applyDetachedStyleOverride } from './section-field-utils';
|
|
5
|
-
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from './item-id-utils';
|
|
6
5
|
const normaliseSocialUrl = (url) => {
|
|
7
6
|
for (const prefix of ['mailto:', 'tel:']) {
|
|
8
7
|
if (url.startsWith(prefix))
|
|
@@ -21,7 +20,6 @@ const applyGlobalFooterChanges = (footer, args, textStyleOptions) => {
|
|
|
21
20
|
// they are superseded by the replacement and running them would cause duplicates.
|
|
22
21
|
if (args._replace_social_links) {
|
|
23
22
|
footer.socialLinks = (links ?? []).map((l) => ({
|
|
24
|
-
id: generateItemId(),
|
|
25
23
|
imageUrl: l.social_link_icon || '',
|
|
26
24
|
url: normaliseSocialUrl(l.url || ''),
|
|
27
25
|
imageTitle: '',
|
|
@@ -32,40 +30,52 @@ const applyGlobalFooterChanges = (footer, args, textStyleOptions) => {
|
|
|
32
30
|
}
|
|
33
31
|
else {
|
|
34
32
|
// Targeted mutations — only run when NOT replacing the entire list.
|
|
35
|
-
//
|
|
36
|
-
// Backfill ids on legacy links so they become referenceable after this call.
|
|
37
|
-
ensureItemIds(footer.socialLinks);
|
|
38
|
-
// Remove by id (order-independent — no index shifting needed).
|
|
39
|
-
footer.socialLinks = removeItemsByIds(footer.socialLinks, args._remove_social_link_ids);
|
|
40
|
-
// Update existing links (item_id present) or insert new links (no item_id).
|
|
33
|
+
// item_index-based updates for existing links
|
|
41
34
|
if (links?.length) {
|
|
42
|
-
footer.socialLinks = footer.socialLinks ?? [];
|
|
43
35
|
for (const link of links) {
|
|
44
|
-
if (link.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
36
|
+
if (link.item_index === undefined)
|
|
37
|
+
continue;
|
|
38
|
+
const existing = footer.socialLinks?.[link.item_index];
|
|
39
|
+
if (!existing)
|
|
40
|
+
continue;
|
|
41
|
+
if (link.social_link_icon !== undefined)
|
|
42
|
+
existing.imageUrl = link.social_link_icon;
|
|
43
|
+
if (link.url !== undefined)
|
|
44
|
+
existing.url = normaliseSocialUrl(link.url);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Remove by index (descending order to preserve indices during splice)
|
|
48
|
+
if (args._remove_social_link_indices?.length && footer.socialLinks) {
|
|
49
|
+
const sorted = [...args._remove_social_link_indices].sort((a, b) => b - a);
|
|
50
|
+
for (const idx of sorted) {
|
|
51
|
+
if (idx >= 0 && idx < footer.socialLinks.length)
|
|
52
|
+
footer.socialLinks.splice(idx, 1);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Insert new links (descending insert_at order to preserve positions during splice)
|
|
56
|
+
const insertLinks = (links ?? []).filter((l) => l.insert_at !== undefined && l.item_index === undefined);
|
|
57
|
+
const sortedInserts = [...insertLinks].sort((a, b) => b.insert_at - a.insert_at);
|
|
58
|
+
for (const link of sortedInserts) {
|
|
59
|
+
const pos = Math.max(0, Math.min(link.insert_at, footer.socialLinks?.length ?? 0));
|
|
60
|
+
footer.socialLinks = footer.socialLinks ?? [];
|
|
61
|
+
footer.socialLinks.splice(pos, 0, {
|
|
62
|
+
imageUrl: link.social_link_icon || '',
|
|
63
|
+
url: normaliseSocialUrl(link.url || ''),
|
|
64
|
+
imageTitle: '',
|
|
65
|
+
width: 0,
|
|
66
|
+
height: 0,
|
|
67
|
+
imageLink: '',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// Reorder (requires ALL indices — partial reorder is not supported)
|
|
71
|
+
if (args._reorder_social_links?.length && footer.socialLinks) {
|
|
72
|
+
const newOrder = args._reorder_social_links;
|
|
73
|
+
const unique = new Set(newOrder);
|
|
74
|
+
if (unique.size === newOrder.length &&
|
|
75
|
+
newOrder.every((idx) => idx >= 0 && idx < footer.socialLinks.length)) {
|
|
76
|
+
const reordered = newOrder.map((idx) => footer.socialLinks[idx]);
|
|
77
|
+
if (reordered.length === footer.socialLinks.length)
|
|
78
|
+
footer.socialLinks = reordered;
|
|
69
79
|
}
|
|
70
80
|
}
|
|
71
81
|
}
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import { applyScalarFields, isValidUrl } from '../section-field-utils';
|
|
2
2
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
3
3
|
import { validateImageGallery } from '../section-rules/image-gallery.rules';
|
|
4
|
-
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
5
4
|
const applyGalleryType = (content, galleryType) => {
|
|
6
5
|
content.galleryStyles = {
|
|
7
6
|
...(content.galleryStyles || {}),
|
|
8
7
|
galleryType: galleryType,
|
|
9
8
|
};
|
|
10
9
|
};
|
|
11
|
-
/**
|
|
12
|
-
* Full rebuild: discard existing images and map the payload directly.
|
|
13
|
-
* Used on create and when `_replace_items` is set. Each item gets a fresh id
|
|
14
|
-
* (or keeps the caller-supplied `item_id` when provided).
|
|
15
|
-
*/
|
|
16
10
|
const applyGalleryImages = (content, images) => {
|
|
17
11
|
content.images = images.map((img) => ({
|
|
18
|
-
id: img.item_id || generateItemId(),
|
|
19
12
|
imageUrl: isValidUrl(img.image_url) ? img.image_url : '',
|
|
20
13
|
imageTitle: img.caption || '',
|
|
21
14
|
width: 0,
|
|
@@ -24,46 +17,59 @@ const applyGalleryImages = (content, images) => {
|
|
|
24
17
|
imageLink: '',
|
|
25
18
|
}));
|
|
26
19
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
20
|
+
const mergeGalleryImages = (content, images, replaceAll) => {
|
|
21
|
+
const itemsToMerge = images.filter((img) => img.insert_at === undefined);
|
|
22
|
+
const updatedImages = itemsToMerge.map((img, index) => {
|
|
23
|
+
const existing = content.images?.[index];
|
|
24
|
+
const imageUrl = isValidUrl(img.image_url)
|
|
25
|
+
? img.image_url
|
|
26
|
+
: existing?.imageUrl ?? '';
|
|
27
|
+
return {
|
|
28
|
+
...(existing || {}),
|
|
29
|
+
imageUrl,
|
|
30
|
+
imageTitle: img.caption ?? existing?.imageTitle ?? '',
|
|
31
|
+
width: existing?.width ?? 0,
|
|
32
|
+
height: existing?.height ?? 0,
|
|
33
|
+
mediaType: existing?.mediaType ?? 'image',
|
|
34
|
+
imageLink: existing?.imageLink ?? '',
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
if (!replaceAll &&
|
|
38
|
+
content.images &&
|
|
39
|
+
content.images.length > itemsToMerge.length) {
|
|
40
|
+
const extras = content.images.slice(itemsToMerge.length).map((img) => ({
|
|
41
|
+
imageUrl: img.imageUrl ?? '',
|
|
42
|
+
imageTitle: img.imageTitle ?? '',
|
|
43
|
+
width: img.width ?? 0,
|
|
44
|
+
height: img.height ?? 0,
|
|
45
|
+
mediaType: img.mediaType ?? 'image',
|
|
46
|
+
imageLink: img.imageLink ?? '',
|
|
47
|
+
}));
|
|
48
|
+
updatedImages.push(...extras);
|
|
49
|
+
}
|
|
50
|
+
content.images = updatedImages;
|
|
51
|
+
};
|
|
52
|
+
const insertGalleryImages = (content, images) => {
|
|
53
|
+
const inserts = images.filter((img) => img.insert_at !== undefined);
|
|
54
|
+
if (!inserts.length)
|
|
55
|
+
return;
|
|
56
|
+
const sorted = [...inserts].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
|
|
57
|
+
for (const img of sorted) {
|
|
58
|
+
if (!isValidUrl(img.image_url))
|
|
59
|
+
continue;
|
|
60
|
+
if (content.images?.some((existing) => existing.imageUrl === img.image_url))
|
|
61
|
+
continue;
|
|
62
|
+
content.images = content.images ?? [];
|
|
63
|
+
const pos = Math.max(0, Math.min(img.insert_at ?? 0, content.images.length));
|
|
64
|
+
content.images.splice(pos, 0, {
|
|
65
|
+
imageUrl: img.image_url,
|
|
66
|
+
imageTitle: img.caption ?? '',
|
|
67
|
+
width: 0,
|
|
68
|
+
height: 0,
|
|
69
|
+
mediaType: 'image',
|
|
70
|
+
imageLink: '',
|
|
71
|
+
colorReplacements: [],
|
|
72
|
+
});
|
|
67
73
|
}
|
|
68
74
|
};
|
|
69
75
|
export class ImageGalleryBuilder extends BaseSectionBuilder {
|
|
@@ -85,19 +91,24 @@ export class ImageGalleryBuilder extends BaseSectionBuilder {
|
|
|
85
91
|
if (args.gallery_type) {
|
|
86
92
|
applyGalleryType(content, args.gallery_type);
|
|
87
93
|
}
|
|
88
|
-
if (args.
|
|
89
|
-
|
|
90
|
-
applyGalleryImages(content, args.images);
|
|
91
|
-
}
|
|
92
|
-
else if (this.mode === 'create') {
|
|
93
|
-
if (args.images && Array.isArray(args.images)) {
|
|
94
|
+
if (args.images && Array.isArray(args.images)) {
|
|
95
|
+
if (this.mode === 'create') {
|
|
94
96
|
applyGalleryImages(content, args.images);
|
|
95
97
|
}
|
|
98
|
+
else {
|
|
99
|
+
mergeGalleryImages(content, args.images, !!args._replace_items);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (args._remove_item_indices?.length && content.images) {
|
|
103
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
104
|
+
for (const idx of sorted) {
|
|
105
|
+
if (idx >= 0 && idx < content.images.length) {
|
|
106
|
+
content.images.splice(idx, 1);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
96
109
|
}
|
|
97
|
-
|
|
98
|
-
args.
|
|
99
|
-
// Update: id-based add / edit / move / remove in a single pass.
|
|
100
|
-
mergeGalleryImages(content, args.images ?? [], args._remove_item_ids);
|
|
110
|
+
if (args.images && Array.isArray(args.images) && this.mode === 'update') {
|
|
111
|
+
insertGalleryImages(content, args.images);
|
|
101
112
|
}
|
|
102
113
|
return { request: this.request, model };
|
|
103
114
|
}
|
|
@@ -3,15 +3,8 @@ import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
|
|
|
3
3
|
import { applyScalarFields, applyStyleFields, isValidUrl, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
|
|
4
4
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
5
5
|
import { validateImagesSection } from '../section-rules/images-section.rules';
|
|
6
|
-
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
7
|
-
/**
|
|
8
|
-
* Full replacement: discard existing items and rebuild from the payload.
|
|
9
|
-
* Assigns a stable `id` to each item; `itemsId` is set positionally here
|
|
10
|
-
* but will be re-indexed at the end of buildAsync.
|
|
11
|
-
*/
|
|
12
6
|
const applyImageItems = (content, items) => {
|
|
13
7
|
content.items = items.map((item, index) => ({
|
|
14
|
-
id: item.item_id || generateItemId(),
|
|
15
8
|
imageUrl: isValidUrl(item.image_url) ? item.image_url : '',
|
|
16
9
|
imageTitle: '',
|
|
17
10
|
width: 0,
|
|
@@ -25,28 +18,16 @@ const applyImageItems = (content, items) => {
|
|
|
25
18
|
colorReplacements: [],
|
|
26
19
|
}));
|
|
27
20
|
};
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
* Existing items are id-backfilled first so legacy sections become
|
|
38
|
-
* referenceable the moment the metadata builder touches them.
|
|
39
|
-
* Order-independent: there are no positional indices to shift between ops.
|
|
40
|
-
*/
|
|
41
|
-
const mergeImageItems = (content, items, removeIds) => {
|
|
42
|
-
ensureItemIds(content.items);
|
|
43
|
-
content.items = removeItemsByIds(content.items, removeIds);
|
|
44
|
-
for (const item of items) {
|
|
45
|
-
if (item.item_id) {
|
|
46
|
-
const existing = content.items.find((i) => i.id === item.item_id);
|
|
47
|
-
// Unknown id — skip rather than guess at a position.
|
|
48
|
-
if (!existing)
|
|
21
|
+
const mergeImageItems = (content, items, replaceAll) => {
|
|
22
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
23
|
+
if (hasIndexedItems) {
|
|
24
|
+
for (const item of items) {
|
|
25
|
+
const targetIndex = item.item_index;
|
|
26
|
+
if (targetIndex === undefined ||
|
|
27
|
+
targetIndex < 0 ||
|
|
28
|
+
!content.items?.[targetIndex])
|
|
49
29
|
continue;
|
|
30
|
+
const existing = content.items[targetIndex];
|
|
50
31
|
if (isValidUrl(item.image_url))
|
|
51
32
|
existing.imageUrl = item.image_url;
|
|
52
33
|
if (item.heading !== undefined)
|
|
@@ -55,26 +36,36 @@ const mergeImageItems = (content, items, removeIds) => {
|
|
|
55
36
|
existing.paragraph = item.text;
|
|
56
37
|
if (item.button_text !== undefined)
|
|
57
38
|
existing.buttonText = item.button_text || false;
|
|
58
|
-
repositionItemByAnchor(content.items, existing, item.move_before_item_id);
|
|
59
|
-
}
|
|
60
|
-
else {
|
|
61
|
-
const newItem = {
|
|
62
|
-
id: generateItemId(),
|
|
63
|
-
imageUrl: isValidUrl(item.image_url) ? item.image_url : '',
|
|
64
|
-
imageTitle: '',
|
|
65
|
-
width: 0,
|
|
66
|
-
height: 0,
|
|
67
|
-
mediaType: 'image',
|
|
68
|
-
imageLink: '',
|
|
69
|
-
title: item.heading ?? '',
|
|
70
|
-
paragraph: item.text ?? '',
|
|
71
|
-
buttonText: item.button_text || false,
|
|
72
|
-
itemsId: 0, // re-indexed at the end of buildAsync
|
|
73
|
-
colorReplacements: [],
|
|
74
|
-
};
|
|
75
|
-
repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
|
|
76
39
|
}
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
43
|
+
const updatedItems = itemsToMerge.map((item, index) => {
|
|
44
|
+
const existing = content.items?.[index];
|
|
45
|
+
return {
|
|
46
|
+
...(existing || {}),
|
|
47
|
+
imageUrl: isValidUrl(item.image_url)
|
|
48
|
+
? item.image_url
|
|
49
|
+
: existing?.imageUrl ?? '',
|
|
50
|
+
imageTitle: existing?.imageTitle ?? '',
|
|
51
|
+
width: existing?.width ?? 0,
|
|
52
|
+
height: existing?.height ?? 0,
|
|
53
|
+
mediaType: existing?.mediaType ?? 'image',
|
|
54
|
+
imageLink: existing?.imageLink ?? '',
|
|
55
|
+
title: item.heading ?? existing?.title ?? '',
|
|
56
|
+
paragraph: item.text ?? existing?.paragraph ?? '',
|
|
57
|
+
buttonText: item.button_text ?? existing?.buttonText ?? '',
|
|
58
|
+
itemsId: existing?.itemsId ?? index,
|
|
59
|
+
colorReplacements: existing?.colorReplacements ?? [],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
if (!replaceAll &&
|
|
63
|
+
content.items &&
|
|
64
|
+
content.items.length > itemsToMerge.length) {
|
|
65
|
+
const remaining = content.items.slice(itemsToMerge.length);
|
|
66
|
+
updatedItems.push(...remaining);
|
|
77
67
|
}
|
|
68
|
+
content.items = updatedItems;
|
|
78
69
|
};
|
|
79
70
|
export class ImagesSectionBuilder extends BaseSectionBuilder {
|
|
80
71
|
get sectionValidate() {
|
|
@@ -132,22 +123,66 @@ export class ImagesSectionBuilder extends BaseSectionBuilder {
|
|
|
132
123
|
])),
|
|
133
124
|
};
|
|
134
125
|
}
|
|
135
|
-
if (args.
|
|
136
|
-
|
|
137
|
-
applyImageItems(content, args.items);
|
|
138
|
-
}
|
|
139
|
-
else if (this.mode === 'create') {
|
|
140
|
-
if (args.items && Array.isArray(args.items)) {
|
|
126
|
+
if (args.items && Array.isArray(args.items)) {
|
|
127
|
+
if (this.mode === 'create') {
|
|
141
128
|
applyImageItems(content, args.items);
|
|
142
129
|
}
|
|
130
|
+
else {
|
|
131
|
+
mergeImageItems(content, args.items, !!args._replace_items);
|
|
132
|
+
}
|
|
143
133
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
134
|
+
// Phase 2: Item operations (remove, insert, reorder)
|
|
135
|
+
if (args._remove_item_indices?.length && content.items) {
|
|
136
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
137
|
+
for (const idx of sorted) {
|
|
138
|
+
if (idx >= 0 && idx < content.items.length) {
|
|
139
|
+
content.items.splice(idx, 1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
if (args.items && Array.isArray(args.items)) {
|
|
144
|
+
const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
|
|
145
|
+
// Process inserts in reverse order of insert_at to preserve indices
|
|
146
|
+
const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
|
|
147
|
+
for (const item of sortedInserts) {
|
|
148
|
+
if (!isValidUrl(item.image_url))
|
|
149
|
+
continue;
|
|
150
|
+
// Skip if this image_url already exists — defensive against duplicate
|
|
151
|
+
// tool call application
|
|
152
|
+
if (content.items?.some((existing) => existing.imageUrl === item.image_url)) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.items?.length ?? 0));
|
|
156
|
+
const newItem = {
|
|
157
|
+
imageUrl: item.image_url,
|
|
158
|
+
imageTitle: '',
|
|
159
|
+
width: 0,
|
|
160
|
+
height: 0,
|
|
161
|
+
mediaType: 'image',
|
|
162
|
+
imageLink: '',
|
|
163
|
+
title: item.heading ?? '',
|
|
164
|
+
paragraph: item.text ?? '',
|
|
165
|
+
buttonText: item.button_text || '',
|
|
166
|
+
itemsId: pos,
|
|
167
|
+
colorReplacements: [],
|
|
168
|
+
};
|
|
169
|
+
content.items = content.items ?? [];
|
|
170
|
+
content.items.splice(pos, 0, newItem);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (args._reorder_items?.length && content.items) {
|
|
174
|
+
const newOrder = args._reorder_items;
|
|
175
|
+
const unique = new Set(newOrder);
|
|
176
|
+
if (unique.size === newOrder.length) {
|
|
177
|
+
const reordered = newOrder
|
|
178
|
+
.filter((idx) => idx >= 0 && idx < content.items.length)
|
|
179
|
+
.map((idx) => content.items[idx]);
|
|
180
|
+
if (reordered.length === content.items.length) {
|
|
181
|
+
content.items = reordered;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
148
184
|
}
|
|
149
|
-
// Re-index itemsId after
|
|
150
|
-
// separate from the stable `id` used for targeting).
|
|
185
|
+
// Re-index itemsId after any item operations
|
|
151
186
|
if (content.items) {
|
|
152
187
|
content.items.forEach((item, i) => {
|
|
153
188
|
item.itemsId = i;
|
|
@@ -4,7 +4,6 @@ import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } fr
|
|
|
4
4
|
import { applyColorThemeToReplacements, resolveShapeColorReplacementsAsync, } from '../shape-color-utils';
|
|
5
5
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
6
6
|
import { validateShapesSection } from '../section-rules/shapes-section.rules';
|
|
7
|
-
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
8
7
|
const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
9
8
|
let colors = existing ?? [];
|
|
10
9
|
const newUrl = item.shape_url ?? item.icon_image_url;
|
|
@@ -16,13 +15,8 @@ const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
|
16
15
|
}
|
|
17
16
|
return colors;
|
|
18
17
|
};
|
|
19
|
-
/**
|
|
20
|
-
* Full replacement: discard existing items and rebuild from the payload.
|
|
21
|
-
* Used for create mode and _replace_items: true.
|
|
22
|
-
*/
|
|
23
18
|
const applyShapeItemsAsync = async (content, items) => {
|
|
24
|
-
content.items = await Promise.all(items.map(async (item) => ({
|
|
25
|
-
id: item.item_id || generateItemId(),
|
|
19
|
+
content.items = await Promise.all(items.map(async (item, index) => ({
|
|
26
20
|
imageUrl: item.shape_url || item.icon_image_url || '',
|
|
27
21
|
imageTitle: '',
|
|
28
22
|
width: 0,
|
|
@@ -31,36 +25,20 @@ const applyShapeItemsAsync = async (content, items) => {
|
|
|
31
25
|
imageLink: '',
|
|
32
26
|
title: item.heading || '',
|
|
33
27
|
paragraph: item.text || '',
|
|
34
|
-
itemsId:
|
|
28
|
+
itemsId: index,
|
|
35
29
|
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
36
30
|
})));
|
|
37
|
-
// Re-index itemsId after build
|
|
38
|
-
content.items.forEach((item, i) => {
|
|
39
|
-
item.itemsId = i;
|
|
40
|
-
});
|
|
41
31
|
};
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
* the moment the metadata builder touches them. Order-independent: there are no
|
|
53
|
-
* positional indices to shift between operations.
|
|
54
|
-
*/
|
|
55
|
-
const mergeShapeItemsAsync = async (content, items, removeIds) => {
|
|
56
|
-
ensureItemIds(content.items);
|
|
57
|
-
content.items = removeItemsByIds(content.items, removeIds);
|
|
58
|
-
for (const item of items) {
|
|
59
|
-
if (item.item_id) {
|
|
60
|
-
const existing = content.items.find((i) => i.id === item.item_id);
|
|
61
|
-
// Unknown id — skip rather than guess at a position.
|
|
62
|
-
if (!existing)
|
|
63
|
-
continue;
|
|
32
|
+
const mergeShapeItemsAsync = async (content, items, replaceAll) => {
|
|
33
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
34
|
+
if (hasIndexedItems) {
|
|
35
|
+
await Promise.all(items.map(async (item) => {
|
|
36
|
+
const targetIndex = item.item_index;
|
|
37
|
+
if (targetIndex === undefined ||
|
|
38
|
+
targetIndex < 0 ||
|
|
39
|
+
!content.items?.[targetIndex])
|
|
40
|
+
return;
|
|
41
|
+
const existing = content.items[targetIndex];
|
|
64
42
|
const prevImageUrl = existing.imageUrl;
|
|
65
43
|
if (item.shape_url !== undefined || item.icon_image_url !== undefined) {
|
|
66
44
|
existing.imageUrl =
|
|
@@ -73,29 +51,34 @@ const mergeShapeItemsAsync = async (content, items, removeIds) => {
|
|
|
73
51
|
if (item.shape_url !== undefined || item.color_theme !== undefined) {
|
|
74
52
|
existing.colorReplacements = await resolveItemColorsAsync(item, existing.colorReplacements, prevImageUrl);
|
|
75
53
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
54
|
+
}));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
58
|
+
const updatedItems = await Promise.all(itemsToMerge.map(async (item, index) => {
|
|
59
|
+
const existing = content.items?.[index];
|
|
60
|
+
const imageUrl = item.shape_url ?? item.icon_image_url ?? existing?.imageUrl ?? '';
|
|
61
|
+
return {
|
|
62
|
+
...(existing || {}),
|
|
63
|
+
imageUrl,
|
|
64
|
+
imageTitle: existing?.imageTitle ?? '',
|
|
65
|
+
width: existing?.width ?? 0,
|
|
66
|
+
height: existing?.height ?? 0,
|
|
67
|
+
mediaType: existing?.mediaType ?? 'shape',
|
|
68
|
+
imageLink: existing?.imageLink ?? '',
|
|
69
|
+
title: item.heading ?? existing?.title ?? '',
|
|
70
|
+
paragraph: item.text ?? existing?.paragraph ?? '',
|
|
71
|
+
itemsId: existing?.itemsId ?? index,
|
|
72
|
+
colorReplacements: await resolveItemColorsAsync(item, existing?.colorReplacements, existing?.imageUrl),
|
|
73
|
+
};
|
|
74
|
+
}));
|
|
75
|
+
if (!replaceAll &&
|
|
76
|
+
content.items &&
|
|
77
|
+
content.items.length > itemsToMerge.length) {
|
|
78
|
+
const remaining = content.items.slice(itemsToMerge.length);
|
|
79
|
+
updatedItems.push(...remaining);
|
|
94
80
|
}
|
|
95
|
-
|
|
96
|
-
content.items.forEach((item, i) => {
|
|
97
|
-
item.itemsId = i;
|
|
98
|
-
});
|
|
81
|
+
content.items = updatedItems;
|
|
99
82
|
};
|
|
100
83
|
export class ShapesSectionBuilder extends BaseSectionBuilder {
|
|
101
84
|
get sectionValidate() {
|
|
@@ -153,19 +136,69 @@ export class ShapesSectionBuilder extends BaseSectionBuilder {
|
|
|
153
136
|
])),
|
|
154
137
|
};
|
|
155
138
|
}
|
|
156
|
-
if (args.
|
|
157
|
-
|
|
158
|
-
await applyShapeItemsAsync(content, args.items);
|
|
159
|
-
}
|
|
160
|
-
else if (this.mode === 'create') {
|
|
161
|
-
if (args.items && Array.isArray(args.items)) {
|
|
139
|
+
if (args.items && Array.isArray(args.items)) {
|
|
140
|
+
if (this.mode === 'create') {
|
|
162
141
|
await applyShapeItemsAsync(content, args.items);
|
|
163
142
|
}
|
|
143
|
+
else {
|
|
144
|
+
await mergeShapeItemsAsync(content, args.items, !!args._replace_items);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Phase 2: Item operations (remove, insert, reorder)
|
|
148
|
+
if (args._remove_item_indices?.length && content.items) {
|
|
149
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
150
|
+
for (const idx of sorted) {
|
|
151
|
+
if (idx >= 0 && idx < content.items.length) {
|
|
152
|
+
content.items.splice(idx, 1);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (args.items && Array.isArray(args.items)) {
|
|
157
|
+
const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
|
|
158
|
+
// Process inserts in reverse order of insert_at to preserve indices
|
|
159
|
+
const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
|
|
160
|
+
for (const item of sortedInserts) {
|
|
161
|
+
const insertUrl = item.shape_url ?? item.icon_image_url ?? '';
|
|
162
|
+
// Skip if this shape/icon already exists — defensive against duplicate
|
|
163
|
+
// tool call application
|
|
164
|
+
if (insertUrl &&
|
|
165
|
+
content.items?.some((existing) => existing.imageUrl === insertUrl)) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.items?.length ?? 0));
|
|
169
|
+
const newItem = {
|
|
170
|
+
imageUrl: insertUrl,
|
|
171
|
+
imageTitle: '',
|
|
172
|
+
width: 0,
|
|
173
|
+
height: 0,
|
|
174
|
+
mediaType: 'shape',
|
|
175
|
+
imageLink: '',
|
|
176
|
+
title: item.heading ?? '',
|
|
177
|
+
paragraph: item.text ?? '',
|
|
178
|
+
itemsId: pos,
|
|
179
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
180
|
+
};
|
|
181
|
+
content.items = content.items ?? [];
|
|
182
|
+
content.items.splice(pos, 0, newItem);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (args._reorder_items?.length && content.items) {
|
|
186
|
+
const newOrder = args._reorder_items;
|
|
187
|
+
const unique = new Set(newOrder);
|
|
188
|
+
if (unique.size === newOrder.length) {
|
|
189
|
+
const reordered = newOrder
|
|
190
|
+
.filter((idx) => idx >= 0 && idx < content.items.length)
|
|
191
|
+
.map((idx) => content.items[idx]);
|
|
192
|
+
if (reordered.length === content.items.length) {
|
|
193
|
+
content.items = reordered;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
164
196
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
197
|
+
// Re-index itemsId after any item operations
|
|
198
|
+
if (content.items) {
|
|
199
|
+
content.items.forEach((item, i) => {
|
|
200
|
+
item.itemsId = i;
|
|
201
|
+
});
|
|
169
202
|
}
|
|
170
203
|
return { request: this.request, model };
|
|
171
204
|
}
|