@designcrowd/library.brand-page 6.0.11 → 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-DopPc6g2.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-CSHmaAWm.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-C4vM0p2e.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-ArpBbnxm.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-ArpBbnxm.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-CGAQLXiX.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-CGAQLXiX.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-3WVSgB6t.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-Dx-RHXrH.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-CfnEYFMA.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-BSHZTiUO.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-DtHssrQb.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-CdX1aRI8.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-jy044FNL.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 +84 -83
- package/lib/src/{lib-Bx7xK4dn.js → lib-BG-pyXi9.js} +19585 -19807
- 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/tracker.d.ts +2 -0
- package/lib/src/tracker.js +12 -0
- package/lib/src/tracker.mjs +12 -0
- package/lib/src/useEventTracker-DmQ6AkNn.js +356 -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 +16 -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/tracker.ts +28 -0
- package/src/utils/minify-brand-page-metadata.ts +14 -2
- package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
- package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
- package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
|
@@ -4,6 +4,7 @@ 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';
|
|
7
8
|
const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
8
9
|
let colors = existing ?? [];
|
|
9
10
|
const newUrl = item.shape_url ?? item.icon_image_url;
|
|
@@ -15,8 +16,13 @@ const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
|
15
16
|
}
|
|
16
17
|
return colors;
|
|
17
18
|
};
|
|
19
|
+
/**
|
|
20
|
+
* Full replacement: discard existing items and rebuild from the payload.
|
|
21
|
+
* Used for create mode and _replace_items: true.
|
|
22
|
+
*/
|
|
18
23
|
const applyShapeItemsAsync = async (content, items) => {
|
|
19
|
-
content.items = await Promise.all(items.map(async (item
|
|
24
|
+
content.items = await Promise.all(items.map(async (item) => ({
|
|
25
|
+
id: item.item_id || generateItemId(),
|
|
20
26
|
imageUrl: item.shape_url || item.icon_image_url || '',
|
|
21
27
|
imageTitle: '',
|
|
22
28
|
width: 0,
|
|
@@ -25,20 +31,36 @@ const applyShapeItemsAsync = async (content, items) => {
|
|
|
25
31
|
imageLink: '',
|
|
26
32
|
title: item.heading || '',
|
|
27
33
|
paragraph: item.text || '',
|
|
28
|
-
itemsId:
|
|
34
|
+
itemsId: 0,
|
|
29
35
|
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
30
36
|
})));
|
|
37
|
+
// Re-index itemsId after build
|
|
38
|
+
content.items.forEach((item, i) => {
|
|
39
|
+
item.itemsId = i;
|
|
40
|
+
});
|
|
31
41
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Apply a batch of item operations by id (BP-5603), mirroring the section-level
|
|
44
|
+
* id-based contract one level deeper:
|
|
45
|
+
* - item with `item_id` matching an existing item → update in place (and
|
|
46
|
+
* reposition if `move_before_item_id` is set);
|
|
47
|
+
* - item without `item_id` → add a new item,
|
|
48
|
+
* positioned by `move_before_item_id`;
|
|
49
|
+
* - `_remove_item_ids` → delete by id.
|
|
50
|
+
*
|
|
51
|
+
* Existing items are id-backfilled first so legacy sections become referenceable
|
|
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;
|
|
42
64
|
const prevImageUrl = existing.imageUrl;
|
|
43
65
|
if (item.shape_url !== undefined || item.icon_image_url !== undefined) {
|
|
44
66
|
existing.imageUrl =
|
|
@@ -51,34 +73,29 @@ const mergeShapeItemsAsync = async (content, items, replaceAll) => {
|
|
|
51
73
|
if (item.shape_url !== undefined || item.color_theme !== undefined) {
|
|
52
74
|
existing.colorReplacements = await resolveItemColorsAsync(item, existing.colorReplacements, prevImageUrl);
|
|
53
75
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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);
|
|
76
|
+
repositionItemByAnchor(content.items, existing, item.move_before_item_id);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const newItem = {
|
|
80
|
+
id: generateItemId(),
|
|
81
|
+
imageUrl: item.shape_url ?? item.icon_image_url ?? '',
|
|
82
|
+
imageTitle: '',
|
|
83
|
+
width: 0,
|
|
84
|
+
height: 0,
|
|
85
|
+
mediaType: 'shape',
|
|
86
|
+
imageLink: '',
|
|
87
|
+
title: item.heading ?? '',
|
|
88
|
+
paragraph: item.text ?? '',
|
|
89
|
+
itemsId: 0,
|
|
90
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
91
|
+
};
|
|
92
|
+
repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
|
|
93
|
+
}
|
|
80
94
|
}
|
|
81
|
-
|
|
95
|
+
// Re-index itemsId after all mutations
|
|
96
|
+
content.items.forEach((item, i) => {
|
|
97
|
+
item.itemsId = i;
|
|
98
|
+
});
|
|
82
99
|
};
|
|
83
100
|
export class ShapesSectionBuilder extends BaseSectionBuilder {
|
|
84
101
|
get sectionValidate() {
|
|
@@ -136,69 +153,19 @@ export class ShapesSectionBuilder extends BaseSectionBuilder {
|
|
|
136
153
|
])),
|
|
137
154
|
};
|
|
138
155
|
}
|
|
139
|
-
if (args.items && Array.isArray(args.items)) {
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
}
|
|
156
|
+
if (args._replace_items && args.items && Array.isArray(args.items)) {
|
|
157
|
+
// Full replacement: discard existing items and rebuild from the payload.
|
|
158
|
+
await applyShapeItemsAsync(content, args.items);
|
|
155
159
|
}
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
}
|
|
160
|
+
else if (this.mode === 'create') {
|
|
161
|
+
if (args.items && Array.isArray(args.items)) {
|
|
162
|
+
await applyShapeItemsAsync(content, args.items);
|
|
195
163
|
}
|
|
196
164
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
});
|
|
165
|
+
else if ((args.items && Array.isArray(args.items)) ||
|
|
166
|
+
args._remove_item_ids?.length) {
|
|
167
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
168
|
+
await mergeShapeItemsAsync(content, args.items ?? [], args._remove_item_ids);
|
|
202
169
|
}
|
|
203
170
|
return { request: this.request, model };
|
|
204
171
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
import { applyScalarFields, applyStyleFields } from '../section-field-utils';
|
|
2
2
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
3
3
|
import { validateSocialLinks } from '../section-rules/social-links.rules';
|
|
4
|
+
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
5
|
+
/**
|
|
6
|
+
* Create path: build the links array from scratch, assigning stable ids.
|
|
7
|
+
* `link.platform` is stored as `imageUrl` — the renderer resolves it to the
|
|
8
|
+
* actual SVG at display time.
|
|
9
|
+
*/
|
|
4
10
|
const applySocialLinks = (content, links) => {
|
|
5
11
|
content.links = links.map((link) => ({
|
|
12
|
+
id: link.item_id || generateItemId(),
|
|
6
13
|
imageUrl: link.platform || '',
|
|
7
14
|
imageTitle: '',
|
|
8
15
|
width: 0,
|
|
@@ -11,19 +18,46 @@ const applySocialLinks = (content, links) => {
|
|
|
11
18
|
url: link.url || '',
|
|
12
19
|
}));
|
|
13
20
|
};
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Merge path: id-based add / edit / move / remove in a single pass (BP-5603).
|
|
23
|
+
*
|
|
24
|
+
* - Existing items are id-backfilled first so legacy sections gain stable ids
|
|
25
|
+
* the moment the metadata builder touches them.
|
|
26
|
+
* - `_remove_item_ids` deletes by id before the edit/add loop.
|
|
27
|
+
* - An item with `item_id` matching an existing link is updated in place and
|
|
28
|
+
* optionally repositioned via `move_before_item_id`.
|
|
29
|
+
* - An item without `item_id` is added as a new link, positioned by
|
|
30
|
+
* `move_before_item_id` (or appended when omitted).
|
|
31
|
+
* - An item with an unknown `item_id` is skipped rather than guessing position.
|
|
32
|
+
*/
|
|
33
|
+
const mergeSocialLinks = (content, links, removeIds) => {
|
|
34
|
+
ensureItemIds(content.links);
|
|
35
|
+
content.links = removeItemsByIds(content.links, removeIds);
|
|
36
|
+
for (const link of links) {
|
|
37
|
+
if (link.item_id) {
|
|
38
|
+
const existing = content.links.find((l) => l.id === link.item_id);
|
|
39
|
+
// Unknown id — skip rather than guess at a position.
|
|
40
|
+
if (!existing)
|
|
41
|
+
continue;
|
|
42
|
+
if (link.platform !== undefined)
|
|
43
|
+
existing.imageUrl = link.platform;
|
|
44
|
+
if (link.url !== undefined)
|
|
45
|
+
existing.url = link.url;
|
|
46
|
+
repositionItemByAnchor(content.links, existing, link.move_before_item_id);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
const newLink = {
|
|
50
|
+
id: generateItemId(),
|
|
51
|
+
imageUrl: link.platform ?? '',
|
|
52
|
+
imageTitle: '',
|
|
53
|
+
width: 0,
|
|
54
|
+
height: 0,
|
|
55
|
+
imageLink: '',
|
|
56
|
+
url: link.url ?? '',
|
|
57
|
+
};
|
|
58
|
+
repositionItemByAnchor(content.links, newLink, link.move_before_item_id);
|
|
59
|
+
}
|
|
25
60
|
}
|
|
26
|
-
content.links = updatedLinks;
|
|
27
61
|
};
|
|
28
62
|
export class SocialLinksBuilder extends BaseSectionBuilder {
|
|
29
63
|
get sectionValidate() {
|
|
@@ -42,13 +76,19 @@ export class SocialLinksBuilder extends BaseSectionBuilder {
|
|
|
42
76
|
applyScalarFields(content, this.args, this.sectionType);
|
|
43
77
|
applyStyleFields(content, this.args, this.sectionType, this.getGlobalTextStyles(model));
|
|
44
78
|
const args = this.args;
|
|
45
|
-
if (args.links && Array.isArray(args.links)) {
|
|
46
|
-
|
|
79
|
+
if (args._replace_items && args.links && Array.isArray(args.links)) {
|
|
80
|
+
// Full replacement: discard existing links and rebuild from the payload.
|
|
81
|
+
applySocialLinks(content, args.links);
|
|
82
|
+
}
|
|
83
|
+
else if (this.mode === 'create') {
|
|
84
|
+
if (args.links && Array.isArray(args.links)) {
|
|
47
85
|
applySocialLinks(content, args.links);
|
|
48
86
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
87
|
+
}
|
|
88
|
+
else if ((args.links && Array.isArray(args.links)) ||
|
|
89
|
+
args._remove_item_ids?.length) {
|
|
90
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
91
|
+
mergeSocialLinks(content, args.links ?? [], args._remove_item_ids);
|
|
52
92
|
}
|
|
53
93
|
return { request: this.request, model };
|
|
54
94
|
}
|
|
@@ -4,6 +4,7 @@ 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 { validateTestimonialSection, TESTIMONIAL_ITEM_CAP, } from '../section-rules/testimonial-section.rules';
|
|
7
|
+
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
7
8
|
const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
8
9
|
let colors = existing ?? [];
|
|
9
10
|
if (item.avatar_shape_url && item.avatar_shape_url !== prevImageUrl) {
|
|
@@ -16,6 +17,7 @@ const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
|
16
17
|
};
|
|
17
18
|
const applyTestimonialItemsAsync = async (content, items) => {
|
|
18
19
|
content.items = await Promise.all(items.map(async (item) => ({
|
|
20
|
+
id: item.item_id || generateItemId(),
|
|
19
21
|
imageUrl: item.avatar_shape_url || item.avatar_image_url || '',
|
|
20
22
|
name: item.name || '',
|
|
21
23
|
position: item.position || '',
|
|
@@ -24,16 +26,28 @@ const applyTestimonialItemsAsync = async (content, items) => {
|
|
|
24
26
|
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
25
27
|
})));
|
|
26
28
|
};
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Apply a batch of item operations by id (BP-5603), mirroring the section-level
|
|
31
|
+
* id-based contract one level deeper:
|
|
32
|
+
* - item with `item_id` matching an existing item → update in place (and
|
|
33
|
+
* reposition if `move_before_item_id` is set);
|
|
34
|
+
* - item without `item_id` → add a new item,
|
|
35
|
+
* positioned by `move_before_item_id`;
|
|
36
|
+
* - `_remove_item_ids` → delete by id.
|
|
37
|
+
*
|
|
38
|
+
* Existing items are id-backfilled first so legacy sections become referenceable
|
|
39
|
+
* the moment the metadata builder touches them. Order-independent: there are no
|
|
40
|
+
* positional indices to shift between operations.
|
|
41
|
+
*/
|
|
42
|
+
const mergeTestimonialItemsAsync = async (content, items, removeIds) => {
|
|
43
|
+
ensureItemIds(content.items);
|
|
44
|
+
content.items = removeItemsByIds(content.items, removeIds);
|
|
45
|
+
for (const item of items) {
|
|
46
|
+
if (item.item_id) {
|
|
47
|
+
const existing = content.items.find((i) => i.id === item.item_id);
|
|
48
|
+
// Unknown id — skip rather than guess at a position.
|
|
49
|
+
if (!existing)
|
|
50
|
+
continue;
|
|
37
51
|
const prevImageUrl = existing.imageUrl;
|
|
38
52
|
if (item.avatar_shape_url !== undefined) {
|
|
39
53
|
existing.imageUrl = item.avatar_shape_url;
|
|
@@ -53,37 +67,21 @@ const mergeTestimonialItemsAsync = async (content, items, replaceAll) => {
|
|
|
53
67
|
item.color_theme !== undefined) {
|
|
54
68
|
existing.colorReplacements = await resolveItemColorsAsync(item, existing.colorReplacements, prevImageUrl);
|
|
55
69
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: (existing?.mediaType ?? 'image');
|
|
71
|
-
return {
|
|
72
|
-
...(existing || {}),
|
|
73
|
-
imageUrl,
|
|
74
|
-
name: item.name ?? existing?.name ?? '',
|
|
75
|
-
position: item.position ?? existing?.position ?? '',
|
|
76
|
-
testimonials: item.testimonial ?? existing?.testimonials ?? '',
|
|
77
|
-
colorReplacements: await resolveItemColorsAsync(item, existing?.colorReplacements, existing?.imageUrl),
|
|
78
|
-
mediaType,
|
|
79
|
-
};
|
|
80
|
-
}));
|
|
81
|
-
if (!replaceAll &&
|
|
82
|
-
content.items &&
|
|
83
|
-
content.items.length > itemsToMerge.length) {
|
|
84
|
-
updatedItems.push(...content.items.slice(itemsToMerge.length));
|
|
70
|
+
repositionItemByAnchor(content.items, existing, item.move_before_item_id);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const newItem = {
|
|
74
|
+
id: generateItemId(),
|
|
75
|
+
imageUrl: item.avatar_shape_url ?? item.avatar_image_url ?? '',
|
|
76
|
+
name: item.name ?? '',
|
|
77
|
+
position: item.position ?? '',
|
|
78
|
+
testimonials: item.testimonial ?? '',
|
|
79
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
80
|
+
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
81
|
+
};
|
|
82
|
+
repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
|
|
83
|
+
}
|
|
85
84
|
}
|
|
86
|
-
content.items = updatedItems;
|
|
87
85
|
};
|
|
88
86
|
export class TestimonialSectionBuilder extends BaseSectionBuilder {
|
|
89
87
|
get sectionValidate() {
|
|
@@ -132,56 +130,24 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder {
|
|
|
132
130
|
])),
|
|
133
131
|
};
|
|
134
132
|
}
|
|
135
|
-
if (args.items && Array.isArray(args.items)) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
139
|
-
else {
|
|
140
|
-
await mergeTestimonialItemsAsync(content, args.items, !!args._replace_items);
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
// Phase 2: Item operations (remove, insert, reorder)
|
|
144
|
-
if (args._remove_item_indices?.length && content.items) {
|
|
145
|
-
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
146
|
-
for (const idx of sorted) {
|
|
147
|
-
if (idx >= 0 && idx < content.items.length) {
|
|
148
|
-
content.items.splice(idx, 1);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
133
|
+
if (args._replace_items && args.items && Array.isArray(args.items)) {
|
|
134
|
+
// Full replacement: discard existing items and rebuild from the payload.
|
|
135
|
+
await applyTestimonialItemsAsync(content, args.items);
|
|
151
136
|
}
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
for (const item of sortedInserts) {
|
|
156
|
-
const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.items?.length ?? 0));
|
|
157
|
-
const newItem = {
|
|
158
|
-
imageUrl: item.avatar_shape_url ?? item.avatar_image_url ?? '',
|
|
159
|
-
name: item.name ?? '',
|
|
160
|
-
position: item.position ?? '',
|
|
161
|
-
testimonials: item.testimonial ?? '',
|
|
162
|
-
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
163
|
-
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
164
|
-
};
|
|
165
|
-
content.items = content.items ?? [];
|
|
166
|
-
content.items.splice(pos, 0, newItem);
|
|
137
|
+
else if (this.mode === 'create') {
|
|
138
|
+
if (args.items && Array.isArray(args.items)) {
|
|
139
|
+
await applyTestimonialItemsAsync(content, args.items);
|
|
167
140
|
}
|
|
168
141
|
}
|
|
169
|
-
if (args.
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const reordered = newOrder
|
|
174
|
-
.filter((idx) => idx >= 0 && idx < content.items.length)
|
|
175
|
-
.map((idx) => content.items[idx]);
|
|
176
|
-
if (reordered.length === content.items.length) {
|
|
177
|
-
content.items = reordered;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
142
|
+
else if ((args.items && Array.isArray(args.items)) ||
|
|
143
|
+
args._remove_item_ids?.length) {
|
|
144
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
145
|
+
await mergeTestimonialItemsAsync(content, args.items ?? [], args._remove_item_ids);
|
|
180
146
|
}
|
|
181
147
|
// Post-merge cap check. itemCapCheck only fires for create / update with
|
|
182
|
-
// _replace_items, so
|
|
183
|
-
// the array. Surface as an error so the AI retry loop gets a signal
|
|
184
|
-
// can reissue with
|
|
148
|
+
// _replace_items, so adding new items to an already-full section silently
|
|
149
|
+
// bloats the array. Surface as an error so the AI retry loop gets a signal
|
|
150
|
+
// and can reissue with _remove_item_ids or fewer additions.
|
|
185
151
|
// brandSite key mirrors base-section-builder's hardcoded brandPageType.
|
|
186
152
|
const cap = TESTIMONIAL_ITEM_CAP.brandSite;
|
|
187
153
|
if (content.items && content.items.length > cap) {
|