@designcrowd/library.brand-page 6.0.12 → 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-BfQcnhof.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-en-us-B86JfNzV.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-es-es-Dy70BV7X.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-fr-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-fr-ca-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-pt-c257u4q-.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnBrandCrowd-pt-br-c257u4q-.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
- package/lib/src/{CreatedOnDcom-de-de-cRZ_Lx3_.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
- package/lib/src/{CreatedOnDcom-en-us-CrfHKR-1.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
- package/lib/src/{CreatedOnDcom-es-es-BFjhMPTR.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-ca-CZimjPcn.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
- package/lib/src/{CreatedOnDcom-fr-fr-V1z3-WzE.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-br-NrmWKrvi.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
- package/lib/src/{CreatedOnDcom-pt-pt-CKLASXM9.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 +1 -1
- package/lib/src/{lib-CHfUKgt-.js → lib-BG-pyXi9.js} +19091 -18974
- 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/utils/minify-brand-page-metadata.d.ts +2 -0
- package/lib/src/utils/minify-brand-page-metadata.js +8 -1
- package/package.json +1 -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/utils/minify-brand-page-metadata.ts +14 -2
|
@@ -23,6 +23,12 @@ import {
|
|
|
23
23
|
} from '../shape-color-utils';
|
|
24
24
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
25
25
|
import { validateShapesSection } from '../section-rules/shapes-section.rules';
|
|
26
|
+
import {
|
|
27
|
+
ensureItemIds,
|
|
28
|
+
generateItemId,
|
|
29
|
+
removeItemsByIds,
|
|
30
|
+
repositionItemByAnchor,
|
|
31
|
+
} from '../item-id-utils';
|
|
26
32
|
|
|
27
33
|
const resolveItemColorsAsync = async (
|
|
28
34
|
item: ShapesSectionItem,
|
|
@@ -43,111 +49,117 @@ const resolveItemColorsAsync = async (
|
|
|
43
49
|
return colors;
|
|
44
50
|
};
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Full replacement: discard existing items and rebuild from the payload.
|
|
54
|
+
* Used for create mode and _replace_items: true.
|
|
55
|
+
*/
|
|
46
56
|
const applyShapeItemsAsync = async (
|
|
47
57
|
content: ShapesSectionContent,
|
|
48
58
|
items: ShapesSectionItem[],
|
|
49
59
|
): Promise<void> => {
|
|
50
60
|
content.items = await Promise.all(
|
|
51
|
-
items.map(async (item
|
|
61
|
+
items.map(async (item) => ({
|
|
62
|
+
id: item.item_id || generateItemId(),
|
|
52
63
|
imageUrl: item.shape_url || item.icon_image_url || '',
|
|
53
64
|
imageTitle: '',
|
|
54
65
|
width: 0,
|
|
55
66
|
height: 0,
|
|
56
|
-
mediaType: 'shape',
|
|
57
|
-
imageLink: '',
|
|
67
|
+
mediaType: 'shape' as const,
|
|
68
|
+
imageLink: '' as const,
|
|
58
69
|
title: item.heading || '',
|
|
59
70
|
paragraph: item.text || '',
|
|
60
|
-
itemsId:
|
|
71
|
+
itemsId: 0,
|
|
61
72
|
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
62
73
|
})),
|
|
63
74
|
);
|
|
75
|
+
|
|
76
|
+
// Re-index itemsId after build
|
|
77
|
+
content.items.forEach((item, i) => {
|
|
78
|
+
(item as TextShapeItem).itemsId = i;
|
|
79
|
+
});
|
|
64
80
|
};
|
|
65
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Apply a batch of item operations by id (BP-5603), mirroring the section-level
|
|
84
|
+
* id-based contract one level deeper:
|
|
85
|
+
* - item with `item_id` matching an existing item → update in place (and
|
|
86
|
+
* reposition if `move_before_item_id` is set);
|
|
87
|
+
* - item without `item_id` → add a new item,
|
|
88
|
+
* positioned by `move_before_item_id`;
|
|
89
|
+
* - `_remove_item_ids` → delete by id.
|
|
90
|
+
*
|
|
91
|
+
* Existing items are id-backfilled first so legacy sections become referenceable
|
|
92
|
+
* the moment the metadata builder touches them. Order-independent: there are no
|
|
93
|
+
* positional indices to shift between operations.
|
|
94
|
+
*/
|
|
66
95
|
const mergeShapeItemsAsync = async (
|
|
67
96
|
content: ShapesSectionContent,
|
|
68
97
|
items: ShapesSectionItem[],
|
|
69
|
-
|
|
98
|
+
removeIds: string[] | undefined,
|
|
70
99
|
): Promise<void> => {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (hasIndexedItems) {
|
|
74
|
-
await Promise.all(
|
|
75
|
-
items.map(async (item) => {
|
|
76
|
-
const targetIndex = item.item_index;
|
|
77
|
-
|
|
78
|
-
if (
|
|
79
|
-
targetIndex === undefined ||
|
|
80
|
-
targetIndex < 0 ||
|
|
81
|
-
!content.items?.[targetIndex]
|
|
82
|
-
)
|
|
83
|
-
return;
|
|
84
|
-
|
|
85
|
-
const existing = content.items[targetIndex] as TextShapeItem;
|
|
86
|
-
const prevImageUrl = existing.imageUrl;
|
|
87
|
-
|
|
88
|
-
if (item.shape_url !== undefined || item.icon_image_url !== undefined) {
|
|
89
|
-
existing.imageUrl =
|
|
90
|
-
item.shape_url ?? item.icon_image_url ?? existing.imageUrl;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (item.heading !== undefined) existing.title = item.heading;
|
|
94
|
-
|
|
95
|
-
if (item.text !== undefined) existing.paragraph = item.text;
|
|
96
|
-
|
|
97
|
-
if (item.shape_url !== undefined || item.color_theme !== undefined) {
|
|
98
|
-
existing.colorReplacements = await resolveItemColorsAsync(
|
|
99
|
-
item,
|
|
100
|
-
existing.colorReplacements,
|
|
101
|
-
prevImageUrl,
|
|
102
|
-
);
|
|
103
|
-
}
|
|
104
|
-
}),
|
|
105
|
-
);
|
|
100
|
+
ensureItemIds(content.items as TextShapeItem[]);
|
|
101
|
+
content.items = removeItemsByIds(content.items as TextShapeItem[], removeIds);
|
|
106
102
|
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
for (const item of items) {
|
|
104
|
+
if (item.item_id) {
|
|
105
|
+
const existing = (content.items as TextShapeItem[]).find(
|
|
106
|
+
(i) => i.id === item.item_id,
|
|
107
|
+
);
|
|
109
108
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const updatedItems = await Promise.all(
|
|
113
|
-
itemsToMerge.map(async (item, index) => {
|
|
114
|
-
const existing = content.items?.[index];
|
|
115
|
-
const imageUrl =
|
|
116
|
-
item.shape_url ?? item.icon_image_url ?? existing?.imageUrl ?? '';
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
...(existing || {}),
|
|
120
|
-
imageUrl,
|
|
121
|
-
imageTitle: existing?.imageTitle ?? '',
|
|
122
|
-
width: existing?.width ?? 0,
|
|
123
|
-
height: existing?.height ?? 0,
|
|
124
|
-
mediaType: existing?.mediaType ?? 'shape',
|
|
125
|
-
imageLink: existing?.imageLink ?? '',
|
|
126
|
-
title: item.heading ?? existing?.title ?? '',
|
|
127
|
-
paragraph: item.text ?? existing?.paragraph ?? '',
|
|
128
|
-
itemsId: existing?.itemsId ?? index,
|
|
129
|
-
colorReplacements: await resolveItemColorsAsync(
|
|
130
|
-
item,
|
|
131
|
-
existing?.colorReplacements,
|
|
132
|
-
existing?.imageUrl,
|
|
133
|
-
),
|
|
134
|
-
};
|
|
135
|
-
}),
|
|
136
|
-
);
|
|
109
|
+
// Unknown id — skip rather than guess at a position.
|
|
110
|
+
if (!existing) continue;
|
|
137
111
|
|
|
138
|
-
|
|
139
|
-
!replaceAll &&
|
|
140
|
-
content.items &&
|
|
141
|
-
content.items.length > itemsToMerge.length
|
|
142
|
-
) {
|
|
143
|
-
const remaining = content.items.slice(
|
|
144
|
-
itemsToMerge.length,
|
|
145
|
-
) as typeof updatedItems;
|
|
112
|
+
const prevImageUrl = existing.imageUrl;
|
|
146
113
|
|
|
147
|
-
|
|
114
|
+
if (item.shape_url !== undefined || item.icon_image_url !== undefined) {
|
|
115
|
+
existing.imageUrl =
|
|
116
|
+
item.shape_url ?? item.icon_image_url ?? existing.imageUrl;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (item.heading !== undefined) existing.title = item.heading;
|
|
120
|
+
|
|
121
|
+
if (item.text !== undefined) existing.paragraph = item.text;
|
|
122
|
+
|
|
123
|
+
if (item.shape_url !== undefined || item.color_theme !== undefined) {
|
|
124
|
+
existing.colorReplacements = await resolveItemColorsAsync(
|
|
125
|
+
item,
|
|
126
|
+
existing.colorReplacements,
|
|
127
|
+
prevImageUrl,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
repositionItemByAnchor(
|
|
132
|
+
content.items as TextShapeItem[],
|
|
133
|
+
existing,
|
|
134
|
+
item.move_before_item_id,
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
const newItem = {
|
|
138
|
+
id: generateItemId(),
|
|
139
|
+
imageUrl: item.shape_url ?? item.icon_image_url ?? '',
|
|
140
|
+
imageTitle: '',
|
|
141
|
+
width: 0,
|
|
142
|
+
height: 0,
|
|
143
|
+
mediaType: 'shape' as const,
|
|
144
|
+
imageLink: '' as const,
|
|
145
|
+
title: item.heading ?? '',
|
|
146
|
+
paragraph: item.text ?? '',
|
|
147
|
+
itemsId: 0,
|
|
148
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
149
|
+
} as TextShapeItem;
|
|
150
|
+
|
|
151
|
+
repositionItemByAnchor(
|
|
152
|
+
content.items as TextShapeItem[],
|
|
153
|
+
newItem,
|
|
154
|
+
item.move_before_item_id,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
148
157
|
}
|
|
149
158
|
|
|
150
|
-
|
|
159
|
+
// Re-index itemsId after all mutations
|
|
160
|
+
(content.items as TextShapeItem[]).forEach((item, i) => {
|
|
161
|
+
item.itemsId = i;
|
|
162
|
+
});
|
|
151
163
|
};
|
|
152
164
|
|
|
153
165
|
export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionContent> {
|
|
@@ -233,90 +245,23 @@ export class ShapesSectionBuilder extends BaseSectionBuilder<ShapesSectionConten
|
|
|
233
245
|
};
|
|
234
246
|
}
|
|
235
247
|
|
|
236
|
-
if (args.items && Array.isArray(args.items)) {
|
|
237
|
-
|
|
248
|
+
if (args._replace_items && args.items && Array.isArray(args.items)) {
|
|
249
|
+
// Full replacement: discard existing items and rebuild from the payload.
|
|
250
|
+
await applyShapeItemsAsync(content, args.items);
|
|
251
|
+
} else if (this.mode === 'create') {
|
|
252
|
+
if (args.items && Array.isArray(args.items)) {
|
|
238
253
|
await applyShapeItemsAsync(content, args.items);
|
|
239
|
-
} else {
|
|
240
|
-
await mergeShapeItemsAsync(content, args.items, !!args._replace_items);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Phase 2: Item operations (remove, insert, reorder)
|
|
245
|
-
if (args._remove_item_indices?.length && content.items) {
|
|
246
|
-
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
247
|
-
|
|
248
|
-
for (const idx of sorted) {
|
|
249
|
-
if (idx >= 0 && idx < content.items.length) {
|
|
250
|
-
content.items.splice(idx, 1);
|
|
251
|
-
}
|
|
252
254
|
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
} else if (
|
|
256
|
+
(args.items && Array.isArray(args.items)) ||
|
|
257
|
+
args._remove_item_ids?.length
|
|
258
|
+
) {
|
|
259
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
260
|
+
await mergeShapeItemsAsync(
|
|
261
|
+
content,
|
|
262
|
+
args.items ?? [],
|
|
263
|
+
args._remove_item_ids,
|
|
258
264
|
);
|
|
259
|
-
// Process inserts in reverse order of insert_at to preserve indices
|
|
260
|
-
const sortedInserts = [...insertItems].sort(
|
|
261
|
-
(a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0),
|
|
262
|
-
);
|
|
263
|
-
|
|
264
|
-
for (const item of sortedInserts) {
|
|
265
|
-
const insertUrl = item.shape_url ?? item.icon_image_url ?? '';
|
|
266
|
-
|
|
267
|
-
// Skip if this shape/icon already exists — defensive against duplicate
|
|
268
|
-
// tool call application
|
|
269
|
-
if (
|
|
270
|
-
insertUrl &&
|
|
271
|
-
content.items?.some(
|
|
272
|
-
(existing: any) => existing.imageUrl === insertUrl,
|
|
273
|
-
)
|
|
274
|
-
) {
|
|
275
|
-
continue;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const pos = Math.max(
|
|
279
|
-
0,
|
|
280
|
-
Math.min(item.insert_at ?? 0, content.items?.length ?? 0),
|
|
281
|
-
);
|
|
282
|
-
const newItem = {
|
|
283
|
-
imageUrl: insertUrl,
|
|
284
|
-
imageTitle: '',
|
|
285
|
-
width: 0,
|
|
286
|
-
height: 0,
|
|
287
|
-
mediaType: 'shape',
|
|
288
|
-
imageLink: '',
|
|
289
|
-
title: item.heading ?? '',
|
|
290
|
-
paragraph: item.text ?? '',
|
|
291
|
-
itemsId: pos,
|
|
292
|
-
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
content.items = content.items ?? [];
|
|
296
|
-
content.items.splice(pos, 0, newItem as any);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (args._reorder_items?.length && content.items) {
|
|
301
|
-
const newOrder = args._reorder_items;
|
|
302
|
-
const unique = new Set(newOrder);
|
|
303
|
-
|
|
304
|
-
if (unique.size === newOrder.length) {
|
|
305
|
-
const reordered = newOrder
|
|
306
|
-
.filter((idx) => idx >= 0 && idx < content.items!.length)
|
|
307
|
-
.map((idx) => content.items![idx]);
|
|
308
|
-
|
|
309
|
-
if (reordered.length === content.items.length) {
|
|
310
|
-
content.items = reordered;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Re-index itemsId after any item operations
|
|
316
|
-
if (content.items) {
|
|
317
|
-
content.items.forEach((item, i) => {
|
|
318
|
-
item.itemsId = i;
|
|
319
|
-
});
|
|
320
265
|
}
|
|
321
266
|
|
|
322
267
|
return { request: this.request, model };
|
|
@@ -10,12 +10,24 @@ import type { BuilderResult } from '../../models';
|
|
|
10
10
|
import { applyScalarFields, applyStyleFields } from '../section-field-utils';
|
|
11
11
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
12
12
|
import { validateSocialLinks } from '../section-rules/social-links.rules';
|
|
13
|
-
|
|
13
|
+
import {
|
|
14
|
+
ensureItemIds,
|
|
15
|
+
generateItemId,
|
|
16
|
+
removeItemsByIds,
|
|
17
|
+
repositionItemByAnchor,
|
|
18
|
+
} from '../item-id-utils';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create path: build the links array from scratch, assigning stable ids.
|
|
22
|
+
* `link.platform` is stored as `imageUrl` — the renderer resolves it to the
|
|
23
|
+
* actual SVG at display time.
|
|
24
|
+
*/
|
|
14
25
|
const applySocialLinks = (
|
|
15
26
|
content: SocialLinksContent,
|
|
16
27
|
links: SocialLink[],
|
|
17
28
|
): void => {
|
|
18
29
|
content.links = links.map((link) => ({
|
|
30
|
+
id: link.item_id || generateItemId(),
|
|
19
31
|
imageUrl: link.platform || '',
|
|
20
32
|
imageTitle: '',
|
|
21
33
|
width: 0,
|
|
@@ -25,26 +37,52 @@ const applySocialLinks = (
|
|
|
25
37
|
}));
|
|
26
38
|
};
|
|
27
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Merge path: id-based add / edit / move / remove in a single pass (BP-5603).
|
|
42
|
+
*
|
|
43
|
+
* - Existing items are id-backfilled first so legacy sections gain stable ids
|
|
44
|
+
* the moment the metadata builder touches them.
|
|
45
|
+
* - `_remove_item_ids` deletes by id before the edit/add loop.
|
|
46
|
+
* - An item with `item_id` matching an existing link is updated in place and
|
|
47
|
+
* optionally repositioned via `move_before_item_id`.
|
|
48
|
+
* - An item without `item_id` is added as a new link, positioned by
|
|
49
|
+
* `move_before_item_id` (or appended when omitted).
|
|
50
|
+
* - An item with an unknown `item_id` is skipped rather than guessing position.
|
|
51
|
+
*/
|
|
28
52
|
const mergeSocialLinks = (
|
|
29
53
|
content: SocialLinksContent,
|
|
30
54
|
links: SocialLink[],
|
|
31
|
-
|
|
55
|
+
removeIds: string[] | undefined,
|
|
32
56
|
): void => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
updatedLinks.push(...content.links.slice(links.length));
|
|
45
|
-
}
|
|
57
|
+
ensureItemIds(content.links);
|
|
58
|
+
content.links = removeItemsByIds(content.links, removeIds);
|
|
59
|
+
|
|
60
|
+
for (const link of links) {
|
|
61
|
+
if (link.item_id) {
|
|
62
|
+
const existing = content.links.find((l) => l.id === link.item_id);
|
|
63
|
+
|
|
64
|
+
// Unknown id — skip rather than guess at a position.
|
|
65
|
+
if (!existing) continue;
|
|
66
|
+
|
|
67
|
+
if (link.platform !== undefined) existing.imageUrl = link.platform;
|
|
46
68
|
|
|
47
|
-
|
|
69
|
+
if (link.url !== undefined) existing.url = link.url;
|
|
70
|
+
|
|
71
|
+
repositionItemByAnchor(content.links, existing, link.move_before_item_id);
|
|
72
|
+
} else {
|
|
73
|
+
const newLink = {
|
|
74
|
+
id: generateItemId(),
|
|
75
|
+
imageUrl: link.platform ?? '',
|
|
76
|
+
imageTitle: '',
|
|
77
|
+
width: 0,
|
|
78
|
+
height: 0,
|
|
79
|
+
imageLink: '' as const,
|
|
80
|
+
url: link.url ?? '',
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
repositionItemByAnchor(content.links, newLink, link.move_before_item_id);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
48
86
|
};
|
|
49
87
|
|
|
50
88
|
export class SocialLinksBuilder extends BaseSectionBuilder<SocialLinksContent> {
|
|
@@ -77,12 +115,19 @@ export class SocialLinksBuilder extends BaseSectionBuilder<SocialLinksContent> {
|
|
|
77
115
|
|
|
78
116
|
const args = this.args as SocialLinksSectionArguments;
|
|
79
117
|
|
|
80
|
-
if (args.links && Array.isArray(args.links)) {
|
|
81
|
-
|
|
118
|
+
if (args._replace_items && args.links && Array.isArray(args.links)) {
|
|
119
|
+
// Full replacement: discard existing links and rebuild from the payload.
|
|
120
|
+
applySocialLinks(content, args.links);
|
|
121
|
+
} else if (this.mode === 'create') {
|
|
122
|
+
if (args.links && Array.isArray(args.links)) {
|
|
82
123
|
applySocialLinks(content, args.links);
|
|
83
|
-
} else {
|
|
84
|
-
mergeSocialLinks(content, args.links, !!args._replace_items);
|
|
85
124
|
}
|
|
125
|
+
} else if (
|
|
126
|
+
(args.links && Array.isArray(args.links)) ||
|
|
127
|
+
args._remove_item_ids?.length
|
|
128
|
+
) {
|
|
129
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
130
|
+
mergeSocialLinks(content, args.links ?? [], args._remove_item_ids);
|
|
86
131
|
}
|
|
87
132
|
|
|
88
133
|
return { request: this.request, model };
|