@capillarytech/creatives-library 9.0.34 → 9.0.35-alpha.1
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/index.js +12 -6
- package/v2Components/CommonTestAndPreview/tests/index.test.js +78 -0
- package/v2Components/CommonTestAndPreview/utils.js +34 -0
- package/v2Components/SmsFallback/index.js +6 -0
- package/v2Containers/CreativesContainer/index.js +15 -6
- package/v2Containers/CreativesContainer/tests/index.test.js +53 -0
- package/v2Containers/Rcs/carouselUtils.js +16 -17
- package/v2Containers/Rcs/components/CarouselCard.js +2 -2
- package/v2Containers/Rcs/constants.js +1 -1
- package/v2Containers/Rcs/index.js +200 -101
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +47 -6
- package/v2Containers/Rcs/tests/carouselUtils.test.js +22 -26
- package/v2Containers/Rcs/tests/index.test.js +58 -20
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +71 -0
- package/v2Containers/Rcs/tests/utils.test.js +99 -0
- package/v2Containers/Rcs/utils.js +34 -4
- package/v2Containers/SmsTrai/Edit/index.js +35 -24
package/package.json
CHANGED
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
buildSyntheticSmsMustacheTags,
|
|
38
38
|
normalizeRcsTestCardMedia,
|
|
39
39
|
mapRcsSuggestionForTestMeta,
|
|
40
|
+
getRcsPrimaryTagExtractionText,
|
|
41
|
+
mergeSyntheticMustacheTags,
|
|
40
42
|
} from './utils';
|
|
41
43
|
import AddTestCustomerButton from './AddTestCustomer';
|
|
42
44
|
import ExistingCustomerModal from './ExistingCustomerModal';
|
|
@@ -574,7 +576,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
574
576
|
}
|
|
575
577
|
const hasFallbackSmsBody = !!(smsFallbackContent?.templateContent || smsFallbackContent?.content);
|
|
576
578
|
if (channel === CHANNELS.RCS && hasFallbackSmsBody) {
|
|
577
|
-
const rcsPrimaryTags = extractedTags
|
|
579
|
+
const rcsPrimaryTags = mergeSyntheticMustacheTags(extractedTags, getRcsPrimaryTagExtractionText(formData));
|
|
578
580
|
const fallbackSmsTextForTags = smsFallbackTextForTagExtraction ?? '';
|
|
579
581
|
const fallbackSmsTagRows = smsTemplateHasMustacheTags(fallbackSmsTextForTags)
|
|
580
582
|
? (smsFallbackExtractedTags?.length > 0
|
|
@@ -585,11 +587,15 @@ const CommonTestAndPreview = (props) => {
|
|
|
585
587
|
if (mergedRcsAndFallbackTags.length > 0) return mergedRcsAndFallbackTags;
|
|
586
588
|
return buildSyntheticSmsMustacheTags(fallbackSmsTextForTags);
|
|
587
589
|
}
|
|
590
|
+
if (channel === CHANNELS.RCS) {
|
|
591
|
+
return mergeSyntheticMustacheTags(extractedTags, getRcsPrimaryTagExtractionText(formData));
|
|
592
|
+
}
|
|
588
593
|
return extractedTags ?? [];
|
|
589
594
|
}, [
|
|
590
595
|
channel,
|
|
591
596
|
extractedTags,
|
|
592
597
|
getCurrentContent,
|
|
598
|
+
formData,
|
|
593
599
|
smsFallbackContent,
|
|
594
600
|
smsFallbackExtractedTags,
|
|
595
601
|
smsFallbackTextForTagExtraction,
|
|
@@ -3184,7 +3190,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
3184
3190
|
* Apply tag extraction when content comes from RCS + SMS fallback (no API call).
|
|
3185
3191
|
*/
|
|
3186
3192
|
const applyRcsSmsFallbackTagExtraction = () => {
|
|
3187
|
-
const
|
|
3193
|
+
const rcsPrimaryTagTree = mergeSyntheticMustacheTags(extractedTags, getRcsPrimaryTagExtractionText(formData));
|
|
3194
|
+
const rcsPrimaryCategorized = categorizeTags(rcsPrimaryTagTree);
|
|
3188
3195
|
const fallbackSmsResolvedForTags = smsFallbackTextForTagExtraction ?? '';
|
|
3189
3196
|
let fallbackSmsTagTree = smsFallbackExtractedTags?.length > 0
|
|
3190
3197
|
? smsFallbackExtractedTags
|
|
@@ -3210,8 +3217,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
3210
3217
|
*/
|
|
3211
3218
|
useEffect(() => {
|
|
3212
3219
|
if (!show) return;
|
|
3213
|
-
|
|
3214
|
-
if (channel === CHANNELS.RCS && hasFallbackSmsTemplate) {
|
|
3220
|
+
if (channel === CHANNELS.RCS) {
|
|
3215
3221
|
applyRcsSmsFallbackTagExtraction();
|
|
3216
3222
|
return;
|
|
3217
3223
|
}
|
|
@@ -3227,8 +3233,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
3227
3233
|
setOptionalTags(optional);
|
|
3228
3234
|
setTagsExtracted(hasPersonalizationTags);
|
|
3229
3235
|
setCustomValues((prev) => mergeCustomValuesWithTagKeys(prev, { required, optional }, { required: [], optional: [] }));
|
|
3230
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags
|
|
3231
|
-
}, [show, extractedTags, channel, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
|
|
3236
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- applyRcsSmsFallbackTagExtraction closes over latest extractedTags/smsFallbackExtractedTags/formData
|
|
3237
|
+
}, [show, extractedTags, channel, formData, smsFallbackContent, smsFallbackExtractedTags, getCurrentContent, smsFallbackTextForTagExtraction]);
|
|
3232
3238
|
|
|
3233
3239
|
/**
|
|
3234
3240
|
* Get content to run tag extraction on (channel-specific).
|
|
@@ -3561,6 +3561,84 @@ describe('CommonTestAndPreview', () => {
|
|
|
3561
3561
|
});
|
|
3562
3562
|
});
|
|
3563
3563
|
|
|
3564
|
+
describe('RCS primary card content synthetic tag fallback', () => {
|
|
3565
|
+
const rcsCardFormData = (description) => ({
|
|
3566
|
+
rcsTitle: '',
|
|
3567
|
+
rcsDesc: description,
|
|
3568
|
+
versions: {
|
|
3569
|
+
base: {
|
|
3570
|
+
content: {
|
|
3571
|
+
RCS: {
|
|
3572
|
+
rcsContent: {
|
|
3573
|
+
cardContent: [{ title: '', description }],
|
|
3574
|
+
},
|
|
3575
|
+
},
|
|
3576
|
+
},
|
|
3577
|
+
},
|
|
3578
|
+
},
|
|
3579
|
+
});
|
|
3580
|
+
|
|
3581
|
+
it('should surface a {{tag}} from RCS card description as a fillable field when the extract-tags API returns nothing (no SMS fallback)', async () => {
|
|
3582
|
+
render(
|
|
3583
|
+
<TestWrapper>
|
|
3584
|
+
<CommonTestAndPreview
|
|
3585
|
+
{...defaultProps}
|
|
3586
|
+
channel={CHANNELS.RCS}
|
|
3587
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3588
|
+
extractedTags={[]}
|
|
3589
|
+
/>
|
|
3590
|
+
</TestWrapper>
|
|
3591
|
+
);
|
|
3592
|
+
|
|
3593
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3594
|
+
|
|
3595
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3596
|
+
expect(tags.some((tag) => tag.name === 'user_id_b64')).toBe(true);
|
|
3597
|
+
});
|
|
3598
|
+
|
|
3599
|
+
it('should surface an RCS card tag alongside a merged fallback SMS tag when both are present', async () => {
|
|
3600
|
+
render(
|
|
3601
|
+
<TestWrapper>
|
|
3602
|
+
<CommonTestAndPreview
|
|
3603
|
+
{...defaultProps}
|
|
3604
|
+
channel={CHANNELS.RCS}
|
|
3605
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3606
|
+
extractedTags={[]}
|
|
3607
|
+
smsFallbackContent={{
|
|
3608
|
+
templateContent: 'Fallback {{name}}',
|
|
3609
|
+
content: 'Fallback {{name}}',
|
|
3610
|
+
}}
|
|
3611
|
+
/>
|
|
3612
|
+
</TestWrapper>
|
|
3613
|
+
);
|
|
3614
|
+
|
|
3615
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3616
|
+
|
|
3617
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3618
|
+
expect(tags.some((tag) => tag.name === 'user_id_b64')).toBe(true);
|
|
3619
|
+
expect(tags.some((tag) => tag.name === 'name')).toBe(true);
|
|
3620
|
+
});
|
|
3621
|
+
|
|
3622
|
+
it('should not duplicate a tag the extract-tags API already returned for RCS card content', async () => {
|
|
3623
|
+
const apiTags = [{ name: 'user_id_b64', metaData: { userDriven: false }, children: [] }];
|
|
3624
|
+
render(
|
|
3625
|
+
<TestWrapper>
|
|
3626
|
+
<CommonTestAndPreview
|
|
3627
|
+
{...defaultProps}
|
|
3628
|
+
channel={CHANNELS.RCS}
|
|
3629
|
+
formData={rcsCardFormData('Visit store for offer {{user_id_b64}}')}
|
|
3630
|
+
extractedTags={apiTags}
|
|
3631
|
+
/>
|
|
3632
|
+
</TestWrapper>
|
|
3633
|
+
);
|
|
3634
|
+
|
|
3635
|
+
await waitFor(() => expect(lastLeftPanelContentProps).not.toBeNull());
|
|
3636
|
+
|
|
3637
|
+
const tags = lastLeftPanelContentProps.extractedTags;
|
|
3638
|
+
expect(tags.filter((tag) => tag.name === 'user_id_b64')).toHaveLength(1);
|
|
3639
|
+
});
|
|
3640
|
+
});
|
|
3641
|
+
|
|
3564
3642
|
describe('handleDiscardCustomValues — preview reset', () => {
|
|
3565
3643
|
it('should call updatePreviewRequested when handleDiscardCustomValues is invoked', async () => {
|
|
3566
3644
|
render(
|
|
@@ -27,6 +27,40 @@ export const buildSyntheticSmsMustacheTags = (body = '') => {
|
|
|
27
27
|
}));
|
|
28
28
|
};
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Raw RCS primary-card text (title + description across all cards) to scan for `{{…}}` tags.
|
|
32
|
+
* Mirrors the carousel/standalone branching in prepareTagExtractionPayload's RCS case so both
|
|
33
|
+
* the extract-tags API payload and the client-side synthetic tag fallback agree on card text.
|
|
34
|
+
*/
|
|
35
|
+
export const getRcsPrimaryTagExtractionText = (formDataObj) => {
|
|
36
|
+
const rcsCardContentArr =
|
|
37
|
+
formDataObj?.versions?.base?.content?.RCS?.rcsContent?.cardContent
|
|
38
|
+
?? formDataObj?.content?.RCS?.rcsContent?.cardContent;
|
|
39
|
+
if (Array.isArray(rcsCardContentArr) && rcsCardContentArr.length > 0) {
|
|
40
|
+
const carouselTagText = rcsCardContentArr
|
|
41
|
+
.map((card) => `${card.title || ''} ${card.description || ''}`)
|
|
42
|
+
.join(' ')
|
|
43
|
+
.trim();
|
|
44
|
+
if (carouselTagText) return carouselTagText;
|
|
45
|
+
}
|
|
46
|
+
return `${formDataObj?.rcsTitle || ''} ${formDataObj?.rcsDesc || ''}`.trim();
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Combine tags the extract-tags API returned with tags synthesized client-side from a raw
|
|
51
|
+
* `{{…}}` scan of `rawText`, so a tag the backend's catalog doesn't recognize (e.g. a reserved
|
|
52
|
+
* token like `user_id_b64`) still surfaces as a fillable field instead of silently disappearing.
|
|
53
|
+
* Dedupes by name so API-recognized tags (which carry richer metaData) are never overridden.
|
|
54
|
+
*/
|
|
55
|
+
export const mergeSyntheticMustacheTags = (apiTags = [], rawText = '') => {
|
|
56
|
+
if (!smsTemplateHasMustacheTags(rawText)) return apiTags ?? [];
|
|
57
|
+
const existingNames = new Set((apiTags ?? []).map((tag) => tag?.name));
|
|
58
|
+
const synthetic = buildSyntheticSmsMustacheTags(rawText).filter(
|
|
59
|
+
(tag) => !existingNames.has(tag.name),
|
|
60
|
+
);
|
|
61
|
+
return [...(apiTags ?? []), ...synthetic];
|
|
62
|
+
};
|
|
63
|
+
|
|
30
64
|
/** RCS createMessageMeta: normalize media shape (mediaUrl, thumbnailUrl, height string). */
|
|
31
65
|
export const normalizeRcsTestCardMedia = (media) => {
|
|
32
66
|
if (!media || typeof media !== 'object') return undefined;
|
|
@@ -747,11 +747,17 @@ export function SmsFallback({
|
|
|
747
747
|
const base = uploadedTemplate.versions?.base || {};
|
|
748
748
|
const content = base.template_message || base['sms-editor'] || '';
|
|
749
749
|
const templateName = uploadedTemplate.name || base.template_name || '';
|
|
750
|
+
// DLT-registered sender IDs for this newly-created template — same field
|
|
751
|
+
// buildFallbackDataFromTemplate reads. Without it, Test & Preview's delivery-settings
|
|
752
|
+
// filter sees an empty registeredSenderIds and falls back to showing every sender.
|
|
753
|
+
const registeredSenderIds = Array.isArray(base.header) ? base.header : [];
|
|
750
754
|
if (content || templateName) {
|
|
751
755
|
onChange({
|
|
756
|
+
smsTemplateId: base.template_id || uploadedTemplate._id || '',
|
|
752
757
|
templateName,
|
|
753
758
|
content,
|
|
754
759
|
templateContent: content,
|
|
760
|
+
registeredSenderIds,
|
|
755
761
|
unicodeValidity: typeof base['unicode-validity'] === JS_TYPE_BOOLEAN
|
|
756
762
|
? base['unicode-validity']
|
|
757
763
|
: true,
|
|
@@ -91,6 +91,7 @@ import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants'
|
|
|
91
91
|
import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
|
|
92
92
|
import { RCS_STATUSES, contentType as RCS_CONTENT_TYPE, EMBEDDED } from '../Rcs/constants';
|
|
93
93
|
import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
|
|
94
|
+
import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
|
|
94
95
|
import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
95
96
|
import { CREATIVE } from '../Facebook/constants';
|
|
96
97
|
import { LOYALTY } from '../App/constants';
|
|
@@ -1444,18 +1445,26 @@ export class Creatives extends React.Component {
|
|
|
1444
1445
|
const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
|
|
1445
1446
|
? cardContentFromSubmit[0]
|
|
1446
1447
|
: null;
|
|
1447
|
-
const
|
|
1448
|
+
const cardVarMappedFromFirstRcsCard =
|
|
1449
|
+
firstCardFromSubmit?.cardVarMapped != null
|
|
1450
|
+
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1451
|
+
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1452
|
+
: null;
|
|
1453
|
+
// Campaigns/consumers should see the literal tag (e.g. `{{first_name}}`) in title/description,
|
|
1454
|
+
// not the internal numeric slot placeholder (`{{1}}`) — that slot scheme is only meaningful
|
|
1455
|
+
// to this editor's own reopen/hydration (see getFormData, which persists raw cardContentFromSubmit
|
|
1456
|
+
// separately for that round-trip and is unaffected by this).
|
|
1457
|
+
const cardContent = mapRcsCardContentForConsumerWithResolvedTags(
|
|
1458
|
+
cardContentFromSubmit,
|
|
1459
|
+
cardVarMappedFromFirstRcsCard,
|
|
1460
|
+
isFullModeForRcsConsumerPayload,
|
|
1461
|
+
);
|
|
1448
1462
|
const rcsContent = {
|
|
1449
1463
|
contentType,
|
|
1450
1464
|
cardType,
|
|
1451
1465
|
cardSettings,
|
|
1452
1466
|
cardContent,
|
|
1453
1467
|
};
|
|
1454
|
-
const cardVarMappedFromFirstRcsCard =
|
|
1455
|
-
firstCardFromSubmit?.cardVarMapped != null
|
|
1456
|
-
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1457
|
-
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1458
|
-
: null;
|
|
1459
1468
|
const includeRootRcsCardVarMappedOnConsumerPayload =
|
|
1460
1469
|
cardVarMappedFromFirstRcsCard
|
|
1461
1470
|
&& Object.keys(cardVarMappedFromFirstRcsCard).length > 0
|
|
@@ -267,6 +267,59 @@ describe('Test SlideBoxContent container', () => {
|
|
|
267
267
|
expect(instance.state.templateData.rcsCardVarMapped).toEqual(cardVarMapped);
|
|
268
268
|
});
|
|
269
269
|
|
|
270
|
+
it('RCS getCreativesData sends the literal tag (not the numeric slot) in title/description to consumers', async () => {
|
|
271
|
+
renderedComponent = shallowWithIntl(
|
|
272
|
+
<Creatives
|
|
273
|
+
loyaltyMetaData={loyaltyMetaData}
|
|
274
|
+
Templates={rcsTemplates}
|
|
275
|
+
channel="RCS"
|
|
276
|
+
slidBoxContent="editTemplate"
|
|
277
|
+
templateData={rcsEditTemplateData}
|
|
278
|
+
handleCloseCreatives={handleCloseCreatives}
|
|
279
|
+
getCreativesData={getCreativesData}
|
|
280
|
+
isFullMode={false}
|
|
281
|
+
templateActions={{
|
|
282
|
+
getCdnTransformationConfig,
|
|
283
|
+
}}
|
|
284
|
+
/>,
|
|
285
|
+
);
|
|
286
|
+
renderedComponent.instance().onEditTemplate();
|
|
287
|
+
|
|
288
|
+
const mockValue = {
|
|
289
|
+
validity: true,
|
|
290
|
+
value: {
|
|
291
|
+
name: 'rcs_creative_name',
|
|
292
|
+
type: 'RCS',
|
|
293
|
+
versions: {
|
|
294
|
+
base: {
|
|
295
|
+
content: {
|
|
296
|
+
RCS: {
|
|
297
|
+
rcsContent: {
|
|
298
|
+
contentType: 'RICHCARD',
|
|
299
|
+
cardType: 'STANDALONE',
|
|
300
|
+
cardSettings: {},
|
|
301
|
+
cardContent: [{
|
|
302
|
+
title: 'Hi {{1}}',
|
|
303
|
+
description: 'Visit {{2}}',
|
|
304
|
+
cardVarMapped: { 1: '{{first_name}}', 2: '{{loyalty_points}}' },
|
|
305
|
+
}],
|
|
306
|
+
},
|
|
307
|
+
smsFallBackContent: {},
|
|
308
|
+
},
|
|
309
|
+
},
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
renderedComponent.instance().getFormData(mockValue);
|
|
316
|
+
await tick();
|
|
317
|
+
|
|
318
|
+
const sentCardContent = getCreativesData.mock.calls[0][0].rcsContent.cardContent[0];
|
|
319
|
+
expect(sentCardContent.title).toBe('Hi {{first_name}}');
|
|
320
|
+
expect(sentCardContent.description).toBe('Visit {{loyalty_points}}');
|
|
321
|
+
});
|
|
322
|
+
|
|
270
323
|
it('Text getCreatives data for rcs, data from creatives done to campaigns', async () => {
|
|
271
324
|
renderFunction('SMS', 'editTemplate', smsTemplates, smsEditTemplateData );
|
|
272
325
|
renderedComponent.instance().onEditTemplate();
|
|
@@ -19,6 +19,9 @@ import { sanitizeCardVarMappedValue } from './utils';
|
|
|
19
19
|
|
|
20
20
|
const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
|
|
21
21
|
|
|
22
|
+
export const getCarouselVarMapKey = (cardIndex, varName) =>
|
|
23
|
+
(cardIndex === 0 ? varName : `c${cardIndex}::${varName}`);
|
|
24
|
+
|
|
22
25
|
export const getCarouselImageAssetIndex = (cardIndex) =>
|
|
23
26
|
RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
|
|
24
27
|
export const getCarouselVideoAssetIndex = (cardIndex) =>
|
|
@@ -142,26 +145,22 @@ export const getCarouselDescriptionCharacterCount = (carouselData, cardIndex) =>
|
|
|
142
145
|
return descriptionText ? descriptionText.length : 0;
|
|
143
146
|
};
|
|
144
147
|
|
|
145
|
-
/**
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
/**
|
|
149
|
+
* Preview (TestAndPreviewSlidebox / listing preview): resolve each card's title/desc through
|
|
150
|
+
* `resolveCarouselTemplateWithMap(str, cardIndex)` — a per-card-scoped resolver (see
|
|
151
|
+
* `getCarouselVarMapKey`). Each card's variables are independent, so no cross-card slot offset is
|
|
152
|
+
* needed (unlike the old flat/global-slot scheme this replaced).
|
|
153
|
+
*/
|
|
154
|
+
export const buildCarouselCardsForPreview = (cards = [], { isFullMode, resolveCarouselTemplateWithMap }) =>
|
|
155
|
+
(cards || []).map((card = {}, cardIndex) => {
|
|
152
156
|
const rawTitle = card.title || '';
|
|
153
157
|
const rawDesc = card.description || '';
|
|
154
|
-
const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
|
|
155
|
-
const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
|
|
156
158
|
const resolvedTitle = !isFullMode
|
|
157
|
-
?
|
|
159
|
+
? resolveCarouselTemplateWithMap(rawTitle, cardIndex)
|
|
158
160
|
: rawTitle;
|
|
159
161
|
const resolvedDesc = !isFullMode
|
|
160
|
-
?
|
|
162
|
+
? resolveCarouselTemplateWithMap(rawDesc, cardIndex)
|
|
161
163
|
: rawDesc;
|
|
162
|
-
if (!isFullMode) {
|
|
163
|
-
slotOffset += titleVarCount + descVarCount;
|
|
164
|
-
}
|
|
165
164
|
const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
|
|
166
165
|
const videoSrc = card.videoAsset?.videoSrc || '';
|
|
167
166
|
return {
|
|
@@ -174,7 +173,6 @@ export const buildCarouselCardsForPreview = (cards = [], { isFullMode, rcsVarReg
|
|
|
174
173
|
suggestions: card.suggestions || [],
|
|
175
174
|
};
|
|
176
175
|
});
|
|
177
|
-
};
|
|
178
176
|
|
|
179
177
|
/** Payload (create/edit RCS template): build `cardContent` for the carousel case. */
|
|
180
178
|
export const buildCarouselCardContentForPayload = (carouselData, {
|
|
@@ -182,7 +180,7 @@ export const buildCarouselCardContentForPayload = (carouselData, {
|
|
|
182
180
|
cardVarMapped,
|
|
183
181
|
selectedCarouselHeight,
|
|
184
182
|
rcsVarRegex,
|
|
185
|
-
}) => (carouselData || []).map((card = {}) => {
|
|
183
|
+
}) => (carouselData || []).map((card = {}, cardIndex) => {
|
|
186
184
|
const cardMediaType = card.mediaType;
|
|
187
185
|
const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
|
|
188
186
|
const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
|
|
@@ -201,7 +199,8 @@ export const buildCarouselCardContentForPayload = (carouselData, {
|
|
|
201
199
|
cardVarTokens.forEach((token) => {
|
|
202
200
|
const varName = getVarNameFromToken(token);
|
|
203
201
|
if (!varName) return;
|
|
204
|
-
|
|
202
|
+
const scopedValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)] ?? '';
|
|
203
|
+
cardVarMappedForCard[varName] = sanitizeCardVarMappedValue(scopedValue);
|
|
205
204
|
});
|
|
206
205
|
}
|
|
207
206
|
return {
|
|
@@ -159,7 +159,7 @@ const CarouselCard = ({
|
|
|
159
159
|
/>
|
|
160
160
|
<CapRow className="rcs_text_area_wrapper">
|
|
161
161
|
{(isEditFlow || !isFullMode) ? (
|
|
162
|
-
renderEditMessage(card.title || '')
|
|
162
|
+
renderEditMessage(card.title || '', cardIndex)
|
|
163
163
|
) : (
|
|
164
164
|
<CapInput
|
|
165
165
|
value={card.title || ''}
|
|
@@ -211,7 +211,7 @@ const CarouselCard = ({
|
|
|
211
211
|
/>
|
|
212
212
|
<CapRow className="rcs_text_area_wrapper">
|
|
213
213
|
{(isEditFlow || !isFullMode) ? (
|
|
214
|
-
renderEditMessage(card.description || '')
|
|
214
|
+
renderEditMessage(card.description || '', cardIndex)
|
|
215
215
|
) : (
|
|
216
216
|
<TextArea
|
|
217
217
|
autosize={{ minRows: 3, maxRows: 5 }}
|
|
@@ -79,7 +79,7 @@ export const RCS_NUMERIC_VAR_TOKEN_REGEX = /\{\{(\d+)\}\}/g;
|
|
|
79
79
|
/** `cardVarMapped` slot keys that are numeric only (legacy ordering). */
|
|
80
80
|
export const RCS_NUMERIC_VAR_NAME_REGEX = /^\d+$/;
|
|
81
81
|
/** Semantic Liquid-style keys on RCS `cardVarMapped` (same class as `{{…}}` inner names in the editor). */
|
|
82
|
-
export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^[\w.]+$/;
|
|
82
|
+
export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^[\w.()]+$/;
|
|
83
83
|
/** Escape all special RegExp characters in an arbitrary string before using it in `new RegExp(...)`. */
|
|
84
84
|
export const REGEX_SPECIAL_CHARS_ESCAPE_PATTERN = /[-/\\^$*+?.()|[\]{}]/g;
|
|
85
85
|
/** Entire string is wrapped in `{{…}}` mustache delimiters (allows any content including `}`). */
|