@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.
Files changed (109) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-BfQcnhof.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-B86JfNzV.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-Dy70BV7X.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-CQ8UhGRG.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-c257u4q-.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-c257u4q-.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-cRZ_Lx3_.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-CrfHKR-1.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-BFjhMPTR.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-CZimjPcn.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-V1z3-WzE.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-NrmWKrvi.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-CKLASXM9.js → CreatedOnDcom-pt-pt-DFsncYax.js} +1 -1
  15. package/lib/src/brand-page/components/image-gallery/ImageGallery.stories.js +3 -0
  16. package/lib/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.d.ts +1 -1
  17. package/lib/src/brand-page/components/images-section/ImagesSection.stories.js +2 -1
  18. package/lib/src/brand-page/components/social-links/SocialLinks.stories.js +3 -0
  19. package/lib/src/brand-page/components/testimonial-section/__fixtures__/data.js +3 -0
  20. package/lib/src/brand-page/components/text/Text.stories.js +2 -0
  21. package/lib/src/brand-page/models/index.d.ts +13 -0
  22. package/lib/src/brand-page/models/index.js +4 -0
  23. package/lib/src/brand-page/utils/migration/migration-01.js +2 -0
  24. package/lib/src/brand-page/utils/migration/migration-18.d.ts +14 -0
  25. package/lib/src/brand-page/utils/migration/migration-18.js +17 -0
  26. package/lib/src/brand-page/utils/migration.util.js +6 -0
  27. package/lib/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.js +1 -0
  28. package/lib/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.js +1 -0
  29. package/lib/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.js +5 -0
  30. package/lib/src/brand-page-maker/constants/config-settings-constant.js +4 -0
  31. package/lib/src/brand-page-maker/models/content-settings.js +4 -0
  32. package/lib/src/brand-page-maker/stores/ai-checkpoint/ai-checkpoint-store.d.ts +18 -0
  33. package/lib/src/index.mjs +1 -1
  34. package/lib/src/{lib-CHfUKgt-.js → lib-BG-pyXi9.js} +19091 -18974
  35. package/lib/src/metadata-builder/ai-metadata-executor.js +24 -1
  36. package/lib/src/metadata-builder/builders/footer-builder.js +32 -41
  37. package/lib/src/metadata-builder/builders/item-id-utils.d.ts +54 -0
  38. package/lib/src/metadata-builder/builders/item-id-utils.js +96 -0
  39. package/lib/src/metadata-builder/builders/move-page-builder.d.ts +32 -0
  40. package/lib/src/metadata-builder/builders/move-page-builder.js +137 -0
  41. package/lib/src/metadata-builder/builders/nav-utils.d.ts +26 -0
  42. package/lib/src/metadata-builder/builders/nav-utils.js +95 -0
  43. package/lib/src/metadata-builder/builders/section-builders/image-gallery-builder.js +57 -68
  44. package/lib/src/metadata-builder/builders/section-builders/images-section-builder.js +58 -93
  45. package/lib/src/metadata-builder/builders/section-builders/move-section-builder.d.ts +13 -1
  46. package/lib/src/metadata-builder/builders/section-builders/move-section-builder.js +40 -30
  47. package/lib/src/metadata-builder/builders/section-builders/shapes-section-builder.js +66 -99
  48. package/lib/src/metadata-builder/builders/section-builders/social-links-builder.js +57 -17
  49. package/lib/src/metadata-builder/builders/section-builders/testimonial-section-builder.js +51 -85
  50. package/lib/src/metadata-builder/builders/section-builders/text-section-builder.js +50 -74
  51. package/lib/src/metadata-builder/builders/section-field-maps.js +17 -0
  52. package/lib/src/metadata-builder/builders/set-page-visibility-builder.d.ts +23 -0
  53. package/lib/src/metadata-builder/builders/set-page-visibility-builder.js +83 -0
  54. package/lib/src/metadata-builder/metadata-executor.js +4 -0
  55. package/lib/src/metadata-builder/models.d.ts +2 -0
  56. package/lib/src/metadata-builder/models.js +2 -0
  57. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.d.ts +31 -7
  58. package/lib/src/metadata-builder/parsers/ai-toolcall-parser.js +2 -0
  59. package/lib/src/metadata-builder/parsers/section-tool-arguments.d.ts +84 -48
  60. package/lib/src/shared/content-settings/components/links-field/LinksField.mixin.js +2 -0
  61. package/lib/src/shared/page-settings/components/page-settings/PageSettings.mixin.d.ts +2 -0
  62. package/lib/src/shared/page-settings/components/page-settings-section-styles/PageSettingsSectionStyles.mixin.d.ts +2 -0
  63. package/lib/src/utils/minify-brand-page-metadata.d.ts +2 -0
  64. package/lib/src/utils/minify-brand-page-metadata.js +8 -1
  65. package/package.json +1 -1
  66. package/src/brand-page/components/image-gallery/ImageGallery.stories.ts +3 -0
  67. package/src/brand-page/components/image-gallery/ImageGallery.test.ts +1 -0
  68. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +2 -1
  69. package/src/brand-page/components/images-section/ImagesSection.stories.ts +2 -1
  70. package/src/brand-page/components/social-links/SocialLinks.stories.ts +3 -0
  71. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +5 -0
  72. package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +3 -0
  73. package/src/brand-page/components/text/Text.stories.ts +2 -0
  74. package/src/brand-page/models/index.ts +17 -0
  75. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +23 -3
  76. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -0
  77. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -0
  78. package/src/brand-page/utils/migration/migration-01.ts +2 -0
  79. package/src/brand-page/utils/migration/migration-18.test.ts +131 -0
  80. package/src/brand-page/utils/migration/migration-18.ts +22 -0
  81. package/src/brand-page/utils/migration.util.ts +7 -0
  82. package/src/brand-page/utils/personalisation.util.test.ts +4 -0
  83. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
  84. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -0
  85. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +6 -0
  86. package/src/brand-page-maker/constants/config-settings-constant.ts +4 -0
  87. package/src/brand-page-maker/models/content-settings.ts +4 -0
  88. package/src/metadata-builder/ai-metadata-executor.ts +31 -1
  89. package/src/metadata-builder/builders/footer-builder.test.ts +335 -28
  90. package/src/metadata-builder/builders/footer-builder.ts +40 -57
  91. package/src/metadata-builder/builders/item-id-utils.test.ts +184 -0
  92. package/src/metadata-builder/builders/item-id-utils.ts +117 -0
  93. package/src/metadata-builder/builders/move-page-builder.ts +227 -0
  94. package/src/metadata-builder/builders/nav-utils.ts +141 -0
  95. package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +62 -88
  96. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +66 -129
  97. package/src/metadata-builder/builders/section-builders/move-section-builder.ts +58 -35
  98. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +105 -160
  99. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +65 -20
  100. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +83 -146
  101. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +71 -122
  102. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  103. package/src/metadata-builder/builders/set-page-visibility-builder.ts +122 -0
  104. package/src/metadata-builder/metadata-executor.ts +4 -0
  105. package/src/metadata-builder/models.ts +2 -0
  106. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +32 -7
  107. package/src/metadata-builder/parsers/section-tool-arguments.ts +84 -48
  108. package/src/shared/content-settings/components/links-field/LinksField.mixin.ts +2 -0
  109. 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, index) => ({
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: index,
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
- replaceAll: boolean,
98
+ removeIds: string[] | undefined,
70
99
  ): Promise<void> => {
71
- const hasIndexedItems = items.some((item) => item.item_index !== undefined);
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
- return;
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
- const itemsToMerge = items.filter((item) => item.insert_at === undefined);
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
- if (
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
- updatedItems.push(...remaining);
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
- content.items = updatedItems;
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
- if (this.mode === 'create') {
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
- if (args.items && Array.isArray(args.items)) {
256
- const insertItems = args.items.filter(
257
- (item) => item.insert_at !== undefined && item.item_index === undefined,
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
- replaceAll: boolean,
55
+ removeIds: string[] | undefined,
32
56
  ): void => {
33
- const updatedLinks = links.map((link, index) => {
34
- const existing = content.links?.[index];
35
-
36
- return {
37
- ...(existing || {}),
38
- imageUrl: link.platform ?? existing?.imageUrl ?? '',
39
- url: link.url ?? existing?.url ?? '',
40
- };
41
- });
42
-
43
- if (!replaceAll && content.links && content.links.length > links.length) {
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
- content.links = updatedLinks;
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
- if (this.mode === 'create') {
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 };