@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.
Files changed (117) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-DopPc6g2.js → CreatedOnBrandCrowd-de-de-CBjFFeBP.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-CSHmaAWm.js → CreatedOnBrandCrowd-en-us-NuNXF81K.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-C4vM0p2e.js → CreatedOnBrandCrowd-es-es-BJG8nfug.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-ArpBbnxm.js → CreatedOnBrandCrowd-fr-ca-uX-nL53e.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-ArpBbnxm.js → CreatedOnBrandCrowd-fr-fr-uX-nL53e.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-CGAQLXiX.js → CreatedOnBrandCrowd-pt-br-DTrgFGQV.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-CGAQLXiX.js → CreatedOnBrandCrowd-pt-pt-DTrgFGQV.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-3WVSgB6t.js → CreatedOnDcom-de-de-DSfpxioT.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-Dx-RHXrH.js → CreatedOnDcom-en-us-C7G4gKem.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CfnEYFMA.js → CreatedOnDcom-es-es-DVckCaji.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-BSHZTiUO.js → CreatedOnDcom-fr-ca-C5GDrm_C.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-DtHssrQb.js → CreatedOnDcom-fr-fr-v5eMm-j2.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-CdX1aRI8.js → CreatedOnDcom-pt-br-Dq3-fPUc.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-jy044FNL.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 +84 -83
  34. package/lib/src/{lib-Bx7xK4dn.js → lib-BG-pyXi9.js} +19585 -19807
  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/tracker.d.ts +2 -0
  64. package/lib/src/tracker.js +12 -0
  65. package/lib/src/tracker.mjs +12 -0
  66. package/lib/src/useEventTracker-DmQ6AkNn.js +356 -0
  67. package/lib/src/utils/minify-brand-page-metadata.d.ts +2 -0
  68. package/lib/src/utils/minify-brand-page-metadata.js +8 -1
  69. package/package.json +16 -1
  70. package/src/brand-page/components/image-gallery/ImageGallery.stories.ts +3 -0
  71. package/src/brand-page/components/image-gallery/ImageGallery.test.ts +1 -0
  72. package/src/brand-page/components/image-gallery/masonry/MasonryGallery.mixin.ts +2 -1
  73. package/src/brand-page/components/images-section/ImagesSection.stories.ts +2 -1
  74. package/src/brand-page/components/social-links/SocialLinks.stories.ts +3 -0
  75. package/src/brand-page/components/social-links/SocialLinks.vue.test.ts +5 -0
  76. package/src/brand-page/components/testimonial-section/__fixtures__/data.ts +3 -0
  77. package/src/brand-page/components/text/Text.stories.ts +2 -0
  78. package/src/brand-page/models/index.ts +17 -0
  79. package/src/brand-page/utils/__snapshots__/migration.util.test.ts.snap +23 -3
  80. package/src/brand-page/utils/__snapshots__/migration2.util.test.ts.snap +6 -0
  81. package/src/brand-page/utils/__snapshots__/model.util.test.ts.snap +12 -0
  82. package/src/brand-page/utils/migration/migration-01.ts +2 -0
  83. package/src/brand-page/utils/migration/migration-18.test.ts +131 -0
  84. package/src/brand-page/utils/migration/migration-18.ts +22 -0
  85. package/src/brand-page/utils/migration.util.ts +7 -0
  86. package/src/brand-page/utils/personalisation.util.test.ts +4 -0
  87. package/src/brand-page-maker/components/brand-page-maker/BrandPageMaker.stories.ts +1 -0
  88. package/src/brand-page-maker/components/brand-page-maker/__fixtures__/brand-page-examples.ts +1 -0
  89. package/src/brand-page-maker/composables/ai-chat/useAiWebsiteBuilderChat.ts +6 -0
  90. package/src/brand-page-maker/constants/config-settings-constant.ts +4 -0
  91. package/src/brand-page-maker/models/content-settings.ts +4 -0
  92. package/src/metadata-builder/ai-metadata-executor.ts +31 -1
  93. package/src/metadata-builder/builders/footer-builder.test.ts +335 -28
  94. package/src/metadata-builder/builders/footer-builder.ts +40 -57
  95. package/src/metadata-builder/builders/item-id-utils.test.ts +184 -0
  96. package/src/metadata-builder/builders/item-id-utils.ts +117 -0
  97. package/src/metadata-builder/builders/move-page-builder.ts +227 -0
  98. package/src/metadata-builder/builders/nav-utils.ts +141 -0
  99. package/src/metadata-builder/builders/section-builders/image-gallery-builder.ts +62 -88
  100. package/src/metadata-builder/builders/section-builders/images-section-builder.ts +66 -129
  101. package/src/metadata-builder/builders/section-builders/move-section-builder.ts +58 -35
  102. package/src/metadata-builder/builders/section-builders/shapes-section-builder.ts +105 -160
  103. package/src/metadata-builder/builders/section-builders/social-links-builder.ts +65 -20
  104. package/src/metadata-builder/builders/section-builders/testimonial-section-builder.ts +83 -146
  105. package/src/metadata-builder/builders/section-builders/text-section-builder.ts +71 -122
  106. package/src/metadata-builder/builders/section-field-maps.ts +18 -0
  107. package/src/metadata-builder/builders/set-page-visibility-builder.ts +122 -0
  108. package/src/metadata-builder/metadata-executor.ts +4 -0
  109. package/src/metadata-builder/models.ts +2 -0
  110. package/src/metadata-builder/parsers/ai-toolcall-parser.ts +32 -7
  111. package/src/metadata-builder/parsers/section-tool-arguments.ts +84 -48
  112. package/src/shared/content-settings/components/links-field/LinksField.mixin.ts +2 -0
  113. package/src/tracker.ts +28 -0
  114. package/src/utils/minify-brand-page-metadata.ts +14 -2
  115. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
  116. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
  117. package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
