@capillarytech/creatives-library 9.0.50-alpha.1 → 9.0.50
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/package.json +1 -1
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +15 -106
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +1 -139
- package/v2Components/CommonTestAndPreview/constants.js +0 -2
- package/v2Components/CommonTestAndPreview/index.js +26 -240
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -49
- package/v2Components/CommonTestAndPreview/utils.js +0 -20
- package/v2Containers/CreativesContainer/index.js +4 -9
- package/v2Containers/CreativesContainer/tests/index.test.js +4 -3
- package/v2Containers/Rcs/carouselUtils.js +43 -13
- package/v2Containers/Rcs/index.js +32 -8
- package/v2Containers/Rcs/tests/carouselUtils.test.js +23 -4
- package/v2Containers/Rcs/tests/index.test.js +265 -3
- package/v2Containers/Rcs/tests/utils.test.js +59 -74
- package/v2Containers/Rcs/utils.js +9 -34
- package/v2Containers/Templates/_templates.scss +1 -179
- package/v2Containers/Templates/index.js +10 -97
- package/v2Containers/Viber/constants.js +0 -21
- package/v2Containers/Viber/index.js +43 -711
- package/v2Containers/Viber/index.scss +0 -175
- package/v2Containers/Viber/messages.js +0 -121
- package/v2Containers/Viber/tests/index.test.js +0 -80
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +0 -127
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +0 -133
|
@@ -200,370 +200,6 @@ describe('ViberPreviewContent', () => {
|
|
|
200
200
|
|
|
201
201
|
expect(screen.getByText('Click Here')).toBeTruthy();
|
|
202
202
|
});
|
|
203
|
-
|
|
204
|
-
it('should not render button when buttonText has only whitespace', () => {
|
|
205
|
-
const props = {
|
|
206
|
-
...defaultProps,
|
|
207
|
-
content: {
|
|
208
|
-
viberPreviewContent: {
|
|
209
|
-
messageContent: 'Message',
|
|
210
|
-
buttonText: ' ',
|
|
211
|
-
},
|
|
212
|
-
},
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
const { container } = render(
|
|
216
|
-
<TestWrapper>
|
|
217
|
-
<ComponentToRender {...props} />
|
|
218
|
-
</TestWrapper>
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
expect(container.querySelector('.viber-button-base')).toBeFalsy();
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
describe('Carousel Content', () => {
|
|
226
|
-
it('should render no content when carousel is selected but cards are empty', () => {
|
|
227
|
-
const props = {
|
|
228
|
-
...defaultProps,
|
|
229
|
-
content: {
|
|
230
|
-
viberPreviewContent: {
|
|
231
|
-
type: 'CAROUSEL',
|
|
232
|
-
cards: [
|
|
233
|
-
{
|
|
234
|
-
text: '',
|
|
235
|
-
mediaUrl: '',
|
|
236
|
-
buttons: [
|
|
237
|
-
{ title: '', action: '' },
|
|
238
|
-
],
|
|
239
|
-
},
|
|
240
|
-
{
|
|
241
|
-
text: ' ',
|
|
242
|
-
mediaUrl: ' ',
|
|
243
|
-
buttons: [
|
|
244
|
-
{ title: ' ', action: 'https://example.com/2' },
|
|
245
|
-
],
|
|
246
|
-
},
|
|
247
|
-
],
|
|
248
|
-
},
|
|
249
|
-
},
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
render(
|
|
253
|
-
<TestWrapper>
|
|
254
|
-
<ComponentToRender {...props} />
|
|
255
|
-
</TestWrapper>
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
expect(screen.getByText('No content available')).toBeTruthy();
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('should render carousel cards when type is CAROUSEL', () => {
|
|
262
|
-
const props = {
|
|
263
|
-
...defaultProps,
|
|
264
|
-
content: {
|
|
265
|
-
viberPreviewContent: {
|
|
266
|
-
type: 'CAROUSEL',
|
|
267
|
-
cards: [
|
|
268
|
-
{
|
|
269
|
-
text: 'Card 1 text',
|
|
270
|
-
mediaUrl: 'https://image.url/card1.jpg',
|
|
271
|
-
buttons: [
|
|
272
|
-
{ title: 'Button 1', action: 'https://example.com/1' },
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
{
|
|
276
|
-
text: 'Card 2 text',
|
|
277
|
-
mediaUrl: 'https://image.url/card2.jpg',
|
|
278
|
-
buttons: [
|
|
279
|
-
{ title: 'Button 2', action: 'https://example.com/2' },
|
|
280
|
-
],
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
},
|
|
284
|
-
},
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
render(
|
|
288
|
-
<TestWrapper>
|
|
289
|
-
<ComponentToRender {...props} />
|
|
290
|
-
</TestWrapper>
|
|
291
|
-
);
|
|
292
|
-
|
|
293
|
-
expect(screen.getByText('Card 1 text')).toBeTruthy();
|
|
294
|
-
expect(screen.getByText('Card 2 text')).toBeTruthy();
|
|
295
|
-
expect(screen.getByText('Button 1')).toBeTruthy();
|
|
296
|
-
expect(screen.getByText('Button 2')).toBeTruthy();
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it('should not render empty carousel button placeholder', () => {
|
|
300
|
-
const props = {
|
|
301
|
-
...defaultProps,
|
|
302
|
-
content: {
|
|
303
|
-
viberPreviewContent: {
|
|
304
|
-
type: 'CAROUSEL',
|
|
305
|
-
cards: [
|
|
306
|
-
{
|
|
307
|
-
text: 'Card 1 text',
|
|
308
|
-
mediaUrl: 'https://image.url/card1.jpg',
|
|
309
|
-
buttons: [
|
|
310
|
-
{ title: '', action: 'https://example.com/1' },
|
|
311
|
-
{ title: ' ', action: 'https://example.com/2' },
|
|
312
|
-
],
|
|
313
|
-
},
|
|
314
|
-
],
|
|
315
|
-
},
|
|
316
|
-
},
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
const { container } = render(
|
|
320
|
-
<TestWrapper>
|
|
321
|
-
<ComponentToRender {...props} />
|
|
322
|
-
</TestWrapper>
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
expect(container.querySelector('.viber-carousel-preview .button')).toBeNull();
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
it('should show carousel shell when showCarouselEditorPreview is true even if cards are empty', () => {
|
|
329
|
-
const props = {
|
|
330
|
-
...defaultProps,
|
|
331
|
-
content: {
|
|
332
|
-
viberPreviewContent: {
|
|
333
|
-
type: 'CAROUSEL',
|
|
334
|
-
showCarouselEditorPreview: true,
|
|
335
|
-
cards: [
|
|
336
|
-
{ text: '', mediaUrl: '', buttons: [{ title: '', action: '' }] },
|
|
337
|
-
{ text: '', mediaUrl: '', buttons: [{ title: '', action: '' }] },
|
|
338
|
-
],
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
};
|
|
342
|
-
|
|
343
|
-
const { container } = render(
|
|
344
|
-
<TestWrapper>
|
|
345
|
-
<ComponentToRender {...props} />
|
|
346
|
-
</TestWrapper>
|
|
347
|
-
);
|
|
348
|
-
|
|
349
|
-
expect(screen.queryByText('No content available')).toBeNull();
|
|
350
|
-
expect(container.querySelector('.viber-carousel-preview .scroll')).toBeTruthy();
|
|
351
|
-
expect(container.querySelector('.viber-carousel-message-box-placeholder')).toBeTruthy();
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('should render carousel message text in message box when type is CAROUSEL', () => {
|
|
355
|
-
const props = {
|
|
356
|
-
...defaultProps,
|
|
357
|
-
content: {
|
|
358
|
-
viberPreviewContent: {
|
|
359
|
-
type: 'CAROUSEL',
|
|
360
|
-
messageContent: 'Carousel intro copy',
|
|
361
|
-
cards: [
|
|
362
|
-
{
|
|
363
|
-
text: 'Card text',
|
|
364
|
-
mediaUrl: 'https://image.url/c.jpg',
|
|
365
|
-
buttons: [{ title: 'Go', action: 'https://example.com' }],
|
|
366
|
-
},
|
|
367
|
-
],
|
|
368
|
-
},
|
|
369
|
-
},
|
|
370
|
-
};
|
|
371
|
-
|
|
372
|
-
const { container } = render(
|
|
373
|
-
<TestWrapper>
|
|
374
|
-
<ComponentToRender {...props} />
|
|
375
|
-
</TestWrapper>
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
expect(container.querySelector('.viber-carousel-message-box-text')).toHaveTextContent('Carousel intro copy');
|
|
379
|
-
expect(screen.queryByText('Carousel intro copy')).toBeTruthy();
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('should hide account icon when carousel is shown', () => {
|
|
383
|
-
const props = {
|
|
384
|
-
...defaultProps,
|
|
385
|
-
content: {
|
|
386
|
-
viberPreviewContent: {
|
|
387
|
-
type: 'CAROUSEL',
|
|
388
|
-
cards: [
|
|
389
|
-
{
|
|
390
|
-
text: 'Carousel card line',
|
|
391
|
-
mediaUrl: '',
|
|
392
|
-
buttons: [{ title: 'Open link', action: 'https://x.com' }],
|
|
393
|
-
},
|
|
394
|
-
],
|
|
395
|
-
},
|
|
396
|
-
},
|
|
397
|
-
};
|
|
398
|
-
|
|
399
|
-
const { container } = render(
|
|
400
|
-
<TestWrapper>
|
|
401
|
-
<ComponentToRender {...props} />
|
|
402
|
-
</TestWrapper>
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
expect(container.querySelector('.viber-account-icon')).toBeNull();
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
it('should use image placeholder when carousel card mediaUrl is whitespace only', () => {
|
|
409
|
-
const props = {
|
|
410
|
-
...defaultProps,
|
|
411
|
-
content: {
|
|
412
|
-
viberPreviewContent: {
|
|
413
|
-
type: 'CAROUSEL',
|
|
414
|
-
cards: [
|
|
415
|
-
{
|
|
416
|
-
text: 'Only text',
|
|
417
|
-
mediaUrl: ' ',
|
|
418
|
-
buttons: [{ title: 'Btn', action: 'https://example.com' }],
|
|
419
|
-
},
|
|
420
|
-
],
|
|
421
|
-
},
|
|
422
|
-
},
|
|
423
|
-
};
|
|
424
|
-
|
|
425
|
-
const { container } = render(
|
|
426
|
-
<TestWrapper>
|
|
427
|
-
<ComponentToRender {...props} />
|
|
428
|
-
</TestWrapper>
|
|
429
|
-
);
|
|
430
|
-
|
|
431
|
-
expect(container.querySelector('.viber-carousel-preview .image-placeholder')).toBeTruthy();
|
|
432
|
-
expect(container.querySelector('.viber-carousel-preview .image')).toBeNull();
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('should render at most two carousel buttons per card', () => {
|
|
436
|
-
const props = {
|
|
437
|
-
...defaultProps,
|
|
438
|
-
content: {
|
|
439
|
-
viberPreviewContent: {
|
|
440
|
-
type: 'CAROUSEL',
|
|
441
|
-
cards: [
|
|
442
|
-
{
|
|
443
|
-
text: 'Card',
|
|
444
|
-
mediaUrl: 'https://image.url/c.jpg',
|
|
445
|
-
buttons: [
|
|
446
|
-
{ title: 'One', action: 'https://a.com' },
|
|
447
|
-
{ title: 'Two', action: 'https://b.com' },
|
|
448
|
-
{ title: 'Three', action: 'https://c.com' },
|
|
449
|
-
],
|
|
450
|
-
},
|
|
451
|
-
],
|
|
452
|
-
},
|
|
453
|
-
},
|
|
454
|
-
};
|
|
455
|
-
|
|
456
|
-
const { container } = render(
|
|
457
|
-
<TestWrapper>
|
|
458
|
-
<ComponentToRender {...props} />
|
|
459
|
-
</TestWrapper>
|
|
460
|
-
);
|
|
461
|
-
|
|
462
|
-
expect(container.querySelectorAll('.viber-carousel-preview .button')).toHaveLength(2);
|
|
463
|
-
expect(screen.getByText('One')).toBeTruthy();
|
|
464
|
-
expect(screen.getByText('Two')).toBeTruthy();
|
|
465
|
-
expect(screen.queryByText('Three')).toBeNull();
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('should apply secondary class to second carousel button', () => {
|
|
469
|
-
const props = {
|
|
470
|
-
...defaultProps,
|
|
471
|
-
content: {
|
|
472
|
-
viberPreviewContent: {
|
|
473
|
-
type: 'CAROUSEL',
|
|
474
|
-
cards: [
|
|
475
|
-
{
|
|
476
|
-
text: 'Card',
|
|
477
|
-
mediaUrl: 'https://image.url/c.jpg',
|
|
478
|
-
buttons: [
|
|
479
|
-
{ title: 'Primary', action: 'https://a.com' },
|
|
480
|
-
{ title: 'Secondary', action: 'https://b.com' },
|
|
481
|
-
],
|
|
482
|
-
},
|
|
483
|
-
],
|
|
484
|
-
},
|
|
485
|
-
},
|
|
486
|
-
};
|
|
487
|
-
|
|
488
|
-
const { container } = render(
|
|
489
|
-
<TestWrapper>
|
|
490
|
-
<ComponentToRender {...props} />
|
|
491
|
-
</TestWrapper>
|
|
492
|
-
);
|
|
493
|
-
|
|
494
|
-
const buttons = container.querySelectorAll('.viber-carousel-preview .button');
|
|
495
|
-
expect(buttons[0].className).toContain('button');
|
|
496
|
-
expect(buttons[0].className).not.toContain('button-secondary');
|
|
497
|
-
expect(buttons[1].className).toContain('button-secondary');
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
it('should show carousel when only a button title is present on a card', () => {
|
|
501
|
-
const props = {
|
|
502
|
-
...defaultProps,
|
|
503
|
-
content: {
|
|
504
|
-
viberPreviewContent: {
|
|
505
|
-
type: 'CAROUSEL',
|
|
506
|
-
cards: [
|
|
507
|
-
{
|
|
508
|
-
text: '',
|
|
509
|
-
mediaUrl: '',
|
|
510
|
-
buttons: [{ title: 'Tap me', action: 'https://example.com' }],
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
},
|
|
514
|
-
},
|
|
515
|
-
};
|
|
516
|
-
|
|
517
|
-
render(
|
|
518
|
-
<TestWrapper>
|
|
519
|
-
<ComponentToRender {...props} />
|
|
520
|
-
</TestWrapper>
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
expect(screen.getByText('Tap me')).toBeTruthy();
|
|
524
|
-
expect(screen.queryByText('No content available')).toBeNull();
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
it('should show one placeholder card when editor preview and cards array is empty', () => {
|
|
528
|
-
const props = {
|
|
529
|
-
...defaultProps,
|
|
530
|
-
content: {
|
|
531
|
-
viberPreviewContent: {
|
|
532
|
-
type: 'CAROUSEL',
|
|
533
|
-
showCarouselEditorPreview: true,
|
|
534
|
-
cards: [],
|
|
535
|
-
},
|
|
536
|
-
},
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const { container } = render(
|
|
540
|
-
<TestWrapper>
|
|
541
|
-
<ComponentToRender {...props} />
|
|
542
|
-
</TestWrapper>
|
|
543
|
-
);
|
|
544
|
-
|
|
545
|
-
expect(container.querySelectorAll('.viber-carousel-preview .card')).toHaveLength(1);
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
it('should show no content when CAROUSEL has empty cards and no editor preview flag', () => {
|
|
549
|
-
const props = {
|
|
550
|
-
...defaultProps,
|
|
551
|
-
content: {
|
|
552
|
-
viberPreviewContent: {
|
|
553
|
-
type: 'CAROUSEL',
|
|
554
|
-
cards: [],
|
|
555
|
-
},
|
|
556
|
-
},
|
|
557
|
-
};
|
|
558
|
-
|
|
559
|
-
render(
|
|
560
|
-
<TestWrapper>
|
|
561
|
-
<ComponentToRender {...props} />
|
|
562
|
-
</TestWrapper>
|
|
563
|
-
);
|
|
564
|
-
|
|
565
|
-
expect(screen.getByText('No content available')).toBeTruthy();
|
|
566
|
-
});
|
|
567
203
|
});
|
|
568
204
|
|
|
569
205
|
describe('Account and Brand Name', () => {
|
|
@@ -6,10 +6,6 @@ import {
|
|
|
6
6
|
mapRcsSuggestionForTestMeta,
|
|
7
7
|
typeMatches,
|
|
8
8
|
unwrapEntity,
|
|
9
|
-
getTrimmedText,
|
|
10
|
-
hasTrimmedText,
|
|
11
|
-
getCarouselButtonsWithTitles,
|
|
12
|
-
getCarouselButtonSlotCount,
|
|
13
9
|
} from '../utils';
|
|
14
10
|
|
|
15
11
|
describe('CommonTestAndPreview/utils', () => {
|
|
@@ -152,49 +148,4 @@ describe('CommonTestAndPreview/utils', () => {
|
|
|
152
148
|
expect(unwrapEntity(response)).toBe(response);
|
|
153
149
|
});
|
|
154
150
|
});
|
|
155
|
-
|
|
156
|
-
describe('getTrimmedText / hasTrimmedText', () => {
|
|
157
|
-
it('trims string values', () => {
|
|
158
|
-
expect(getTrimmedText(' hello ')).toBe('hello');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('treats null/undefined as empty', () => {
|
|
162
|
-
expect(getTrimmedText(null)).toBe('');
|
|
163
|
-
expect(getTrimmedText(undefined)).toBe('');
|
|
164
|
-
expect(hasTrimmedText(' ')).toBe(false);
|
|
165
|
-
expect(hasTrimmedText('ok')).toBe(true);
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
describe('getCarouselButtonsWithTitles', () => {
|
|
170
|
-
it('returns up to 2 buttons with non-empty titles', () => {
|
|
171
|
-
expect(getCarouselButtonsWithTitles({
|
|
172
|
-
buttons: [
|
|
173
|
-
{ title: ' A ' },
|
|
174
|
-
{ title: '' },
|
|
175
|
-
{ title: 'B' },
|
|
176
|
-
{ title: 'C' },
|
|
177
|
-
],
|
|
178
|
-
})).toEqual([{ title: ' A ' }, { title: 'B' }]);
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('returns empty array when card has no titled buttons', () => {
|
|
182
|
-
expect(getCarouselButtonsWithTitles({})).toEqual([]);
|
|
183
|
-
expect(getCarouselButtonsWithTitles(undefined)).toEqual([]);
|
|
184
|
-
});
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
describe('getCarouselButtonSlotCount', () => {
|
|
188
|
-
it('returns the max titled-button count across cards, capped at 2', () => {
|
|
189
|
-
expect(getCarouselButtonSlotCount([
|
|
190
|
-
{ buttons: [{ title: 'A' }] },
|
|
191
|
-
{ buttons: [{ title: 'A' }, { title: 'B' }, { title: 'C' }] },
|
|
192
|
-
])).toBe(2);
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('returns 0 when no cards have titled buttons', () => {
|
|
196
|
-
expect(getCarouselButtonSlotCount([])).toBe(0);
|
|
197
|
-
expect(getCarouselButtonSlotCount([{ buttons: [{ title: ' ' }] }])).toBe(0);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
151
|
});
|
|
@@ -116,23 +116,3 @@ export const unwrapEntity = (response) => {
|
|
|
116
116
|
if (response.entity === null) return null;
|
|
117
117
|
return response;
|
|
118
118
|
};
|
|
119
|
-
|
|
120
|
-
/** Trim string-like values; null/undefined become ''. */
|
|
121
|
-
export const getTrimmedText = (value = '') => (value ?? '').trim();
|
|
122
|
-
|
|
123
|
-
/** True when trimmed text is non-empty. */
|
|
124
|
-
export const hasTrimmedText = (value = '') => Boolean(getTrimmedText(value));
|
|
125
|
-
|
|
126
|
-
/** First up to 2 carousel buttons that have a non-empty title. */
|
|
127
|
-
export const getCarouselButtonsWithTitles = (card) => (
|
|
128
|
-
(card?.buttons ?? []).filter((button) => hasTrimmedText(button?.title)).slice(0, 2)
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Max button slots to reserve across carousel cards (0–2), so cards align
|
|
133
|
-
* even when some cards have fewer titled buttons.
|
|
134
|
-
*/
|
|
135
|
-
export const getCarouselButtonSlotCount = (cards = []) => {
|
|
136
|
-
const buttonCounts = cards.map((card) => getCarouselButtonsWithTitles(card).length);
|
|
137
|
-
return Math.min(2, Math.max(0, ...buttonCounts, 0));
|
|
138
|
-
};
|
|
@@ -1450,15 +1450,10 @@ export class Creatives extends React.Component {
|
|
|
1450
1450
|
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1451
1451
|
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1452
1452
|
: null;
|
|
1453
|
-
// Campaigns/consumers
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
|
|
1457
|
-
const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
|
|
1458
|
-
cardContentFromSubmit,
|
|
1459
|
-
cardVarMappedFromFirstRcsCard,
|
|
1460
|
-
isFullModeForRcsConsumerPayload,
|
|
1461
|
-
);
|
|
1453
|
+
// Campaigns/consumers get the same numeric-slot contract as the editor's own round-trip
|
|
1454
|
+
// (see createPayload): title/description keep {{1}}, {{2}}, … and every slot's value —
|
|
1455
|
+
// tag reference or literal — lives only in cardVarMapped for the backend to resolve.
|
|
1456
|
+
const cardContent = mapRcsCardContentForConsumerWithResolvedTags(cardContentFromSubmit);
|
|
1462
1457
|
const rcsContent = {
|
|
1463
1458
|
contentType,
|
|
1464
1459
|
cardType,
|
|
@@ -267,7 +267,7 @@ describe('Test SlideBoxContent container', () => {
|
|
|
267
267
|
expect(instance.state.templateData.rcsCardVarMapped).toEqual(cardVarMapped);
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
-
it('RCS getCreativesData sends the
|
|
270
|
+
it('RCS getCreativesData sends the numeric slot (not the literal tag) in title/description to consumers, with the tag only in cardVarMapped', async () => {
|
|
271
271
|
renderedComponent = shallowWithIntl(
|
|
272
272
|
<Creatives
|
|
273
273
|
loyaltyMetaData={loyaltyMetaData}
|
|
@@ -316,8 +316,9 @@ describe('Test SlideBoxContent container', () => {
|
|
|
316
316
|
await tick();
|
|
317
317
|
|
|
318
318
|
const sentCardContent = getCreativesData.mock.calls[0][0].rcsContent.cardContent[0];
|
|
319
|
-
expect(sentCardContent.title).toBe('Hi {{
|
|
320
|
-
expect(sentCardContent.description).toBe('Visit {{
|
|
319
|
+
expect(sentCardContent.title).toBe('Hi {{1}}');
|
|
320
|
+
expect(sentCardContent.description).toBe('Visit {{2}}');
|
|
321
|
+
expect(sentCardContent.cardVarMapped).toEqual({ 1: '{{first_name}}', 2: '{{loyalty_points}}' });
|
|
321
322
|
});
|
|
322
323
|
|
|
323
324
|
it('Text getCreatives data for rcs, data from creatives done to campaigns', async () => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
TEMPLATE_TITLE_MAX_LENGTH,
|
|
12
12
|
RCS_RICH_CARD_MAX_LENGTH,
|
|
13
13
|
RCS_NUMERIC_VAR_TOKEN_REGEX,
|
|
14
|
+
RCS_NUMERIC_VAR_NAME_REGEX,
|
|
14
15
|
RCS_CAROUSEL_ASSET_INDEX_BASE,
|
|
15
16
|
RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
|
|
16
17
|
MEDIUM,
|
|
@@ -190,22 +191,51 @@ export const buildCarouselCardContentForPayload = (carouselData, {
|
|
|
190
191
|
const thumbnailUrl = isCardVideo
|
|
191
192
|
? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
|
|
192
193
|
: '';
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
...((card.description || '').match(rcsVarRegex) ?? []),
|
|
196
|
-
];
|
|
194
|
+
let cardTitle = card.title || '';
|
|
195
|
+
let cardDescription = card.description || '';
|
|
197
196
|
const cardVarMappedForCard = {};
|
|
198
|
-
if (isSlotMappingMode
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
197
|
+
if (isSlotMappingMode) {
|
|
198
|
+
const titleTokens = cardTitle.match(rcsVarRegex) ?? [];
|
|
199
|
+
const descTokens = cardDescription.match(rcsVarRegex) ?? [];
|
|
200
|
+
if (titleTokens.length > 0 || descTokens.length > 0) {
|
|
201
|
+
// The persisted API contract requires every {{...}} in title/description to be a plain
|
|
202
|
+
// numeric slot ({{1}}, {{2}}, …) — the actual tag/value belongs only in cardVarMapped. A
|
|
203
|
+
// slot can still hold a non-numeric/legacy token here (hand-typed, or picked before this
|
|
204
|
+
// card had a numeric slot for it) since the carousel tag-picker never rewrites card text.
|
|
205
|
+
// Rename only those to the next free number for this card, leaving already-numeric slots
|
|
206
|
+
// (and the card's normal case) untouched.
|
|
207
|
+
const usedNumbers = new Set(
|
|
208
|
+
[...titleTokens, ...descTokens]
|
|
209
|
+
.map(getVarNameFromToken)
|
|
210
|
+
.filter((name) => RCS_NUMERIC_VAR_NAME_REGEX.test(name))
|
|
211
|
+
.map((name) => parseInt(name, 10)),
|
|
212
|
+
);
|
|
213
|
+
let nextFreeNumber = 1;
|
|
214
|
+
const claimNextFreeNumber = () => {
|
|
215
|
+
while (usedNumbers.has(nextFreeNumber)) nextFreeNumber++;
|
|
216
|
+
usedNumbers.add(nextFreeNumber);
|
|
217
|
+
return String(nextFreeNumber);
|
|
218
|
+
};
|
|
219
|
+
const renameFieldVarTokens = (fieldTokens, str) => {
|
|
220
|
+
let ordinal = 0;
|
|
221
|
+
return str.replace(rcsVarRegex, (matchedToken) => {
|
|
222
|
+
const varName = getVarNameFromToken(fieldTokens[ordinal]);
|
|
223
|
+
ordinal += 1;
|
|
224
|
+
if (!varName) return matchedToken;
|
|
225
|
+
const isNumericSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(varName);
|
|
226
|
+
const outputVarName = isNumericSlot ? varName : claimNextFreeNumber();
|
|
227
|
+
const scopedValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)] ?? '';
|
|
228
|
+
cardVarMappedForCard[outputVarName] = sanitizeCardVarMappedValue(scopedValue);
|
|
229
|
+
return `{{${outputVarName}}}`;
|
|
230
|
+
});
|
|
231
|
+
};
|
|
232
|
+
cardTitle = renameFieldVarTokens(titleTokens, cardTitle);
|
|
233
|
+
cardDescription = renameFieldVarTokens(descTokens, cardDescription);
|
|
234
|
+
}
|
|
205
235
|
}
|
|
206
236
|
return {
|
|
207
|
-
title:
|
|
208
|
-
description:
|
|
237
|
+
title: cardTitle,
|
|
238
|
+
description: cardDescription,
|
|
209
239
|
mediaType: cardMediaType,
|
|
210
240
|
...((isCardImage || isCardVideo) && {
|
|
211
241
|
media: {
|
|
@@ -1571,17 +1571,21 @@ export const Rcs = (props) => {
|
|
|
1571
1571
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
|
|
1572
1572
|
// Legacy slot that already holds a named tag (e.g. {{first_name}}) is replaced by
|
|
1573
1573
|
// field-local position, since a name-based replace would also overwrite other slots
|
|
1574
|
-
// sharing that same tag name.
|
|
1574
|
+
// sharing that same tag name. The persisted API contract requires title/description to
|
|
1575
|
+
// only ever contain {{1}}, {{2}}, … — the actual tag lives in cardVarMapped (written
|
|
1576
|
+
// above) — so normalize the slot text to its numeric token instead of re-embedding the
|
|
1577
|
+
// tag name, self-healing any legacy/hand-typed token the moment it's touched here.
|
|
1575
1578
|
const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
|
|
1576
1579
|
? 0
|
|
1577
1580
|
: (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1578
1581
|
const localSlotOrdinal = (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined)
|
|
1579
1582
|
? globalVarSlotIndexZeroBased - fieldOffsetForSlot
|
|
1580
1583
|
: null;
|
|
1584
|
+
const replacementToken = cardVarMappedNumericSlotKey || selectedTagNameFromPicker;
|
|
1581
1585
|
|
|
1582
1586
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1583
1587
|
setTemplateTitle((previousTitle) => {
|
|
1584
|
-
const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal,
|
|
1588
|
+
const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, replacementToken);
|
|
1585
1589
|
if (titleAfterReplacingTag === previousTitle) return previousTitle;
|
|
1586
1590
|
setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
|
|
1587
1591
|
// Remount segment editor: tag insert replaces {{n}} with e.g. {{tag.FORMAT_1}} — slot ids change; avoids stale UI vs manual typing in full-mode TextArea
|
|
@@ -1590,7 +1594,7 @@ export const Rcs = (props) => {
|
|
|
1590
1594
|
});
|
|
1591
1595
|
} else {
|
|
1592
1596
|
setTemplateDesc((previousDescription) => {
|
|
1593
|
-
const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal,
|
|
1597
|
+
const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, replacementToken);
|
|
1594
1598
|
if (descriptionAfterReplacingTag === previousDescription) {
|
|
1595
1599
|
return previousDescription;
|
|
1596
1600
|
}
|
|
@@ -2776,10 +2780,9 @@ const onTitleAddVar = () => {
|
|
|
2776
2780
|
: RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.heightType || MEDIUM,
|
|
2777
2781
|
}}),
|
|
2778
2782
|
...(isSlotMappingMode && (() => {
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
];
|
|
2783
|
+
const titleVarTokens = templateTitle?.match(rcsVarRegex) ?? [];
|
|
2784
|
+
const descVarTokens = templateDesc?.match(rcsVarRegex) ?? [];
|
|
2785
|
+
const templateVarTokens = [...titleVarTokens, ...descVarTokens];
|
|
2783
2786
|
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
2784
2787
|
cardVarMapped,
|
|
2785
2788
|
);
|
|
@@ -2800,7 +2803,28 @@ const onTitleAddVar = () => {
|
|
|
2800
2803
|
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
2801
2804
|
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2802
2805
|
});
|
|
2803
|
-
|
|
2806
|
+
// The API contract requires title/description to only ever contain {{1}}, {{2}}, …
|
|
2807
|
+
// in this exact slot order — never a semantic/legacy name — regardless of whether
|
|
2808
|
+
// the user ever touched TagList for that slot (e.g. content hydrated/pasted with a
|
|
2809
|
+
// named token). persistedSlotVarMap above already keys values by this same slot
|
|
2810
|
+
// position, so rewrite the text to match instead of persisting it verbatim.
|
|
2811
|
+
let titleOrdinal = 0;
|
|
2812
|
+
const renumberedTitle = (templateTitle || '').replace(rcsVarRegex, () => {
|
|
2813
|
+
const slotIndexZeroBased = titleOrdinal;
|
|
2814
|
+
titleOrdinal += 1;
|
|
2815
|
+
return `{{${slotIndexZeroBased + 1}}}`;
|
|
2816
|
+
});
|
|
2817
|
+
let descOrdinal = 0;
|
|
2818
|
+
const renumberedDesc = (templateDesc || '').replace(rcsVarRegex, () => {
|
|
2819
|
+
const slotIndexZeroBased = titleVarTokens.length + descOrdinal;
|
|
2820
|
+
descOrdinal += 1;
|
|
2821
|
+
return `{{${slotIndexZeroBased + 1}}}`;
|
|
2822
|
+
});
|
|
2823
|
+
return {
|
|
2824
|
+
title: renumberedTitle,
|
|
2825
|
+
description: renumberedDesc,
|
|
2826
|
+
cardVarMapped: persistedSlotVarMap,
|
|
2827
|
+
};
|
|
2804
2828
|
})()),
|
|
2805
2829
|
...(suggestions.length > 0 && { suggestions }),
|
|
2806
2830
|
}
|