@capillarytech/creatives-library 9.0.50-alpha.0 → 9.0.50-alpha.2
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/constants/unified.js +1 -0
- package/package.json +1 -1
- package/services/api.js +0 -9
- package/utils/common.js +4 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +58 -85
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +4 -20
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +51 -146
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +0 -16
- package/v2Containers/CommunicationFlow/constants.js +0 -40
- package/v2Containers/CommunicationFlow/index.js +20 -15
- package/v2Containers/CommunicationFlow/messages.js +0 -4
- 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
|
@@ -108,22 +108,6 @@ describe('CommunicationFlowCard', () => {
|
|
|
108
108
|
fireEvent.click(screen.getByText('Add creatives'));
|
|
109
109
|
expect(screen.getByTestId('comm-flow-mock')).toHaveAttribute('data-mode', 'create');
|
|
110
110
|
});
|
|
111
|
-
|
|
112
|
-
it('disables the Add creatives button when disabled prop is true', () => {
|
|
113
|
-
renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
|
|
114
|
-
expect(screen.getByText('Add creatives').closest('button')).toBeDisabled();
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('does not open SlideBox when Add creatives is clicked while disabled', () => {
|
|
118
|
-
renderCard({ disabled: true, disabledTooltip: 'Enter an Alert name to add content' });
|
|
119
|
-
fireEvent.click(screen.getByText('Add creatives'));
|
|
120
|
-
expect(screen.queryByTestId('slide-box')).not.toBeInTheDocument();
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('Add creatives button is enabled by default (disabled prop absent)', () => {
|
|
124
|
-
renderCard();
|
|
125
|
-
expect(screen.getByText('Add creatives').closest('button')).not.toBeDisabled();
|
|
126
|
-
});
|
|
127
111
|
});
|
|
128
112
|
|
|
129
113
|
describe('configured state (initialData provided)', () => {
|
|
@@ -38,46 +38,6 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
|
|
|
38
38
|
export const AB_TEST = 'AB_TEST';
|
|
39
39
|
export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
|
|
40
40
|
|
|
41
|
-
// CCS CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which
|
|
42
|
-
// is this UI's own communicationStrategy value. Only SINGLE is supported this
|
|
43
|
-
// phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
|
|
44
|
-
export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
|
|
45
|
-
|
|
46
|
-
// Mirrors CCS's SingleChannelStrategy field names exactly (Cap Notify API
|
|
47
|
-
// Design doc, "Channel Strategy overview" / class dump). Kept separate from
|
|
48
|
-
// CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP below, which are the legacy
|
|
49
|
-
// messageMeta payload's keys and do not match CCS naming for every channel
|
|
50
|
-
// (e.g. legacy uses lowercase 'mpushMessageContent'; CCS uses 'mPushMessageContent').
|
|
51
|
-
export const CCS_CHANNEL_CONTENT_KEY_MAP = {
|
|
52
|
-
SMS: 'smsMessageContent',
|
|
53
|
-
EMAIL: 'emailMessageContent',
|
|
54
|
-
WHATSAPP: 'whatsappMessageContent',
|
|
55
|
-
MPUSH: 'mPushMessageContent',
|
|
56
|
-
INAPP: 'inAppMessageContent',
|
|
57
|
-
ANDROID: 'androidMessageContent',
|
|
58
|
-
IOS: 'iosMessageContent',
|
|
59
|
-
ZALO: 'zaloMessageContent',
|
|
60
|
-
VIBER: 'viberMessageContent',
|
|
61
|
-
LINE: 'lineMessageContent',
|
|
62
|
-
WEBPUSH: 'webPushMessageContent',
|
|
63
|
-
RCS: 'rcsMessageContent',
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
|
|
67
|
-
SMS: 'smsDeliverySettings',
|
|
68
|
-
EMAIL: 'emailDeliverySettings',
|
|
69
|
-
WHATSAPP: 'whatsappDeliverySettings',
|
|
70
|
-
MPUSH: 'mPushDeliverySettings',
|
|
71
|
-
INAPP: 'inAppDeliverySettings',
|
|
72
|
-
ANDROID: 'androidDeliverySettings',
|
|
73
|
-
IOS: 'iosDeliverySettings',
|
|
74
|
-
ZALO: 'zaloDeliverySettings',
|
|
75
|
-
VIBER: 'viberDeliverySettings',
|
|
76
|
-
LINE: 'lineDeliverySettings',
|
|
77
|
-
WEBPUSH: 'webPushDeliverySettings',
|
|
78
|
-
RCS: 'rcsDeliverySettings',
|
|
79
|
-
};
|
|
80
|
-
|
|
81
41
|
// Channel config - single source of truth for CreativesContainer and TemplatesV2
|
|
82
42
|
// paneKey: TemplatesV2 defaultPanes object key (for channelsToHide)
|
|
83
43
|
// channelProp: CreativesContainer channel prop (must match pane.key for tab to be active)
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import PropTypes from 'prop-types';
|
|
11
11
|
import CommunicationFlow from './CommunicationFlow';
|
|
12
|
+
import { hasSupportEngagementModule } from '../../utils/common';
|
|
12
13
|
|
|
13
14
|
const CommunicationFlowContainer = ({
|
|
14
15
|
config,
|
|
@@ -17,16 +18,22 @@ const CommunicationFlowContainer = ({
|
|
|
17
18
|
onCancel,
|
|
18
19
|
onChange,
|
|
19
20
|
...otherProps
|
|
20
|
-
}) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
}) => {
|
|
22
|
+
if (!hasSupportEngagementModule()) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<CommunicationFlow
|
|
28
|
+
config={config}
|
|
29
|
+
initialData={initialData}
|
|
30
|
+
onSave={onSave}
|
|
31
|
+
onCancel={onCancel}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
{...otherProps}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
};
|
|
30
37
|
|
|
31
38
|
CommunicationFlowContainer.propTypes = {
|
|
32
39
|
config: PropTypes.shape({
|
|
@@ -80,13 +87,11 @@ CommunicationFlowContainer.propTypes = {
|
|
|
80
87
|
controls: PropTypes.array,
|
|
81
88
|
}),
|
|
82
89
|
}),
|
|
83
|
-
context: PropTypes.object, // ouId, campaignId, programId
|
|
84
|
-
//
|
|
85
|
-
useCCS: PropTypes.bool, // If false, skips the CCS createCommDefinition call on save. Defaults to true.
|
|
90
|
+
context: PropTypes.object, // ouId, campaignId, programId, etc.
|
|
91
|
+
useCCS: PropTypes.bool, // If false, skips CCS bulk-claim-approve on save. Defaults to true.
|
|
86
92
|
}).isRequired,
|
|
87
93
|
initialData: PropTypes.object, // for edit/preview mode
|
|
88
|
-
onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
|
|
89
|
-
// ({id, referenceId, version, status}) is present when the CCS create succeeded
|
|
94
|
+
onSave: PropTypes.func.isRequired, // (data) => void - called when user saves
|
|
90
95
|
onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
|
|
91
96
|
onChange: PropTypes.func, // (data) => void - optional, called on data changes
|
|
92
97
|
};
|
|
@@ -363,8 +363,4 @@ export default {
|
|
|
363
363
|
id: `${prefix}.senderNotConfiguredError`,
|
|
364
364
|
defaultMessage: 'Selected domain gateway id is not correct. Please change the domain id or contact the gateway team to register them with Capillary.',
|
|
365
365
|
},
|
|
366
|
-
duplicateReferenceIdError: {
|
|
367
|
-
id: `${prefix}.duplicateReferenceIdError`,
|
|
368
|
-
defaultMessage: 'This reference ID is already in use in your organization. Please use a different reference ID.',
|
|
369
|
-
},
|
|
370
366
|
};
|
|
@@ -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
|
}
|
|
@@ -853,7 +853,7 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
853
853
|
expect(out[0].cardVarMapped).toBeUndefined();
|
|
854
854
|
});
|
|
855
855
|
|
|
856
|
-
it('
|
|
856
|
+
it('renumbers non-numeric title/description tokens to sequential {{N}} slots and keys cardVarMapped by number', () => {
|
|
857
857
|
const out = buildCarouselCardContentForPayload(
|
|
858
858
|
[{ title: 'Hi {{name}}', description: 'Pts {{points}}', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
859
859
|
{
|
|
@@ -863,7 +863,9 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
863
863
|
rcsVarRegex,
|
|
864
864
|
},
|
|
865
865
|
);
|
|
866
|
-
expect(out[0].
|
|
866
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
867
|
+
expect(out[0].description).toBe('Pts {{2}}');
|
|
868
|
+
expect(out[0].cardVarMapped).toEqual({ 1: 'Bob', 2: '' });
|
|
867
869
|
});
|
|
868
870
|
|
|
869
871
|
it('defaults an unmapped token value to empty string via sanitizeCardVarMappedValue', () => {
|
|
@@ -871,7 +873,8 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
871
873
|
[{ title: 'Hi {{missing}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
872
874
|
{ isSlotMappingMode: true, cardVarMapped: {}, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
873
875
|
);
|
|
874
|
-
expect(out[0].
|
|
876
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
877
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
875
878
|
});
|
|
876
879
|
|
|
877
880
|
it('handles a missing cardVarMapped object entirely', () => {
|
|
@@ -879,7 +882,23 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
879
882
|
[{ title: 'Hi {{name}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
880
883
|
{ isSlotMappingMode: true, cardVarMapped: undefined, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
881
884
|
);
|
|
882
|
-
expect(out[0].
|
|
885
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
886
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
it('leaves an already-numeric slot untouched and only renumbers the legacy/named slot alongside it', () => {
|
|
890
|
+
const out = buildCarouselCardContentForPayload(
|
|
891
|
+
[{ title: 'Hi {{1}}', description: 'Pts {{first_name}} exp {{3}}', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
892
|
+
{
|
|
893
|
+
isSlotMappingMode: true,
|
|
894
|
+
cardVarMapped: { 1: 'test', first_name: '{{first_name}}', 3: 'test' },
|
|
895
|
+
selectedCarouselHeight: 'SHORT',
|
|
896
|
+
rcsVarRegex,
|
|
897
|
+
},
|
|
898
|
+
);
|
|
899
|
+
expect(out[0].title).toBe('Hi {{1}}');
|
|
900
|
+
expect(out[0].description).toBe('Pts {{2}} exp {{3}}');
|
|
901
|
+
expect(out[0].cardVarMapped).toEqual({ 1: 'test', 2: '{{first_name}}', 3: 'test' });
|
|
883
902
|
});
|
|
884
903
|
|
|
885
904
|
it('skips a matched token that strips down to an empty variable name', () => {
|