@@ -1,14 +1,21 @@
1
1
  import { applyScalarFields, isValidUrl } from '../section-field-utils';
2
2
  import { BaseSectionBuilder } from './base-section-builder';
3
3
  import { validateImageGallery } from '../section-rules/image-gallery.rules';
4
+ import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
4
5
  const applyGalleryType = (content, galleryType) => {
5
6
  content.galleryStyles = {
6
7
  ...(content.galleryStyles || {}),
7
8
  galleryType: galleryType,
8
9
  };
9
10
  };
11
+ /**
12
+ * Full rebuild: discard existing images and map the payload directly.
13
+ * Used on create and when `_replace_items` is set. Each item gets a fresh id
14
+ * (or keeps the caller-supplied `item_id` when provided).
15
+ */
10
16
  const applyGalleryImages = (content, images) => {
11
17
  content.images = images.map((img) => ({
18
+ id: img.item_id || generateItemId(),
12
19
  imageUrl: isValidUrl(img.image_url) ? img.image_url : '',
13
20
  imageTitle: img.caption || '',
14
21
  width: 0,
@@ -17,59 +24,46 @@ const applyGalleryImages = (content, images) => {
17
24
  imageLink: '',
18
25
  }));
19
26
  };
20
- const mergeGalleryImages = (content, images, replaceAll) => {
21
- const itemsToMerge = images.filter((img) => img.insert_at === undefined);
22
- const updatedImages = itemsToMerge.map((img, index) => {
23
- const existing = content.images?.[index];
24
- const imageUrl = isValidUrl(img.image_url)
25
- ? img.image_url
26
- : (existing?.imageUrl ?? '');
27
- return {
28
- ...(existing || {}),
29
- imageUrl,
30
- imageTitle: img.caption ?? existing?.imageTitle ?? '',
31
- width: existing?.width ?? 0,
32
- height: existing?.height ?? 0,
33
- mediaType: existing?.mediaType ?? 'image',
34
- imageLink: existing?.imageLink ?? '',
35
- };
36
- });
37
- if (!replaceAll &&
38
- content.images &&
39
- content.images.length > itemsToMerge.length) {
40
- const extras = content.images.slice(itemsToMerge.length).map((img) => ({
41
- imageUrl: img.imageUrl ?? '',
42
- imageTitle: img.imageTitle ?? '',
43
- width: img.width ?? 0,
44
- height: img.height ?? 0,
45
- mediaType: img.mediaType ?? 'image',
46
- imageLink: img.imageLink ?? '',
47
- }));
48
- updatedImages.push(...extras);
49
- }
50
- content.images = updatedImages;
51
- };
52
- const insertGalleryImages = (content, images) => {
53
- const inserts = images.filter((img) => img.insert_at !== undefined);
54
- if (!inserts.length)
55
- return;
56
- const sorted = [...inserts].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
57
- for (const img of sorted) {
58
- if (!isValidUrl(img.image_url))
59
- continue;
60
- if (content.images?.some((existing) => existing.imageUrl === img.image_url))
61
- continue;
62
- content.images = content.images ?? [];
63
- const pos = Math.max(0, Math.min(img.insert_at ?? 0, content.images.length));
64
- content.images.splice(pos, 0, {
65
- imageUrl: img.image_url,
66
- imageTitle: img.caption ?? '',
67
- width: 0,
68
- height: 0,
69
- mediaType: 'image',
70
- imageLink: '',
71
- colorReplacements: [],
72
- });
27
+ /**
28
+ * Id-based merge (BP-5603), mirroring testimonial-section-builder one level
29
+ * deeper into `content.images`:
30
+ * - `item_id` matches existing item → update in place; reposition if
31
+ * `move_before_item_id` is set.
32
+ * - no `item_id` → add new item, positioned by
33
+ * `move_before_item_id`.
34
+ * - `removeIds` → delete by id (order-independent).
35
+ *
36
+ * Existing items are id-backfilled first so legacy galleries become
37
+ * referenceable as soon as the metadata builder touches them.
38
+ */
39
+ const mergeGalleryImages = (content, images, removeIds) => {
40
+ ensureItemIds(content.images);
41
+ content.images = removeItemsByIds(content.images, removeIds);
42
+ for (const img of images) {
43
+ if (img.item_id) {
44
+ const existing = content.images.find((i) => i.id === img.item_id);
45
+ // Unknown id — skip rather than guess at a position.
46
+ if (!existing)
47
+ continue;
48
+ if (img.image_url !== undefined) {
49
+ existing.imageUrl = isValidUrl(img.image_url) ? img.image_url : '';
50
+ }
51
+ if (img.caption !== undefined)
52
+ existing.imageTitle = img.caption;
53
+ repositionItemByAnchor(content.images, existing, img.move_before_item_id);
54
+ }
55
+ else {
56
+ const newItem = {
57
+ id: generateItemId(),
58
+ imageUrl: isValidUrl(img.image_url) ? img.image_url : '',
59
+ imageTitle: img.caption ?? '',
60
+ width: 0,
61
+ height: 0,
62
+ mediaType: 'image',
63
+ imageLink: '',
64
+ };
65
+ repositionItemByAnchor(content.images, newItem, img.move_before_item_id);
66
+ }
73
67
  }
74
68
  };
75
69
  export class ImageGalleryBuilder extends BaseSectionBuilder {
@@ -91,24 +85,19 @@ export class ImageGalleryBuilder extends BaseSectionBuilder {
91
85
  if (args.gallery_type) {
92
86
  applyGalleryType(content, args.gallery_type);
93
87
  }
94
- if (args.images && Array.isArray(args.images)) {
95
- if (this.mode === 'create') {
96
- applyGalleryImages(content, args.images);
97
- }
98
- else {
99
- mergeGalleryImages(content, args.images, !!args._replace_items);
100
- }
88
+ if (args._replace_items && args.images && Array.isArray(args.images)) {
89
+ // Full replacement: discard existing images and rebuild from the payload.
90
+ applyGalleryImages(content, args.images);
101
91
  }
102
- if (args._remove_item_indices?.length && content.images) {
103
- const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
104
- for (const idx of sorted) {
105
- if (idx >= 0 && idx < content.images.length) {
106
- content.images.splice(idx, 1);
107
- }
92
+ else if (this.mode === 'create') {
93
+ if (args.images && Array.isArray(args.images)) {
94
+ applyGalleryImages(content, args.images);
108
95
  }
109
96
  }
110
- if (args.images && Array.isArray(args.images) && this.mode === 'update') {
111
- insertGalleryImages(content, args.images);
97
+ else if ((args.images && Array.isArray(args.images)) ||
98
+ args._remove_item_ids?.length) {
99
+ // Update: id-based add / edit / move / remove in a single pass.
100
+ mergeGalleryImages(content, args.images ?? [], args._remove_item_ids);
112
101
  }
113
102
  return { request: this.request, model };
114
103
  }
@@ -3,8 +3,15 @@ import { isTiptapDocument, } from '../../parsers/section-tool-arguments';
3
3
  import { applyScalarFields, applyStyleFields, isValidUrl, resolveGlobalPresetIntoStyle, } from '../section-field-utils';
4
4
  import { BaseSectionBuilder } from './base-section-builder';
5
5
  import { validateImagesSection } from '../section-rules/images-section.rules';
6
+ import { ensureItemIds, generateItemId, removeItemsByIds, repositionItemByAnchor, } from '../item-id-utils';
7
+ /**
8
+ * Full replacement: discard existing items and rebuild from the payload.
9
+ * Assigns a stable `id` to each item; `itemsId` is set positionally here
10
+ * but will be re-indexed at the end of buildAsync.
11
+ */
6
12
  const applyImageItems = (content, items) => {
7
13
  content.items = items.map((item, index) => ({
14
+ id: item.item_id || generateItemId(),
8
15
  imageUrl: isValidUrl(item.image_url) ? item.image_url : '',
9
16
  imageTitle: '',
10
17
  width: 0,
@@ -18,16 +25,28 @@ const applyImageItems = (content, items) => {
18
25
  colorReplacements: [],
19
26
  }));
20
27
  };
21
- const mergeImageItems = (content, items, replaceAll) => {
22
- const hasIndexedItems = items.some((item) => item.item_index !== undefined);
23
- if (hasIndexedItems) {
24
- for (const item of items) {
25
- const targetIndex = item.item_index;
26
- if (targetIndex === undefined ||
27
- targetIndex < 0 ||
28
- !content.items?.[targetIndex])
28
+ /**
29
+ * Apply a batch of item operations by id (BP-5603), mirroring the
30
+ * section-level id-based contract one level deeper:
31
+ * - item with `item_id` matching an existing item → update in place (and
32
+ * reposition if `move_before_item_id` is set);
33
+ * - item without `item_id` → add a new item,
34
+ * positioned by `move_before_item_id`;
35
+ * - `_remove_item_ids` → delete by id.
36
+ *
37
+ * Existing items are id-backfilled first so legacy sections become
38
+ * referenceable the moment the metadata builder touches them.
39
+ * Order-independent: there are no positional indices to shift between ops.
40
+ */
41
+ const mergeImageItems = (content, items, removeIds) => {
42
+ ensureItemIds(content.items);
43
+ content.items = removeItemsByIds(content.items, removeIds);
44
+ for (const item of items) {
45
+ if (item.item_id) {
46
+ const existing = content.items.find((i) => i.id === item.item_id);
47
+ // Unknown id — skip rather than guess at a position.
48
+ if (!existing)
29
49
  continue;
30
- const existing = content.items[targetIndex];
31
50
  if (isValidUrl(item.image_url))
32
51
  existing.imageUrl = item.image_url;
33
52
  if (item.heading !== undefined)
@@ -36,36 +55,26 @@ const mergeImageItems = (content, items, replaceAll) => {
36
55
  existing.paragraph = item.text;
37
56
  if (item.button_text !== undefined)
38
57
  existing.buttonText = item.button_text || false;
58
+ repositionItemByAnchor(content.items, existing, item.move_before_item_id);
59
+ }
60
+ else {
61
+ const newItem = {
62
+ id: generateItemId(),
63
+ imageUrl: isValidUrl(item.image_url) ? item.image_url : '',
64
+ imageTitle: '',
65
+ width: 0,
66
+ height: 0,
67
+ mediaType: 'image',
68
+ imageLink: '',
69
+ title: item.heading ?? '',
70
+ paragraph: item.text ?? '',
71
+ buttonText: item.button_text || false,
72
+ itemsId: 0, // re-indexed at the end of buildAsync
73
+ colorReplacements: [],
74
+ };
75
+ repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
39
76
  }
40
- return;
41
- }
42
- const itemsToMerge = items.filter((item) => item.insert_at === undefined);
43
- const updatedItems = itemsToMerge.map((item, index) => {
44
- const existing = content.items?.[index];
45
- return {
46
- ...(existing || {}),
47
- imageUrl: isValidUrl(item.image_url)
48
- ? item.image_url
49
- : (existing?.imageUrl ?? ''),
50
- imageTitle: existing?.imageTitle ?? '',
51
- width: existing?.width ?? 0,
52
- height: existing?.height ?? 0,
53
- mediaType: existing?.mediaType ?? 'image',
54
- imageLink: existing?.imageLink ?? '',
55
- title: item.heading ?? existing?.title ?? '',
56
- paragraph: item.text ?? existing?.paragraph ?? '',
57
- buttonText: item.button_text ?? existing?.buttonText ?? '',
58
- itemsId: existing?.itemsId ?? index,
59
- colorReplacements: existing?.colorReplacements ?? [],
60
- };
61
- });
62
- if (!replaceAll &&
63
- content.items &&
64
- content.items.length > itemsToMerge.length) {
65
- const remaining = content.items.slice(itemsToMerge.length);
66
- updatedItems.push(...remaining);
67
77
  }
68
- content.items = updatedItems;
69
78
  };
70
79
  export class ImagesSectionBuilder extends BaseSectionBuilder {
71
80
  get sectionValidate() {
@@ -123,66 +132,22 @@ export class ImagesSectionBuilder extends BaseSectionBuilder {
123
132
  ])),
124
133
  };
125
134
  }
126
- if (args.items && Array.isArray(args.items)) {
127
- if (this.mode === 'create') {
128
- applyImageItems(content, args.items);
129
- }
130
- else {
131
- mergeImageItems(content, args.items, !!args._replace_items);
132
- }
133
- }
134
- // Phase 2: Item operations (remove, insert, reorder)
135
- if (args._remove_item_indices?.length && content.items) {
136
- const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
137
- for (const idx of sorted) {
138
- if (idx >= 0 && idx < content.items.length) {
139
- content.items.splice(idx, 1);
140
- }
141
- }
135
+ if (args._replace_items && args.items && Array.isArray(args.items)) {
136
+ // Full replacement: discard existing items and rebuild from the payload.
137
+ applyImageItems(content, args.items);
142
138
  }
143
- if (args.items && Array.isArray(args.items)) {
144
- const insertItems = args.items.filter((item) => item.insert_at !== undefined && item.item_index === undefined);
145
- // Process inserts in reverse order of insert_at to preserve indices
146
- const sortedInserts = [...insertItems].sort((a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0));
147
- for (const item of sortedInserts) {
148
- if (!isValidUrl(item.image_url))
149
- continue;
150
- // Skip if this image_url already exists — defensive against duplicate
151
- // tool call application
152
- if (content.items?.some((existing) => existing.imageUrl === item.image_url)) {
153
- continue;
154
- }
155
- const pos = Math.max(0, Math.min(item.insert_at ?? 0, content.items?.length ?? 0));
156
- const newItem = {
157
- imageUrl: item.image_url,
158
- imageTitle: '',
159
- width: 0,
160
- height: 0,
161
- mediaType: 'image',
162
- imageLink: '',
163
- title: item.heading ?? '',
164
- paragraph: item.text ?? '',
165
- buttonText: item.button_text || '',
166
- itemsId: pos,
167
- colorReplacements: [],
168
- };
169
- content.items = content.items ?? [];
170
- content.items.splice(pos, 0, newItem);
139
+ else if (this.mode === 'create') {
140
+ if (args.items && Array.isArray(args.items)) {
141
+ applyImageItems(content, args.items);
171
142
  }
172
143
  }
173
- if (args._reorder_items?.length && content.items) {
174
- const newOrder = args._reorder_items;
175
- const unique = new Set(newOrder);
176
- if (unique.size === newOrder.length) {
177
- const reordered = newOrder
178
- .filter((idx) => idx >= 0 && idx < content.items.length)
179
- .map((idx) => content.items[idx]);
180
- if (reordered.length === content.items.length) {
181
- content.items = reordered;
182
- }
183
- }
144
+ else if ((args.items && Array.isArray(args.items)) ||
145
+ args._remove_item_ids?.length) {
146
+ // Update: id-based add / edit / move / remove in a single pass.
147
+ mergeImageItems(content, args.items ?? [], args._remove_item_ids);
184
148
  }
185
- // Re-index itemsId after any item operations
149
+ // Re-index itemsId after all id-based item operations (render concern —
150
+ // separate from the stable `id` used for targeting).
186
151
  if (content.items) {
187
152
  content.items.forEach((item, i) => {
188
153
  item.itemsId = i;
@@ -3,7 +3,12 @@ import { BaseMetadataBuilder, type BuilderResult, type MetadataBuilderRequest }
3
3
  import type { Logger } from '../../logger';
4
4
  export interface MoveSectionPayload {
5
5
  contentId: string;
6
- position: number;
6
+ /**
7
+ * Relative anchor on the destination page: the content_id to insert this section
8
+ * BEFORE (a page's first content_id moves it to the very top). Omit to append to the
9
+ * end — a move always relocates the section, so "no anchor" means "append".
10
+ */
11
+ moveBeforeContentId?: string;
7
12
  targetPageId?: string;
8
13
  }
9
14
  export declare class MoveSectionBuilder extends BaseMetadataBuilder<MoveSectionPayload> {
@@ -12,4 +17,11 @@ export declare class MoveSectionBuilder extends BaseMetadataBuilder<MoveSectionP
12
17
  errors: string[];
13
18
  }>;
14
19
  buildAsync(model: BrandPageModel): Promise<BuilderResult>;
20
+ /**
21
+ * Resolve the insert index on a page from the relative anchor. `moveBeforeContentId`
22
+ * is the content_id to insert before; anything else — omitted, or an id not on the
23
+ * page — appends to the end. A move always relocates the section, so "no anchor"
24
+ * naturally means "append" (this also absorbs a legacy "__end__" token as append).
25
+ */
26
+ private resolveIndex;
15
27
  }
@@ -5,13 +5,9 @@ export class MoveSectionBuilder extends BaseMetadataBuilder {
5
5
  }
6
6
  async validateAsync() {
7
7
  const errors = [];
8
- const payload = this.request.payload;
9
- if (!payload?.contentId) {
8
+ if (!this.request.payload?.contentId) {
10
9
  errors.push('contentId is required for move_section');
11
10
  }
12
- if (payload?.position === undefined || payload.position < 0) {
13
- errors.push('position must be a non-negative integer');
14
- }
15
11
  return { errors };
16
12
  }
17
13
  async buildAsync(model) {
@@ -20,38 +16,38 @@ export class MoveSectionBuilder extends BaseMetadataBuilder {
20
16
  if (validation.errors.length) {
21
17
  return { request: this.request, model, errors: validation.errors };
22
18
  }
23
- const { contentId, position, targetPageId } = this.request.payload;
19
+ const { contentId, moveBeforeContentId, targetPageId } = this.request.payload;
24
20
  for (const page of model.pages) {
25
21
  const currentIndex = page.contents.findIndex((c) => c.id === contentId);
26
- if (currentIndex !== -1) {
27
- const [section] = page.contents.splice(currentIndex, 1);
28
- // Cross-page move: insert into target page
29
- if (targetPageId && targetPageId !== page.id) {
30
- const targetPage = model.pages.find((p) => p.id === targetPageId);
31
- if (!targetPage) {
32
- // Undo the splice and return an error
33
- page.contents.splice(currentIndex, 0, section);
34
- return {
35
- request: this.request,
36
- model,
37
- errors: [`Target page with ID "${targetPageId}" not found`],
38
- };
39
- }
40
- const targetIndex = Math.min(position, targetPage.contents.length);
41
- targetPage.contents.splice(targetIndex, 0, section);
42
- this.log({
43
- message: `Moved section ${contentId} from page ${page.id} index ${currentIndex} to page ${targetPageId} index ${targetIndex}`,
44
- });
45
- return { request: this.request, model };
22
+ if (currentIndex === -1)
23
+ continue;
24
+ const [section] = page.contents.splice(currentIndex, 1);
25
+ // Cross-page move: insert into target page
26
+ if (targetPageId && targetPageId !== page.id) {
27
+ const targetPage = model.pages.find((p) => p.id === targetPageId);
28
+ if (!targetPage) {
29
+ // Undo the splice and return an error
30
+ page.contents.splice(currentIndex, 0, section);
31
+ return {
32
+ request: this.request,
33
+ model,
34
+ errors: [`Target page with ID "${targetPageId}" not found`],
35
+ };
46
36
  }
47
- // Within-page reorder
48
- const targetIndex = Math.min(position, page.contents.length);
49
- page.contents.splice(targetIndex, 0, section);
37
+ const targetIndex = this.resolveIndex(targetPage.contents, moveBeforeContentId);
38
+ targetPage.contents.splice(targetIndex, 0, section);
50
39
  this.log({
51
- message: `Moved section ${contentId} from index ${currentIndex} to ${targetIndex} on page ${page.id}`,
40
+ message: `Moved section ${contentId} from page ${page.id} index ${currentIndex} to page ${targetPageId} index ${targetIndex}`,
52
41
  });
53
42
  return { request: this.request, model };
54
43
  }
44
+ // Within-page reorder
45
+ const targetIndex = this.resolveIndex(page.contents, moveBeforeContentId);
46
+ page.contents.splice(targetIndex, 0, section);
47
+ this.log({
48
+ message: `Moved section ${contentId} from index ${currentIndex} to ${targetIndex} on page ${page.id}`,
49
+ });
50
+ return { request: this.request, model };
55
51
  }
56
52
  return {
57
53
  request: this.request,
@@ -59,4 +55,18 @@ export class MoveSectionBuilder extends BaseMetadataBuilder {
59
55
  errors: [`Section with ID "${contentId}" not found`],
60
56
  };
61
57
  }
58
+ /**
59
+ * Resolve the insert index on a page from the relative anchor. `moveBeforeContentId`
60
+ * is the content_id to insert before; anything else — omitted, or an id not on the
61
+ * page — appends to the end. A move always relocates the section, so "no anchor"
62
+ * naturally means "append" (this also absorbs a legacy "__end__" token as append).
63
+ */
64
+ resolveIndex(contents, moveBeforeContentId) {
65
+ if (moveBeforeContentId) {
66
+ const idx = contents.findIndex((c) => c.id === moveBeforeContentId);
67
+ if (idx !== -1)
68
+ return idx;
69
+ }
70
+ return contents.length;
71
+ }
62
72
  }