@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
|
@@ -25,6 +25,12 @@ import {
|
|
|
25
25
|
validateTestimonialSection,
|
|
26
26
|
TESTIMONIAL_ITEM_CAP,
|
|
27
27
|
} from '../section-rules/testimonial-section.rules';
|
|
28
|
+
import {
|
|
29
|
+
ensureItemIds,
|
|
30
|
+
generateItemId,
|
|
31
|
+
removeItemsByIds,
|
|
32
|
+
repositionItemByAnchor,
|
|
33
|
+
} from '../item-id-utils';
|
|
28
34
|
|
|
29
35
|
const resolveItemColorsAsync = async (
|
|
30
36
|
item: TestimonialItem,
|
|
@@ -50,6 +56,7 @@ const applyTestimonialItemsAsync = async (
|
|
|
50
56
|
): Promise<void> => {
|
|
51
57
|
content.items = await Promise.all(
|
|
52
58
|
items.map(async (item) => ({
|
|
59
|
+
id: item.item_id || generateItemId(),
|
|
53
60
|
imageUrl: item.avatar_shape_url || item.avatar_image_url || '',
|
|
54
61
|
name: item.name || '',
|
|
55
62
|
position: item.position || '',
|
|
@@ -60,101 +67,77 @@ const applyTestimonialItemsAsync = async (
|
|
|
60
67
|
);
|
|
61
68
|
};
|
|
62
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Apply a batch of item operations by id (BP-5603), mirroring the section-level
|
|
72
|
+
* id-based contract one level deeper:
|
|
73
|
+
* - item with `item_id` matching an existing item → update in place (and
|
|
74
|
+
* reposition if `move_before_item_id` is set);
|
|
75
|
+
* - item without `item_id` → add a new item,
|
|
76
|
+
* positioned by `move_before_item_id`;
|
|
77
|
+
* - `_remove_item_ids` → delete by id.
|
|
78
|
+
*
|
|
79
|
+
* Existing items are id-backfilled first so legacy sections become referenceable
|
|
80
|
+
* the moment the metadata builder touches them. Order-independent: there are no
|
|
81
|
+
* positional indices to shift between operations.
|
|
82
|
+
*/
|
|
63
83
|
const mergeTestimonialItemsAsync = async (
|
|
64
84
|
content: TestimonialSectionContent,
|
|
65
85
|
items: TestimonialItem[],
|
|
66
|
-
|
|
86
|
+
removeIds: string[] | undefined,
|
|
67
87
|
): Promise<void> => {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (hasIndexedItems) {
|
|
71
|
-
await Promise.all(
|
|
72
|
-
items.map(async (item) => {
|
|
73
|
-
const targetIndex = item.item_index;
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
targetIndex === undefined ||
|
|
77
|
-
targetIndex < 0 ||
|
|
78
|
-
!content.items?.[targetIndex]
|
|
79
|
-
)
|
|
80
|
-
return;
|
|
81
|
-
|
|
82
|
-
const existing = content.items[targetIndex];
|
|
83
|
-
const prevImageUrl = existing.imageUrl;
|
|
84
|
-
|
|
85
|
-
if (item.avatar_shape_url !== undefined) {
|
|
86
|
-
existing.imageUrl = item.avatar_shape_url;
|
|
87
|
-
existing.mediaType = 'shape';
|
|
88
|
-
} else if (item.avatar_image_url !== undefined) {
|
|
89
|
-
existing.imageUrl = item.avatar_image_url;
|
|
90
|
-
existing.mediaType = 'image';
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (item.name !== undefined) existing.name = item.name;
|
|
94
|
-
|
|
95
|
-
if (item.position !== undefined) existing.position = item.position;
|
|
96
|
-
|
|
97
|
-
if (item.testimonial !== undefined)
|
|
98
|
-
existing.testimonials = item.testimonial;
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
item.avatar_shape_url !== undefined ||
|
|
102
|
-
item.color_theme !== undefined
|
|
103
|
-
) {
|
|
104
|
-
existing.colorReplacements = await resolveItemColorsAsync(
|
|
105
|
-
item,
|
|
106
|
-
existing.colorReplacements,
|
|
107
|
-
prevImageUrl,
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
}),
|
|
111
|
-
);
|
|
88
|
+
ensureItemIds(content.items);
|
|
89
|
+
content.items = removeItemsByIds(content.items, removeIds);
|
|
112
90
|
|
|
113
|
-
|
|
114
|
-
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
if (item.item_id) {
|
|
93
|
+
const existing = content.items.find((i) => i.id === item.item_id);
|
|
94
|
+
|
|
95
|
+
// Unknown id — skip rather than guess at a position.
|
|
96
|
+
if (!existing) continue;
|
|
115
97
|
|
|
116
|
-
|
|
98
|
+
const prevImageUrl = existing.imageUrl;
|
|
117
99
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
100
|
+
if (item.avatar_shape_url !== undefined) {
|
|
101
|
+
existing.imageUrl = item.avatar_shape_url;
|
|
102
|
+
existing.mediaType = 'shape';
|
|
103
|
+
} else if (item.avatar_image_url !== undefined) {
|
|
104
|
+
existing.imageUrl = item.avatar_image_url;
|
|
105
|
+
existing.mediaType = 'image';
|
|
106
|
+
}
|
|
121
107
|
|
|
122
|
-
|
|
123
|
-
item.avatar_shape_url ??
|
|
124
|
-
item.avatar_image_url ??
|
|
125
|
-
existing?.imageUrl ??
|
|
126
|
-
'';
|
|
127
|
-
const mediaType = item.avatar_shape_url
|
|
128
|
-
? 'shape'
|
|
129
|
-
: item.avatar_image_url
|
|
130
|
-
? 'image'
|
|
131
|
-
: (existing?.mediaType ?? 'image');
|
|
108
|
+
if (item.name !== undefined) existing.name = item.name;
|
|
132
109
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
110
|
+
if (item.position !== undefined) existing.position = item.position;
|
|
111
|
+
|
|
112
|
+
if (item.testimonial !== undefined)
|
|
113
|
+
existing.testimonials = item.testimonial;
|
|
114
|
+
|
|
115
|
+
if (
|
|
116
|
+
item.avatar_shape_url !== undefined ||
|
|
117
|
+
item.color_theme !== undefined
|
|
118
|
+
) {
|
|
119
|
+
existing.colorReplacements = await resolveItemColorsAsync(
|
|
140
120
|
item,
|
|
141
|
-
existing
|
|
142
|
-
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
};
|
|
146
|
-
}),
|
|
147
|
-
);
|
|
121
|
+
existing.colorReplacements,
|
|
122
|
+
prevImageUrl,
|
|
123
|
+
);
|
|
124
|
+
}
|
|
148
125
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
126
|
+
repositionItemByAnchor(content.items, existing, item.move_before_item_id);
|
|
127
|
+
} else {
|
|
128
|
+
const newItem = {
|
|
129
|
+
id: generateItemId(),
|
|
130
|
+
imageUrl: item.avatar_shape_url ?? item.avatar_image_url ?? '',
|
|
131
|
+
name: item.name ?? '',
|
|
132
|
+
position: item.position ?? '',
|
|
133
|
+
testimonials: item.testimonial ?? '',
|
|
134
|
+
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
135
|
+
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
136
|
+
} as TestimonialSectionContent['items'][number];
|
|
137
|
+
|
|
138
|
+
repositionItemByAnchor(content.items, newItem, item.move_before_item_id);
|
|
139
|
+
}
|
|
155
140
|
}
|
|
156
|
-
|
|
157
|
-
content.items = updatedItems;
|
|
158
141
|
};
|
|
159
142
|
|
|
160
143
|
export class TestimonialSectionBuilder extends BaseSectionBuilder<TestimonialSectionContent> {
|
|
@@ -228,75 +211,29 @@ export class TestimonialSectionBuilder extends BaseSectionBuilder<TestimonialSec
|
|
|
228
211
|
};
|
|
229
212
|
}
|
|
230
213
|
|
|
231
|
-
if (args.items && Array.isArray(args.items)) {
|
|
232
|
-
|
|
214
|
+
if (args._replace_items && args.items && Array.isArray(args.items)) {
|
|
215
|
+
// Full replacement: discard existing items and rebuild from the payload.
|
|
216
|
+
await applyTestimonialItemsAsync(content, args.items);
|
|
217
|
+
} else if (this.mode === 'create') {
|
|
218
|
+
if (args.items && Array.isArray(args.items)) {
|
|
233
219
|
await applyTestimonialItemsAsync(content, args.items);
|
|
234
|
-
} else {
|
|
235
|
-
await mergeTestimonialItemsAsync(
|
|
236
|
-
content,
|
|
237
|
-
args.items,
|
|
238
|
-
!!args._replace_items,
|
|
239
|
-
);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// Phase 2: Item operations (remove, insert, reorder)
|
|
244
|
-
if (args._remove_item_indices?.length && content.items) {
|
|
245
|
-
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
246
|
-
|
|
247
|
-
for (const idx of sorted) {
|
|
248
|
-
if (idx >= 0 && idx < content.items.length) {
|
|
249
|
-
content.items.splice(idx, 1);
|
|
250
|
-
}
|
|
251
220
|
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
221
|
+
} else if (
|
|
222
|
+
(args.items && Array.isArray(args.items)) ||
|
|
223
|
+
args._remove_item_ids?.length
|
|
224
|
+
) {
|
|
225
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
226
|
+
await mergeTestimonialItemsAsync(
|
|
227
|
+
content,
|
|
228
|
+
args.items ?? [],
|
|
229
|
+
args._remove_item_ids,
|
|
260
230
|
);
|
|
261
|
-
|
|
262
|
-
for (const item of sortedInserts) {
|
|
263
|
-
const pos = Math.max(
|
|
264
|
-
0,
|
|
265
|
-
Math.min(item.insert_at ?? 0, content.items?.length ?? 0),
|
|
266
|
-
);
|
|
267
|
-
const newItem = {
|
|
268
|
-
imageUrl: item.avatar_shape_url ?? item.avatar_image_url ?? '',
|
|
269
|
-
name: item.name ?? '',
|
|
270
|
-
position: item.position ?? '',
|
|
271
|
-
testimonials: item.testimonial ?? '',
|
|
272
|
-
colorReplacements: await resolveItemColorsAsync(item, [], undefined),
|
|
273
|
-
mediaType: item.avatar_shape_url ? 'shape' : 'image',
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
content.items = content.items ?? [];
|
|
277
|
-
content.items.splice(pos, 0, newItem as any);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
if (args._reorder_items?.length && content.items) {
|
|
282
|
-
const newOrder = args._reorder_items;
|
|
283
|
-
const unique = new Set(newOrder);
|
|
284
|
-
|
|
285
|
-
if (unique.size === newOrder.length) {
|
|
286
|
-
const reordered = newOrder
|
|
287
|
-
.filter((idx) => idx >= 0 && idx < content.items!.length)
|
|
288
|
-
.map((idx) => content.items![idx]);
|
|
289
|
-
|
|
290
|
-
if (reordered.length === content.items.length) {
|
|
291
|
-
content.items = reordered;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
231
|
}
|
|
295
232
|
|
|
296
233
|
// Post-merge cap check. itemCapCheck only fires for create / update with
|
|
297
|
-
// _replace_items, so
|
|
298
|
-
// the array. Surface as an error so the AI retry loop gets a signal
|
|
299
|
-
// can reissue with
|
|
234
|
+
// _replace_items, so adding new items to an already-full section silently
|
|
235
|
+
// bloats the array. Surface as an error so the AI retry loop gets a signal
|
|
236
|
+
// and can reissue with _remove_item_ids or fewer additions.
|
|
300
237
|
// brandSite key mirrors base-section-builder's hardcoded brandPageType.
|
|
301
238
|
const cap = TESTIMONIAL_ITEM_CAP.brandSite;
|
|
302
239
|
|
|
@@ -19,6 +19,12 @@ import {
|
|
|
19
19
|
} from '../section-field-utils';
|
|
20
20
|
import { BaseSectionBuilder } from './base-section-builder';
|
|
21
21
|
import { validateTextSection } from '../section-rules/text-section.rules';
|
|
22
|
+
import {
|
|
23
|
+
ensureItemIds,
|
|
24
|
+
generateItemId,
|
|
25
|
+
removeItemsByIds,
|
|
26
|
+
repositionItemByAnchor,
|
|
27
|
+
} from '../item-id-utils';
|
|
22
28
|
|
|
23
29
|
/** Resolve a RichTextField (string or TiptapDocument) to a plain HTML string. */
|
|
24
30
|
const resolveText = (value: unknown): { html: string; wasTiptap: boolean } => {
|
|
@@ -52,6 +58,7 @@ const createTextItem = (
|
|
|
52
58
|
const { html, wasTiptap } = resolveText(item.text);
|
|
53
59
|
|
|
54
60
|
return {
|
|
61
|
+
id: item.item_id || generateItemId(),
|
|
55
62
|
text: html,
|
|
56
63
|
textStyle: clearTextStyleTypeIfTiptap(
|
|
57
64
|
textStyle,
|
|
@@ -61,6 +68,10 @@ const createTextItem = (
|
|
|
61
68
|
};
|
|
62
69
|
};
|
|
63
70
|
|
|
71
|
+
/**
|
|
72
|
+
* Full replacement: discard existing items and rebuild from the payload,
|
|
73
|
+
* assigning a stable id to each new item.
|
|
74
|
+
*/
|
|
64
75
|
const applyTextItems = (
|
|
65
76
|
content: TextContent,
|
|
66
77
|
items: TextSectionItem[],
|
|
@@ -71,26 +82,33 @@ const applyTextItems = (
|
|
|
71
82
|
);
|
|
72
83
|
};
|
|
73
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Id-based merge (BP-5603): mirrors the section-level id contract one level
|
|
87
|
+
* deeper.
|
|
88
|
+
* - item with `item_id` matching an existing item → update in place and
|
|
89
|
+
* reposition if `move_before_item_id` is set; unknown id is skipped.
|
|
90
|
+
* - item without `item_id` → add a new item,
|
|
91
|
+
* positioned by `move_before_item_id`.
|
|
92
|
+
* - `removeIds` → delete by id.
|
|
93
|
+
*
|
|
94
|
+
* Existing items are id-backfilled first so legacy sections without ids
|
|
95
|
+
* become referenceable the moment this builder processes them.
|
|
96
|
+
*/
|
|
74
97
|
const mergeTextItems = (
|
|
75
98
|
content: TextContent,
|
|
76
99
|
items: TextSectionItem[],
|
|
77
|
-
|
|
100
|
+
removeIds: string[] | undefined,
|
|
78
101
|
textStyleOptions: TextStyleOptions | undefined,
|
|
79
102
|
): void => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
if (hasIndexedItems) {
|
|
83
|
-
for (const item of items) {
|
|
84
|
-
const targetIndex = item.item_index;
|
|
103
|
+
ensureItemIds(content.textItems);
|
|
104
|
+
content.textItems = removeItemsByIds(content.textItems, removeIds);
|
|
85
105
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
!content.textItems?.[targetIndex]
|
|
90
|
-
)
|
|
91
|
-
continue;
|
|
106
|
+
for (const item of items) {
|
|
107
|
+
if (item.item_id) {
|
|
108
|
+
const existing = content.textItems?.find((i) => i.id === item.item_id);
|
|
92
109
|
|
|
93
|
-
|
|
110
|
+
// Unknown id — skip rather than guess at a position.
|
|
111
|
+
if (!existing) continue;
|
|
94
112
|
|
|
95
113
|
if (item.text !== undefined) {
|
|
96
114
|
const { html, wasTiptap } = resolveText(item.text);
|
|
@@ -104,51 +122,32 @@ const mergeTextItems = (
|
|
|
104
122
|
}
|
|
105
123
|
|
|
106
124
|
if (item.text_style && typeof item.text_style === 'object') {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
) as TextStyleDeclaration;
|
|
112
|
-
|
|
113
|
-
existing.textStyle = mergedStyle;
|
|
125
|
+
existing.textStyle = {
|
|
126
|
+
...(existing.textStyle || {}),
|
|
127
|
+
...item.text_style,
|
|
128
|
+
} as TextStyleDeclaration;
|
|
114
129
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const itemsToMerge = items.filter((item) => item.insert_at === undefined);
|
|
121
|
-
|
|
122
|
-
const updatedItems = itemsToMerge.map((item, index) => {
|
|
123
|
-
const existing = content.textItems?.[index];
|
|
124
|
-
const baseStyle = existing?.textStyle || content.textStyle || {};
|
|
125
|
-
|
|
126
|
-
const mergedStyle = (
|
|
127
|
-
item.text_style ? { ...baseStyle, ...item.text_style } : baseStyle
|
|
128
|
-
) as TextStyleDeclaration;
|
|
129
130
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
repositionItemByAnchor(
|
|
132
|
+
content.textItems!,
|
|
133
|
+
existing,
|
|
134
|
+
item.move_before_item_id,
|
|
135
|
+
);
|
|
136
|
+
} else {
|
|
137
|
+
const newItem = createTextItem(
|
|
138
|
+
item,
|
|
139
|
+
content.textStyle || {},
|
|
138
140
|
textStyleOptions,
|
|
139
|
-
)
|
|
140
|
-
};
|
|
141
|
-
});
|
|
141
|
+
);
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
143
|
+
content.textItems = content.textItems ?? [];
|
|
144
|
+
repositionItemByAnchor(
|
|
145
|
+
content.textItems,
|
|
146
|
+
newItem,
|
|
147
|
+
item.move_before_item_id,
|
|
148
|
+
);
|
|
149
|
+
}
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
content.textItems = updatedItems;
|
|
152
151
|
};
|
|
153
152
|
|
|
154
153
|
/**
|
|
@@ -176,6 +175,7 @@ const applySingleTextField = (
|
|
|
176
175
|
} else {
|
|
177
176
|
content.textItems = [
|
|
178
177
|
{
|
|
178
|
+
id: generateItemId(),
|
|
179
179
|
text: html,
|
|
180
180
|
textStyle: clearTextStyleTypeIfTiptap(
|
|
181
181
|
{ ...(content.textStyle || {}) },
|
|
@@ -236,82 +236,31 @@ export class TextSectionBuilder extends BaseSectionBuilder<TextContent> {
|
|
|
236
236
|
|
|
237
237
|
const args = this.args as TextSectionArguments;
|
|
238
238
|
|
|
239
|
-
if (args.items && Array.isArray(args.items)) {
|
|
240
|
-
|
|
239
|
+
if (args._replace_items && args.items && Array.isArray(args.items)) {
|
|
240
|
+
// Full replacement: discard existing items and rebuild from the payload.
|
|
241
|
+
applyTextItems(content, args.items, textStyleOptions);
|
|
242
|
+
} else if (this.mode === 'create') {
|
|
243
|
+
if (args.items && Array.isArray(args.items)) {
|
|
241
244
|
applyTextItems(content, args.items, textStyleOptions);
|
|
242
245
|
} else {
|
|
243
|
-
|
|
244
|
-
content,
|
|
245
|
-
args.items,
|
|
246
|
-
!!args._replace_items,
|
|
247
|
-
textStyleOptions,
|
|
248
|
-
);
|
|
246
|
+
applySingleTextField(content, args, textStyleOptions);
|
|
249
247
|
}
|
|
248
|
+
} else if (
|
|
249
|
+
(args.items && Array.isArray(args.items)) ||
|
|
250
|
+
args._remove_item_ids?.length
|
|
251
|
+
) {
|
|
252
|
+
// Update: id-based add / edit / move / remove in a single pass.
|
|
253
|
+
mergeTextItems(
|
|
254
|
+
content,
|
|
255
|
+
args.items ?? [],
|
|
256
|
+
args._remove_item_ids,
|
|
257
|
+
textStyleOptions,
|
|
258
|
+
);
|
|
250
259
|
} else {
|
|
260
|
+
// No items array: apply the single text/text_style shorthand fields.
|
|
251
261
|
applySingleTextField(content, args, textStyleOptions);
|
|
252
262
|
}
|
|
253
263
|
|
|
254
|
-
// Phase 2: Item operations (remove, insert, reorder)
|
|
255
|
-
if (args._remove_item_indices?.length && content.textItems) {
|
|
256
|
-
const sorted = [...args._remove_item_indices].sort((a, b) => b - a);
|
|
257
|
-
|
|
258
|
-
for (const idx of sorted) {
|
|
259
|
-
if (idx >= 0 && idx < content.textItems.length) {
|
|
260
|
-
content.textItems.splice(idx, 1);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (args.items && Array.isArray(args.items)) {
|
|
266
|
-
const insertItems = args.items.filter(
|
|
267
|
-
(item) => item.insert_at !== undefined && item.item_index === undefined,
|
|
268
|
-
);
|
|
269
|
-
const sortedInserts = [...insertItems].sort(
|
|
270
|
-
(a, b) => (b.insert_at ?? 0) - (a.insert_at ?? 0),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
|
-
for (const item of sortedInserts) {
|
|
274
|
-
const pos = Math.max(
|
|
275
|
-
0,
|
|
276
|
-
Math.min(item.insert_at ?? 0, content.textItems?.length ?? 0),
|
|
277
|
-
);
|
|
278
|
-
const baseStyle = content.textStyle || {};
|
|
279
|
-
const textStyle = (
|
|
280
|
-
item.text_style
|
|
281
|
-
? { ...baseStyle, ...item.text_style }
|
|
282
|
-
: { ...baseStyle }
|
|
283
|
-
) as TextStyleDeclaration;
|
|
284
|
-
|
|
285
|
-
const { html, wasTiptap } = resolveText(item.text);
|
|
286
|
-
const newItem: TextItem = {
|
|
287
|
-
text: html,
|
|
288
|
-
textStyle: clearTextStyleTypeIfTiptap(
|
|
289
|
-
textStyle,
|
|
290
|
-
wasTiptap,
|
|
291
|
-
textStyleOptions,
|
|
292
|
-
),
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
content.textItems = content.textItems ?? [];
|
|
296
|
-
content.textItems.splice(pos, 0, newItem);
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (args._reorder_items?.length && content.textItems) {
|
|
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.textItems!.length)
|
|
307
|
-
.map((idx) => content.textItems![idx]);
|
|
308
|
-
|
|
309
|
-
if (reordered.length === content.textItems.length) {
|
|
310
|
-
content.textItems = reordered;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
264
|
if (args.background_color !== undefined) {
|
|
316
265
|
content.backgroundColor = {
|
|
317
266
|
...content.backgroundColor,
|
|
@@ -37,6 +37,13 @@ function defineFieldMap<C extends Content>(
|
|
|
37
37
|
return map;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
// BP-5687: vertical padding fields shared by all sections that render
|
|
41
|
+
// contentPaddingStyle (see src/brand-page/utils/content-padding.util.ts).
|
|
42
|
+
const PADDING_FIELDS = {
|
|
43
|
+
padding_top: 'topPadding',
|
|
44
|
+
padding_bottom: 'bottomPadding',
|
|
45
|
+
} as const;
|
|
46
|
+
|
|
40
47
|
const bannerFields = defineFieldMap<BannerSectionContent>({
|
|
41
48
|
title: 'title',
|
|
42
49
|
description: 'description',
|
|
@@ -45,6 +52,7 @@ const bannerFields = defineFieldMap<BannerSectionContent>({
|
|
|
45
52
|
background_image_url: 'imageUrl',
|
|
46
53
|
background_shape_url: 'imageUrl',
|
|
47
54
|
display_style: 'displayStyle',
|
|
55
|
+
...PADDING_FIELDS,
|
|
48
56
|
});
|
|
49
57
|
|
|
50
58
|
const bookingFields = defineFieldMap<BookingSectionContent>({
|
|
@@ -52,6 +60,7 @@ const bookingFields = defineFieldMap<BookingSectionContent>({
|
|
|
52
60
|
description: 'description',
|
|
53
61
|
button_text: 'buttonText',
|
|
54
62
|
button_url: 'url',
|
|
63
|
+
...PADDING_FIELDS,
|
|
55
64
|
});
|
|
56
65
|
|
|
57
66
|
const aboutFields = defineFieldMap<AboutSectionContent>({
|
|
@@ -60,12 +69,14 @@ const aboutFields = defineFieldMap<AboutSectionContent>({
|
|
|
60
69
|
about_image_url: 'imageUrl',
|
|
61
70
|
about_shape_url: 'imageUrl',
|
|
62
71
|
display_style: 'displayStyle',
|
|
72
|
+
...PADDING_FIELDS,
|
|
63
73
|
});
|
|
64
74
|
|
|
65
75
|
const testimonialFields = defineFieldMap<TestimonialSectionContent>({
|
|
66
76
|
title: 'title',
|
|
67
77
|
display_style: 'displayStyle',
|
|
68
78
|
// items handled separately
|
|
79
|
+
...PADDING_FIELDS,
|
|
69
80
|
});
|
|
70
81
|
|
|
71
82
|
const imagesSectionFields = defineFieldMap<ImagesSectionContent>({
|
|
@@ -75,6 +86,7 @@ const imagesSectionFields = defineFieldMap<ImagesSectionContent>({
|
|
|
75
86
|
aspect_ratio: 'aspectRatio',
|
|
76
87
|
are_images_full_width: 'areImagesFullWidth',
|
|
77
88
|
// items handled separately
|
|
89
|
+
...PADDING_FIELDS,
|
|
78
90
|
});
|
|
79
91
|
|
|
80
92
|
const shapesSectionFields = defineFieldMap<ShapesSectionContent>({
|
|
@@ -83,11 +95,13 @@ const shapesSectionFields = defineFieldMap<ShapesSectionContent>({
|
|
|
83
95
|
display_style: 'displayStyle',
|
|
84
96
|
size: 'size',
|
|
85
97
|
// items handled separately
|
|
98
|
+
...PADDING_FIELDS,
|
|
86
99
|
});
|
|
87
100
|
|
|
88
101
|
const textFields = defineFieldMap<TextContent>({
|
|
89
102
|
text: 'text',
|
|
90
103
|
column_style: 'columnStyle',
|
|
104
|
+
...PADDING_FIELDS,
|
|
91
105
|
});
|
|
92
106
|
|
|
93
107
|
const profileFields = defineFieldMap<ProfileContent>({
|
|
@@ -106,17 +120,20 @@ const contactFormFields = defineFieldMap<ContactFormContent>({
|
|
|
106
120
|
header_image_url: 'imageUrl',
|
|
107
121
|
display_style: 'displayStyle',
|
|
108
122
|
layout_style: 'layoutStyle',
|
|
123
|
+
...PADDING_FIELDS,
|
|
109
124
|
});
|
|
110
125
|
|
|
111
126
|
const mailingListFields = defineFieldMap<MailingListContent>({
|
|
112
127
|
button_text: 'buttonText',
|
|
113
128
|
accepted_fields: 'acceptedFields',
|
|
129
|
+
...PADDING_FIELDS,
|
|
114
130
|
});
|
|
115
131
|
|
|
116
132
|
const socialLinksFields = defineFieldMap<SocialLinksContent>({
|
|
117
133
|
title: 'title',
|
|
118
134
|
icon_style: 'iconDisplayStyle',
|
|
119
135
|
// links handled separately
|
|
136
|
+
...PADDING_FIELDS,
|
|
120
137
|
});
|
|
121
138
|
|
|
122
139
|
const dividerFields = defineFieldMap<DividerSectionContent>({
|
|
@@ -132,6 +149,7 @@ const imageFields = defineFieldMap<ImageContent>({
|
|
|
132
149
|
const imageGalleryFields = defineFieldMap<ImageGalleryContent>({
|
|
133
150
|
is_full_width: 'isFullWidth',
|
|
134
151
|
// images handled separately
|
|
152
|
+
...PADDING_FIELDS,
|
|
135
153
|
});
|
|
136
154
|
|
|
137
155
|
const buttonWithIconFields = defineFieldMap<ButtonContent>({
|