@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
|
@@ -1,15 +1,8 @@
|
|
|
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
|
-
*/
|
|
10
4
|
const applySocialLinks = (content, links) => {
|
|
11
5
|
content.links = links.map((link) => ({
|
|
12
|
-
id: link.item_id || generateItemId(),
|
|
13
6
|
imageUrl: link.platform || '',
|
|
14
7
|
imageTitle: '',
|
|
15
8
|
width: 0,
|
|
@@ -18,46 +11,19 @@ const applySocialLinks = (content, links) => {
|
|
|
18
11
|
url: link.url || '',
|
|
19
12
|
}));
|
|
20
13
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
}
|
|
14
|
+
const mergeSocialLinks = (content, links, replaceAll) => {
|
|
15
|
+
const updatedLinks = links.map((link, index) => {
|
|
16
|
+
const existing = content.links?.[index];
|
|
17
|
+
return {
|
|
18
|
+
...(existing || {}),
|
|
19
|
+
imageUrl: link.platform ?? existing?.imageUrl ?? '',
|
|
20
|
+
url: link.url ?? existing?.url ?? '',
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
if (!replaceAll && content.links && content.links.length > links.length) {
|
|
24
|
+
updatedLinks.push(...content.links.slice(links.length));
|
|
60
25
|
}
|
|
26
|
+
content.links = updatedLinks;
|
|
61
27
|
};
|
|
62
28
|
export class SocialLinksBuilder extends BaseSectionBuilder {
|
|
63
29
|
get sectionValidate() {
|
|
@@ -76,19 +42,13 @@ export class SocialLinksBuilder extends BaseSectionBuilder {
|
|
|
76
42
|
applyScalarFields(content, this.args, this.sectionType);
|
|
77
43
|
applyStyleFields(content, this.args, this.sectionType, this.getGlobalTextStyles(model));
|
|
78
44
|
const args = this.args;
|
|
79
|
-
if (args.
|
|
80
|
-
|
|
81
|
-
applySocialLinks(content, args.links);
|
|
82
|
-
}
|
|
83
|
-
else if (this.mode === 'create') {
|
|
84
|
-
if (args.links && Array.isArray(args.links)) {
|
|
45
|
+
if (args.links && Array.isArray(args.links)) {
|
|
46
|
+
if (this.mode === 'create') {
|
|
85
47
|
applySocialLinks(content, args.links);
|
|
86
48
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
// Update: id-based add / edit / move / remove in a single pass.
|
|
91
|
-
mergeSocialLinks(content, args.links ?? [], args._remove_item_ids);
|
|
49
|
+
else {
|
|
50
|
+
mergeSocialLinks(content, args.links, !!args._replace_items);
|
|
51
|
+
}
|
|
92
52
|
}
|
|
93
53
|
return { request: this.request, model };
|
|
94
54
|
}
|
|
@@ -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 { validateTestimonialSection, TESTIMONIAL_ITEM_CAP, } from '../section-rules/testimonial-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
|
if (item.avatar_shape_url && item.avatar_shape_url !== prevImageUrl) {
|
|
@@ -17,7 +16,6 @@ const resolveItemColorsAsync = async (item, existing, prevImageUrl) => {
|
|
|
17
16
|
};
|
|
18
17
|
const applyTestimonialItemsAsync = async (content, items) => {
|
|
19
18
|
content.items = await Promise.all(items.map(async (item) => ({
|
|
20
|
-
id: item.item_id || generateItemId(),
|
|
21
19
|
imageUrl: item.avatar_shape_url || item.avatar_image_url || '',
|
|
22
20
|
name: item.name || '',
|
|
23
21
|
position: item.position || '',
|
|
@@ -26,28 +24,16 @@ const applyTestimonialItemsAsync = async (content, items) => {
|
|
|
26
24
|
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
27
25
|
})));
|
|
28
26
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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;
|
|
27
|
+
const mergeTestimonialItemsAsync = async (content, items, replaceAll) => {
|
|
28
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
29
|
+
if (hasIndexedItems) {
|
|
30
|
+
await Promise.all(items.map(async (item) => {
|
|
31
|
+
const targetIndex = item.item_index;
|
|
32
|
+
if (targetIndex === undefined ||
|
|
33
|
+
targetIndex < 0 ||
|
|
34
|
+
!content.items?.[targetIndex])
|
|
35
|
+
return;
|
|
36
|
+
const existing = content.items[targetIndex];
|
|
51
37
|
const prevImageUrl = existing.imageUrl;
|
|
52
38
|
if (item.avatar_shape_url !== undefined) {
|
|
53
39
|
existing.imageUrl = item.avatar_shape_url;
|
|
@@ -67,21 +53,37 @@ const mergeTestimonialItemsAsync = async (content, items, removeIds) => {
|
|
|
67
53
|
item.color_theme !== undefined) {
|
|
68
54
|
existing.colorReplacements = await resolveItemColorsAsync(item, existing.colorReplacements, prevImageUrl);
|
|
69
55
|
}
|
|
70
|
-
|
|
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
|
-
}
|
|
56
|
+
}));
|
|
57
|
+
return;
|
|
84
58
|
}
|
|
59
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
60
|
+
const updatedItems = await Promise.all(itemsToMerge.map(async (item, index) => {
|
|
61
|
+
const existing = content.items?.[index];
|
|
62
|
+
const imageUrl = item.avatar_shape_url ??
|
|
63
|
+
item.avatar_image_url ??
|
|
64
|
+
existing?.imageUrl ??
|
|
65
|
+
'';
|
|
66
|
+
const mediaType = item.avatar_shape_url
|
|
67
|
+
? 'shape'
|
|
68
|
+
: item.avatar_image_url
|
|
69
|
+
? 'image'
|
|
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));
|
|
85
|
+
}
|
|
86
|
+
content.items = updatedItems;
|
|
85
87
|
};
|
|
86
88
|
export class TestimonialSectionBuilder extends BaseSectionBuilder {
|
|
87
89
|
get sectionValidate() {
|
|
@@ -130,24 +132,56 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder {
|
|
|
130
132
|
])),
|
|
131
133
|
};
|
|
132
134
|
}
|
|
133
|
-
if (args.
|
|
134
|
-
|
|
135
|
-
await applyTestimonialItemsAsync(content, args.items);
|
|
136
|
-
}
|
|
137
|
-
else if (this.mode === 'create') {
|
|
138
|
-
if (args.items && Array.isArray(args.items)) {
|
|
135
|
+
if (args.items && Array.isArray(args.items)) {
|
|
136
|
+
if (this.mode === 'create') {
|
|
139
137
|
await applyTestimonialItemsAsync(content, args.items);
|
|
140
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
|
+
}
|
|
141
151
|
}
|
|
142
|
-
|
|
143
|
-
args.
|
|
144
|
-
|
|
145
|
-
|
|
152
|
+
if (args.items && Array.isArray(args.items)) {
|
|
153
|
+
const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
|
|
154
|
+
const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
|
|
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);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (args._reorder_items?.length && content.items) {
|
|
170
|
+
const newOrder = args._reorder_items;
|
|
171
|
+
const unique = new Set(newOrder);
|
|
172
|
+
if (unique.size === newOrder.length) {
|
|
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
|
+
}
|
|
146
180
|
}
|
|
147
181
|
// Post-merge cap check. itemCapCheck only fires for create / update with
|
|
148
|
-
// _replace_items, so
|
|
149
|
-
//
|
|
150
|
-
//
|
|
182
|
+
// _replace_items, so insert_at on an already-full section silently bloats
|
|
183
|
+
// the array. Surface as an error so the AI retry loop gets a signal and
|
|
184
|
+
// can reissue with _remove_item_indices or fewer inserts.
|
|
151
185
|
// brandSite key mirrors base-section-builder's hardcoded brandPageType.
|
|
152
186
|
const cap = TESTIMONIAL_ITEM_CAP.brandSite;
|
|
153
187
|
if (content.items && content.items.length > cap) {
|
|
@@ -3,7 +3,6 @@ import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
|
|
|
3
3
|
import { applyScalarFields, applyStyleFields, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
|
|
4
4
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
5
5
|
import { validateTextSection } from '../section-rules/text-section.rules';
|
|
6
|
-
import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
|
|
7
6
|
/** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
|
|
8
7
|
const resolveText = (value) => {
|
|
9
8
|
if (isTiptapDocument(value))
|
|
@@ -17,58 +16,55 @@ const createTextItem = (item, baseStyle, textStyleOptions) => {
|
|
|
17
16
|
const textStyle = (item.text_style ? { ...baseStyle, ...item.text_style } : { ...baseStyle });
|
|
18
17
|
const { html, wasTiptap } = resolveText(item.text);
|
|
19
18
|
return {
|
|
20
|
-
id: item.item_id || generateItemId(),
|
|
21
19
|
text: html,
|
|
22
20
|
textStyle: clearTextStyleTypeIfTiptap(textStyle, wasTiptap, textStyleOptions),
|
|
23
21
|
};
|
|
24
22
|
};
|
|
25
|
-
/**
|
|
26
|
-
* Full replacement: discard existing items and rebuild from the payload,
|
|
27
|
-
* assigning a stable id to each new item.
|
|
28
|
-
*/
|
|
29
23
|
const applyTextItems = (content, items, textStyleOptions) => {
|
|
30
24
|
content.textItems = items.map((item) => createTextItem(item, content.textStyle || {}, textStyleOptions));
|
|
31
25
|
};
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*
|
|
41
|
-
* Existing items are id-backfilled first so legacy sections without ids
|
|
42
|
-
* become referenceable the moment this builder processes them.
|
|
43
|
-
*/
|
|
44
|
-
const mergeTextItems = (content, items, removeIds, textStyleOptions) => {
|
|
45
|
-
ensureItemIds(content.textItems);
|
|
46
|
-
content.textItems = removeItemsByIds(content.textItems, removeIds);
|
|
47
|
-
for (const item of items) {
|
|
48
|
-
if (item.item_id) {
|
|
49
|
-
const existing = content.textItems?.find((i) => i.id === item.item_id);
|
|
50
|
-
// Unknown id — skip rather than guess at a position.
|
|
51
|
-
if (!existing)
|
|
26
|
+
const mergeTextItems = (content, items, replaceAll, textStyleOptions) => {
|
|
27
|
+
const hasIndexedItems = items.some((item) => item.item_index !== undefined);
|
|
28
|
+
if (hasIndexedItems) {
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
const targetIndex = item.item_index;
|
|
31
|
+
if (targetIndex === undefined ||
|
|
32
|
+
targetIndex < 0 ||
|
|
33
|
+
!content.textItems?.[targetIndex])
|
|
52
34
|
continue;
|
|
35
|
+
const existing = content.textItems[targetIndex];
|
|
53
36
|
if (item.text !== undefined) {
|
|
54
37
|
const { html, wasTiptap } = resolveText(item.text);
|
|
55
38
|
existing.text = html;
|
|
56
39
|
existing.textStyle = clearTextStyleTypeIfTiptap(existing.textStyle || {}, wasTiptap, textStyleOptions);
|
|
57
40
|
}
|
|
58
41
|
if (item.text_style && typeof item.text_style === 'object') {
|
|
59
|
-
|
|
60
|
-
...(existing.textStyle || {}),
|
|
61
|
-
|
|
62
|
-
|
|
42
|
+
const mergedStyle = (item.text_style
|
|
43
|
+
? { ...(existing.textStyle || {}), ...item.text_style }
|
|
44
|
+
: existing.textStyle || {});
|
|
45
|
+
existing.textStyle = mergedStyle;
|
|
63
46
|
}
|
|
64
|
-
repositionItemByAnchor(content.textItems, existing, item.move_before_item_id);
|
|
65
|
-
}
|
|
66
|
-
else {
|
|
67
|
-
const newItem = createTextItem(item, content.textStyle || {}, textStyleOptions);
|
|
68
|
-
content.textItems = content.textItems ?? [];
|
|
69
|
-
repositionItemByAnchor(content.textItems, newItem, item.move_before_item_id);
|
|
70
47
|
}
|
|
48
|
+
return;
|
|
71
49
|
}
|
|
50
|
+
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
51
|
+
const updatedItems = itemsToMerge.map((item, index) => {
|
|
52
|
+
const existing = content.textItems?.[index];
|
|
53
|
+
const baseStyle = existing?.textStyle || content.textStyle || {};
|
|
54
|
+
const mergedStyle = (item.text_style ? { ...baseStyle, ...item.text_style } : baseStyle);
|
|
55
|
+
const resolved = item.text !== undefined ? resolveText(item.text) : null;
|
|
56
|
+
const text = resolved ? resolved.html : existing?.text ?? '';
|
|
57
|
+
return {
|
|
58
|
+
text,
|
|
59
|
+
textStyle: clearTextStyleTypeIfTiptap(mergedStyle, resolved?.wasTiptap ?? false, textStyleOptions),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
if (!replaceAll &&
|
|
63
|
+
content.textItems &&
|
|
64
|
+
content.textItems.length > itemsToMerge.length) {
|
|
65
|
+
updatedItems.push(...content.textItems.slice(itemsToMerge.length));
|
|
66
|
+
}
|
|
67
|
+
content.textItems = updatedItems;
|
|
72
68
|
};
|
|
73
69
|
/**
|
|
74
70
|
* Handles the single `text` + `text_style` fields (BrandPage/BrandContact
|
|
@@ -125,27 +121,56 @@ export class TextSectionBuilder extends BaseSectionBuilder {
|
|
|
125
121
|
applyScalarFields(content, this.args, this.sectionType);
|
|
126
122
|
applyStyleFields(content, this.args, this.sectionType, this.getGlobalTextStyles(model));
|
|
127
123
|
const args = this.args;
|
|
128
|
-
if (args.
|
|
129
|
-
|
|
130
|
-
applyTextItems(content, args.items, textStyleOptions);
|
|
131
|
-
}
|
|
132
|
-
else if (this.mode === 'create') {
|
|
133
|
-
if (args.items && Array.isArray(args.items)) {
|
|
124
|
+
if (args.items && Array.isArray(args.items)) {
|
|
125
|
+
if (this.mode === 'create') {
|
|
134
126
|
applyTextItems(content, args.items, textStyleOptions);
|
|
135
127
|
}
|
|
136
128
|
else {
|
|
137
|
-
|
|
129
|
+
mergeTextItems(content, args.items, !!args._replace_items, textStyleOptions);
|
|
138
130
|
}
|
|
139
131
|
}
|
|
140
|
-
else if ((args.items && Array.isArray(args.items)) ||
|
|
141
|
-
args._remove_item_ids?.length) {
|
|
142
|
-
// Update: id-based add / edit / move / remove in a single pass.
|
|
143
|
-
mergeTextItems(content, args.items ?? [], args._remove_item_ids, textStyleOptions);
|
|
144
|
-
}
|
|
145
132
|
else {
|
|
146
|
-
// No items array: apply the single text/text_style shorthand fields.
|
|
147
133
|
applySingleTextField(content, args, textStyleOptions);
|
|
148
134
|
}
|
|
135
|
+
// Phase 2: Item operations (remove, insert, reorder)
|
|
136
|
+
if (args._remove_item_indices?.length && content.textItems) {
|
|
137
|
+
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
138
|
+
for (const idx of sorted) {
|
|
139
|
+
if (idx >= 0 && idx < content.textItems.length) {
|
|
140
|
+
content.textItems.splice(idx, 1);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (args.items && Array.isArray(args.items)) {
|
|
145
|
+
const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
|
|
146
|
+
const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
|
|
147
|
+
for (const item of sortedInserts) {
|
|
148
|
+
const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.textItems?.length ?? 0));
|
|
149
|
+
const baseStyle = content.textStyle || {};
|
|
150
|
+
const textStyle = (item.text_style
|
|
151
|
+
? { ...baseStyle, ...item.text_style }
|
|
152
|
+
: { ...baseStyle });
|
|
153
|
+
const { html, wasTiptap } = resolveText(item.text);
|
|
154
|
+
const newItem = {
|
|
155
|
+
text: html,
|
|
156
|
+
textStyle: clearTextStyleTypeIfTiptap(textStyle, wasTiptap, textStyleOptions),
|
|
157
|
+
};
|
|
158
|
+
content.textItems = content.textItems ?? [];
|
|
159
|
+
content.textItems.splice(pos, 0, newItem);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (args._reorder_items?.length && content.textItems) {
|
|
163
|
+
const newOrder = args._reorder_items;
|
|
164
|
+
const unique = new Set(newOrder);
|
|
165
|
+
if (unique.size === newOrder.length) {
|
|
166
|
+
const reordered = newOrder
|
|
167
|
+
.filter((idx) => idx >= 0 && idx < content.textItems.length)
|
|
168
|
+
.map((idx) => content.textItems[idx]);
|
|
169
|
+
if (reordered.length === content.textItems.length) {
|
|
170
|
+
content.textItems = reordered;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
149
174
|
if (args.background_color !== undefined) {
|
|
150
175
|
content.backgroundColor = {
|
|
151
176
|
...content.backgroundColor,
|
|
@@ -99,17 +99,8 @@ export interface ReorderPagesArguments {
|
|
|
99
99
|
export interface UndoArguments {
|
|
100
100
|
}
|
|
101
101
|
export interface FooterSocialLinkItem {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
* Mirrors the section-level item_id convention (BP-5603).
|
|
105
|
-
*/
|
|
106
|
-
item_id?: string;
|
|
107
|
-
/**
|
|
108
|
-
* Id of an existing link to position this link before (insert for new links,
|
|
109
|
-
* move for existing ones). Use "__end__" for the end of the list. Omit to
|
|
110
|
-
* leave an existing link in place / append a new link.
|
|
111
|
-
*/
|
|
112
|
-
move_before_item_id?: string;
|
|
102
|
+
item_index?: number;
|
|
103
|
+
insert_at?: number;
|
|
113
104
|
social_link_icon?: string;
|
|
114
105
|
url?: string;
|
|
115
106
|
}
|
|
@@ -131,8 +122,8 @@ export interface UpdateFooterArguments {
|
|
|
131
122
|
copyright_text_font_family?: string;
|
|
132
123
|
social_links?: FooterSocialLinkItem[];
|
|
133
124
|
_replace_social_links?: boolean;
|
|
134
|
-
|
|
135
|
-
|
|
125
|
+
_remove_social_link_indices?: number[];
|
|
126
|
+
_reorder_social_links?: number[];
|
|
136
127
|
/** Desktop-only styling (display_style, background_color, icon_color, is_logo_visible, logo sizes). */
|
|
137
128
|
desktop?: ViewportFooterOverrides;
|
|
138
129
|
/** Mobile-only styling (display_style, background_color, icon_color, is_logo_visible, logo sizes). */
|