@capillarytech/creatives-library 8.0.328 → 8.0.330-alpha.0
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 +4 -0
- package/package.json +1 -1
- package/services/api.js +17 -0
- package/services/tests/api.test.js +85 -0
- package/utils/commonUtils.js +28 -0
- package/utils/tagValidations.js +2 -3
- package/utils/templateVarUtils.js +35 -6
- package/utils/tests/commonUtil.test.js +169 -0
- package/utils/tests/tagValidations.test.js +1 -1
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
- package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
- package/v2Components/CommonTestAndPreview/actions.js +10 -0
- package/v2Components/CommonTestAndPreview/constants.js +15 -1
- package/v2Components/CommonTestAndPreview/index.js +364 -72
- package/v2Components/CommonTestAndPreview/messages.js +106 -0
- package/v2Components/CommonTestAndPreview/reducer.js +10 -0
- package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
- package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
- package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
- package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
- package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
- package/v2Containers/CreativesContainer/index.js +15 -10
- package/v2Containers/Rcs/constants.js +6 -2
- package/v2Containers/Rcs/index.js +219 -91
- package/v2Containers/Rcs/messages.js +2 -1
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +2370 -1758
- package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
- package/v2Containers/Rcs/tests/utils.test.js +56 -0
- package/v2Containers/Rcs/utils.js +53 -6
- package/v2Containers/SmsTrai/Edit/index.js +27 -0
- package/v2Containers/SmsTrai/Edit/messages.js +5 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +357 -324
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5212
|
@@ -43,6 +43,7 @@ import {EXTERNAL_URL, SITE_URL, WEBPUSH_MEDIA_TYPES} from '../WebPush/constants'
|
|
|
43
43
|
import { IMAGE, VIDEO } from '../Facebook/Advertisement/constant';
|
|
44
44
|
import { RCS_STATUSES } from '../Rcs/constants';
|
|
45
45
|
import { mapRcsCardContentForConsumerWithResolvedTags } from '../Rcs/utils';
|
|
46
|
+
import { pickRcsCardVarMappedEntries } from '../Rcs/rcsLibraryHydrationUtils';
|
|
46
47
|
import { RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../../v2Components/CommonTestAndPreview/constants';
|
|
47
48
|
import { CREATIVE } from '../Facebook/constants';
|
|
48
49
|
import { LOYALTY } from '../App/constants';
|
|
@@ -743,7 +744,6 @@ export class Creatives extends React.Component {
|
|
|
743
744
|
smsFallBackContent = {},
|
|
744
745
|
creativeName = "",
|
|
745
746
|
channel = constants.RCS,
|
|
746
|
-
accountId = "",
|
|
747
747
|
rcsCardVarMapped,
|
|
748
748
|
} = templateData || {};
|
|
749
749
|
const { isFullMode: isFullModeForRcsPayload } = this.props;
|
|
@@ -765,8 +765,13 @@ export class Creatives extends React.Component {
|
|
|
765
765
|
rootMirrorCardVarMapped != null && typeof rootMirrorCardVarMapped === 'object'
|
|
766
766
|
? rootMirrorCardVarMapped
|
|
767
767
|
: {};
|
|
768
|
-
const mergedFromRootAndNested = {
|
|
769
|
-
|
|
768
|
+
const mergedFromRootAndNested = {
|
|
769
|
+
...pickRcsCardVarMappedEntries(rootRecord),
|
|
770
|
+
...pickRcsCardVarMappedEntries(nestedRecord),
|
|
771
|
+
};
|
|
772
|
+
return Object.keys(mergedFromRootAndNested).length > 0
|
|
773
|
+
? mergedFromRootAndNested
|
|
774
|
+
: null;
|
|
770
775
|
})();
|
|
771
776
|
// Campaigns (embedded): do not duplicate `cardVarMapped` as root `rcsCardVarMapped` on send —
|
|
772
777
|
// slot map stays on `versions…cardContent[0].cardVarMapped` only. Library full mode keeps root mirror.
|
|
@@ -785,7 +790,6 @@ export class Creatives extends React.Component {
|
|
|
785
790
|
RCS: {
|
|
786
791
|
rcsContent: {
|
|
787
792
|
...rcsContent,
|
|
788
|
-
...(accountId && !isFullMode && { accountId }),
|
|
789
793
|
cardContent: [
|
|
790
794
|
{
|
|
791
795
|
...cardContent,
|
|
@@ -1237,7 +1241,7 @@ export class Creatives extends React.Component {
|
|
|
1237
1241
|
};
|
|
1238
1242
|
}
|
|
1239
1243
|
break;
|
|
1240
|
-
case constants.FACEBOOK:
|
|
1244
|
+
case constants.FACEBOOK: {
|
|
1241
1245
|
if (template.value) {
|
|
1242
1246
|
const FacebookAd = template?.value?.versions?.base?.content?.FacebookAd;
|
|
1243
1247
|
const { type } = FacebookAd[0];
|
|
@@ -1281,8 +1285,9 @@ export class Creatives extends React.Component {
|
|
|
1281
1285
|
selectedMarketingObjective: template.value.selectedMarketingObjective,
|
|
1282
1286
|
};
|
|
1283
1287
|
}
|
|
1288
|
+
}
|
|
1284
1289
|
break;
|
|
1285
|
-
case constants.RCS:
|
|
1290
|
+
case constants.RCS: {
|
|
1286
1291
|
if (template.value) {
|
|
1287
1292
|
const { isFullMode: isFullModeForRcsConsumerPayload } = this.props;
|
|
1288
1293
|
const { name = "", versions = {} } = template.value || {};
|
|
@@ -1330,7 +1335,6 @@ export class Creatives extends React.Component {
|
|
|
1330
1335
|
contentType = "",
|
|
1331
1336
|
cardType = "",
|
|
1332
1337
|
cardSettings = {},
|
|
1333
|
-
accountId = "",
|
|
1334
1338
|
} = get(versions, 'base.content.RCS.rcsContent', {});
|
|
1335
1339
|
const rootRcsCardVarMappedFromSubmit = get(template, 'value.rcsCardVarMapped');
|
|
1336
1340
|
const firstCardFromSubmit = Array.isArray(cardContentFromSubmit)
|
|
@@ -1350,7 +1354,7 @@ export class Creatives extends React.Component {
|
|
|
1350
1354
|
const cardVarMappedFromFirstRcsCard =
|
|
1351
1355
|
firstCardFromSubmit?.cardVarMapped != null
|
|
1352
1356
|
&& typeof firstCardFromSubmit.cardVarMapped === 'object'
|
|
1353
|
-
? firstCardFromSubmit.cardVarMapped
|
|
1357
|
+
? pickRcsCardVarMappedEntries(firstCardFromSubmit.cardVarMapped)
|
|
1354
1358
|
: null;
|
|
1355
1359
|
const includeRootRcsCardVarMappedOnConsumerPayload =
|
|
1356
1360
|
cardVarMappedFromFirstRcsCard
|
|
@@ -1360,7 +1364,6 @@ export class Creatives extends React.Component {
|
|
|
1360
1364
|
channel,
|
|
1361
1365
|
creativeName: name,
|
|
1362
1366
|
rcsContent,
|
|
1363
|
-
accountId: accountId,
|
|
1364
1367
|
...(includeRootRcsCardVarMappedOnConsumerPayload
|
|
1365
1368
|
? { rcsCardVarMapped: cardVarMappedFromFirstRcsCard }
|
|
1366
1369
|
: {}),
|
|
@@ -1382,8 +1385,9 @@ export class Creatives extends React.Component {
|
|
|
1382
1385
|
};
|
|
1383
1386
|
}
|
|
1384
1387
|
}
|
|
1388
|
+
}
|
|
1385
1389
|
break;
|
|
1386
|
-
case constants.ZALO:
|
|
1390
|
+
case constants.ZALO: {
|
|
1387
1391
|
if (template.value) {
|
|
1388
1392
|
templateData = {
|
|
1389
1393
|
...template.value,
|
|
@@ -1392,6 +1396,7 @@ export class Creatives extends React.Component {
|
|
|
1392
1396
|
delete templateData.type;
|
|
1393
1397
|
}
|
|
1394
1398
|
}
|
|
1399
|
+
}
|
|
1395
1400
|
break;
|
|
1396
1401
|
case constants.WEBPUSH: {
|
|
1397
1402
|
if (template.value) {
|
|
@@ -90,13 +90,17 @@ export const RCS_MEDIA_TYPES = {
|
|
|
90
90
|
VIDEO: 'VIDEO',
|
|
91
91
|
};
|
|
92
92
|
|
|
93
|
-
|
|
94
|
-
export const
|
|
93
|
+
/** Match `{{…}}` placeholders including Liquid-style names (e.g. `tag.FORMAT_1`). Must align with split + `isAnyTemplateVarToken` / SMS fallback. */
|
|
94
|
+
export const rcsVarRegex = /\{\{[^}]+\}\}/g;
|
|
95
|
+
/** True when the whole string is a single RCS/mustache token (used when testing segment tokens). */
|
|
96
|
+
export const rcsVarTestRegex = /^\{\{[^}]+\}\}$/;
|
|
95
97
|
|
|
96
98
|
/** Matches all `{{N}}` numeric-index variable tokens in a template string (global). */
|
|
97
99
|
export const RCS_NUMERIC_VAR_TOKEN_REGEX = /\{\{(\d+)\}\}/g;
|
|
98
100
|
/** `cardVarMapped` slot keys that are numeric only (legacy ordering). */
|
|
99
101
|
export const RCS_NUMERIC_VAR_NAME_REGEX = /^\d+$/;
|
|
102
|
+
/** Semantic Liquid-style keys on RCS `cardVarMapped` (same class as `{{…}}` inner names in the editor). */
|
|
103
|
+
export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^[\w.]+$/;
|
|
100
104
|
/** Escape `RegExp` metacharacters when building a pattern from user/tag text. */
|
|
101
105
|
export const RCS_REGEX_META_CHARS_PATTERN = /[.*+?^${}()|[\]\\]/g;
|
|
102
106
|
/** Entire string is a single `{{tag}}` token (value still a placeholder). */
|
|
@@ -44,10 +44,12 @@ import {
|
|
|
44
44
|
normalizeLibraryLoadedTitleDesc,
|
|
45
45
|
mergeRcsSmsFallBackContentFromDetails,
|
|
46
46
|
mergeRcsSmsFallbackVarMapLayers,
|
|
47
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord,
|
|
47
48
|
pickFirstSmsFallbackTemplateString,
|
|
48
49
|
syncCardVarMappedSemanticsFromSlots,
|
|
49
50
|
hasMeaningfulSmsFallbackShape,
|
|
50
51
|
getLibrarySmsFallbackApiBaselineFromTemplateData,
|
|
52
|
+
pickRcsCardVarMappedEntries,
|
|
51
53
|
} from './rcsLibraryHydrationUtils';
|
|
52
54
|
import {
|
|
53
55
|
RCS,
|
|
@@ -118,6 +120,7 @@ import {
|
|
|
118
120
|
getTemplateStatusType,
|
|
119
121
|
normalizeCardVarMapped,
|
|
120
122
|
coalesceCardVarMappedToTemplate,
|
|
123
|
+
getRcsSemanticVarNamesSpanningTitleAndDesc,
|
|
121
124
|
resolveCardVarMappedSlotValue,
|
|
122
125
|
sanitizeCardVarMappedValue,
|
|
123
126
|
} from './utils';
|
|
@@ -261,7 +264,6 @@ export const Rcs = (props) => {
|
|
|
261
264
|
const [accessToken, setAccessToken] = useState('');
|
|
262
265
|
const [hostName, setHostName] = useState('');
|
|
263
266
|
const [accountName, setAccountName] = useState('');
|
|
264
|
-
const [rcsAccount, setRcsAccount] = useState('');
|
|
265
267
|
useEffect(() => {
|
|
266
268
|
const accountObj = accountData.selectedRcsAccount || {};
|
|
267
269
|
if (!isEmpty(accountObj)) {
|
|
@@ -277,14 +279,12 @@ export const Rcs = (props) => {
|
|
|
277
279
|
setAccessToken(configs.accessToken || '');
|
|
278
280
|
setHostName(accountObj.hostName || '');
|
|
279
281
|
setAccountName(accountObj.name || '');
|
|
280
|
-
setRcsAccount(accountObj.id || '');
|
|
281
282
|
} else {
|
|
282
283
|
setAccountId('');
|
|
283
284
|
setWecrmAccountId('');
|
|
284
285
|
setAccessToken('');
|
|
285
286
|
setHostName('');
|
|
286
287
|
setAccountName('');
|
|
287
|
-
setRcsAccount('');
|
|
288
288
|
}
|
|
289
289
|
}, [accountData.selectedRcsAccount]);
|
|
290
290
|
|
|
@@ -389,6 +389,12 @@ export const Rcs = (props) => {
|
|
|
389
389
|
|
|
390
390
|
const splitTemplateVarStringRcs = (str) => splitTemplateVarString(str, rcsVarRegex);
|
|
391
391
|
|
|
392
|
+
/** Same `{{tag}}` in both title and description must not share one semantic map entry. */
|
|
393
|
+
const rcsSpanningSemanticVarNames = useMemo(
|
|
394
|
+
() => getRcsSemanticVarNamesSpanningTitleAndDesc(templateTitle, templateDesc, rcsVarRegex),
|
|
395
|
+
[templateTitle, templateDesc],
|
|
396
|
+
);
|
|
397
|
+
|
|
392
398
|
/** Global slot index (0-based, title vars then desc) for a VarSegmentMessageEditor `id` (`{{tok}}_segIdx`), or null if not found. */
|
|
393
399
|
const getGlobalSlotIndexForRcsFieldId = (varSegmentCompositeId, fieldTemplateStr, fieldType) => {
|
|
394
400
|
const titleVarTokenMatches = templateTitle?.match(rcsVarRegex) ?? [];
|
|
@@ -421,6 +427,7 @@ export const Rcs = (props) => {
|
|
|
421
427
|
key,
|
|
422
428
|
globalSlot,
|
|
423
429
|
isEditLike,
|
|
430
|
+
rcsSpanningSemanticVarNames.has(key),
|
|
424
431
|
);
|
|
425
432
|
if (isNil(v) || String(v)?.trim?.() === '') return elem;
|
|
426
433
|
return String(v);
|
|
@@ -487,6 +494,7 @@ export const Rcs = (props) => {
|
|
|
487
494
|
isFullMode,
|
|
488
495
|
isEditFlow,
|
|
489
496
|
cardVarMapped,
|
|
497
|
+
rcsSpanningSemanticVarNames,
|
|
490
498
|
]);
|
|
491
499
|
|
|
492
500
|
const testAndPreviewContent = useMemo(() => getTemplateContent(), [getTemplateContent]);
|
|
@@ -517,7 +525,7 @@ export const Rcs = (props) => {
|
|
|
517
525
|
const nextVarMap = {};
|
|
518
526
|
let varOrdinal = 0;
|
|
519
527
|
arr.forEach((elem, idx) => {
|
|
520
|
-
//
|
|
528
|
+
// Mustache tokens: {{1}}, {{user_name}}, {{tag.FORMAT_1}}, etc.
|
|
521
529
|
if (rcsVarTestRegex.test(elem)) {
|
|
522
530
|
const id = `${elem}_${idx}`;
|
|
523
531
|
const varName = getVarNameFromToken(elem);
|
|
@@ -528,6 +536,7 @@ export const Rcs = (props) => {
|
|
|
528
536
|
varName,
|
|
529
537
|
globalSlot,
|
|
530
538
|
isEditLike,
|
|
539
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
531
540
|
);
|
|
532
541
|
nextVarMap[id] = mappedValue;
|
|
533
542
|
}
|
|
@@ -537,7 +546,7 @@ export const Rcs = (props) => {
|
|
|
537
546
|
|
|
538
547
|
initField(templateTitle, setTitleVarMappedData, 0);
|
|
539
548
|
initField(templateDesc, setDescVarMappedData, titleTokenCount);
|
|
540
|
-
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
|
|
549
|
+
}, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames]);
|
|
541
550
|
|
|
542
551
|
useEffect(() => {
|
|
543
552
|
if (!isEditFlow && isFullMode) {
|
|
@@ -752,10 +761,7 @@ export const Rcs = (props) => {
|
|
|
752
761
|
get(details, 'versions.base.content.RCS.rcsContent.cardSettings', '')
|
|
753
762
|
|| get(details, 'rcsContent.cardSettings', '');
|
|
754
763
|
setMediaData(mediaData, mediaType, cardSettings);
|
|
755
|
-
|
|
756
|
-
const rcsAccountId = get(details, 'versions.base.content.RCS.rcsContent.accountId', '');
|
|
757
|
-
setRcsAccount(rcsAccountId);
|
|
758
|
-
}
|
|
764
|
+
|
|
759
765
|
const smsFallbackContent = mergeRcsSmsFallBackContentFromDetails(details);
|
|
760
766
|
const base = get(smsFallbackContent, 'versions.base', {});
|
|
761
767
|
const updatedEditor = base['updated-sms-editor'] ?? base['sms-editor'];
|
|
@@ -786,12 +792,17 @@ export const Rcs = (props) => {
|
|
|
786
792
|
typeof smsFallbackContent.unicodeValidity === 'boolean'
|
|
787
793
|
? smsFallbackContent.unicodeValidity
|
|
788
794
|
: (typeof base['unicode-validity'] === 'boolean' ? base['unicode-validity'] : true);
|
|
795
|
+
const registeredSenderIdsFromApi =
|
|
796
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackContent);
|
|
789
797
|
const nextSmsState = {
|
|
790
798
|
templateName: smsFallbackContent.smsTemplateName || '',
|
|
791
799
|
content: fallbackMessage,
|
|
792
800
|
templateContent: fallbackMessage,
|
|
793
801
|
unicodeValidity: unicodeFromApi,
|
|
794
802
|
...(hasVarMapped && { rcsSmsFallbackVarMapped: varMappedFromPayload }),
|
|
803
|
+
...(Array.isArray(registeredSenderIdsFromApi) && registeredSenderIdsFromApi.length > 0
|
|
804
|
+
? { registeredSenderIds: registeredSenderIdsFromApi }
|
|
805
|
+
: {}),
|
|
795
806
|
};
|
|
796
807
|
const hydrationKey = JSON.stringify({
|
|
797
808
|
creativeKey: details._id || details.name || details.creativeName || '',
|
|
@@ -799,6 +810,10 @@ export const Rcs = (props) => {
|
|
|
799
810
|
content: nextSmsState.content,
|
|
800
811
|
unicodeValidity: nextSmsState.unicodeValidity,
|
|
801
812
|
varMapped: nextSmsState.rcsSmsFallbackVarMapped || {},
|
|
813
|
+
senderIds:
|
|
814
|
+
Array.isArray(registeredSenderIdsFromApi)
|
|
815
|
+
? registeredSenderIdsFromApi.join('\u001f')
|
|
816
|
+
: '',
|
|
802
817
|
});
|
|
803
818
|
if (
|
|
804
819
|
isFullMode
|
|
@@ -898,58 +913,87 @@ export const Rcs = (props) => {
|
|
|
898
913
|
return templateStr.replace(re, `{{${tagName}}}`);
|
|
899
914
|
};
|
|
900
915
|
|
|
901
|
-
const onTagSelect = (
|
|
902
|
-
if (!
|
|
903
|
-
const
|
|
904
|
-
if (
|
|
905
|
-
const
|
|
906
|
-
if (
|
|
907
|
-
const
|
|
908
|
-
const
|
|
909
|
-
if (!
|
|
910
|
-
const
|
|
911
|
-
const
|
|
912
|
-
|
|
913
|
-
const
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
916
|
+
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
917
|
+
if (!varSegmentCompositeDomId) return;
|
|
918
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
919
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
920
|
+
const segmentIndexSuffix = varSegmentCompositeDomId.slice(underscoreIndexInCompositeId + 1);
|
|
921
|
+
if (segmentIndexSuffix === '' || isNaN(Number(segmentIndexSuffix))) return;
|
|
922
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
923
|
+
const semanticOrNumericVarName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
924
|
+
if (!semanticOrNumericVarName) return;
|
|
925
|
+
const isNumericPlaceholderSlot = RCS_NUMERIC_VAR_NAME_REGEX.test(String(semanticOrNumericVarName));
|
|
926
|
+
const templateStringForField =
|
|
927
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? templateTitle : templateDesc;
|
|
928
|
+
const titleOrMessageFieldType =
|
|
929
|
+
tagAreaField === RCS_TAG_AREA_FIELD_TITLE ? TITLE_TEXT : MESSAGE_TEXT;
|
|
930
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
931
|
+
varSegmentCompositeDomId,
|
|
932
|
+
templateStringForField,
|
|
933
|
+
titleOrMessageFieldType,
|
|
934
|
+
);
|
|
935
|
+
const cardVarMappedNumericSlotKey =
|
|
936
|
+
globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined
|
|
937
|
+
? String(globalVarSlotIndexZeroBased + 1)
|
|
938
|
+
: null;
|
|
939
|
+
|
|
940
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
941
|
+
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
942
|
+
if (isNumericPlaceholderSlot) {
|
|
943
|
+
const existingValueBeforeAppend = (
|
|
944
|
+
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
945
|
+
).toString();
|
|
946
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
947
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
948
|
+
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
922
949
|
} else {
|
|
923
|
-
//
|
|
924
|
-
//
|
|
925
|
-
//
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
950
|
+
// Same semantic token (e.g. {{adv}}) in title and body must not share one map key for
|
|
951
|
+
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
952
|
+
// read/write the global numeric slot only and drop the shared semantic key.
|
|
953
|
+
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
954
|
+
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
955
|
+
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
956
|
+
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
957
|
+
delete updatedCardVarMapped[semanticOrNumericVarName];
|
|
958
|
+
if (cardVarMappedNumericSlotKey) {
|
|
959
|
+
updatedCardVarMapped[cardVarMappedNumericSlotKey] = mappedValueAfterAppendingTag;
|
|
932
960
|
} else {
|
|
933
|
-
|
|
961
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
934
962
|
}
|
|
935
963
|
}
|
|
936
|
-
return
|
|
964
|
+
return updatedCardVarMapped;
|
|
937
965
|
});
|
|
938
966
|
|
|
939
|
-
if (
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
967
|
+
if (
|
|
968
|
+
isNumericPlaceholderSlot
|
|
969
|
+
&& (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC)
|
|
970
|
+
) {
|
|
971
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
972
|
+
setTemplateTitle((previousTitle) => {
|
|
973
|
+
const titleAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
974
|
+
previousTitle || '',
|
|
975
|
+
semanticOrNumericVarName,
|
|
976
|
+
selectedTagNameFromPicker,
|
|
977
|
+
);
|
|
978
|
+
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
979
|
+
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
980
|
+
// 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
|
|
981
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
982
|
+
return titleAfterReplacingNumericPlaceholder;
|
|
946
983
|
});
|
|
947
984
|
} else {
|
|
948
|
-
setTemplateDesc((
|
|
949
|
-
const
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
985
|
+
setTemplateDesc((previousDescription) => {
|
|
986
|
+
const descriptionAfterReplacingNumericPlaceholder = replaceNumericPlaceholderWithTagInTemplate(
|
|
987
|
+
previousDescription || '',
|
|
988
|
+
semanticOrNumericVarName,
|
|
989
|
+
selectedTagNameFromPicker,
|
|
990
|
+
);
|
|
991
|
+
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
992
|
+
return previousDescription;
|
|
993
|
+
}
|
|
994
|
+
setTemplateDescError(variableErrorHandling(descriptionAfterReplacingNumericPlaceholder));
|
|
995
|
+
setRcsVarSegmentEditorRemountKey((k) => k + 1);
|
|
996
|
+
return descriptionAfterReplacingNumericPlaceholder;
|
|
953
997
|
});
|
|
954
998
|
}
|
|
955
999
|
}
|
|
@@ -1107,7 +1151,8 @@ export const Rcs = (props) => {
|
|
|
1107
1151
|
if(!isFullMode){
|
|
1108
1152
|
return false;
|
|
1109
1153
|
}
|
|
1110
|
-
|
|
1154
|
+
// Allow Liquid-style param names: letters, digits, underscore, dots (e.g. dynamic_expiry_date_after_3_days.FORMAT_1)
|
|
1155
|
+
if (!/^[\w.]+$/.test(paramName)) {
|
|
1111
1156
|
return formatMessage(messages.unknownCharactersError);
|
|
1112
1157
|
}
|
|
1113
1158
|
}
|
|
@@ -1220,6 +1265,7 @@ const onTitleAddVar = () => {
|
|
|
1220
1265
|
varName,
|
|
1221
1266
|
globalSlot,
|
|
1222
1267
|
isEditLike,
|
|
1268
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
1223
1269
|
);
|
|
1224
1270
|
}
|
|
1225
1271
|
});
|
|
@@ -1228,42 +1274,37 @@ const onTitleAddVar = () => {
|
|
|
1228
1274
|
|
|
1229
1275
|
const titleVarSegmentValueMapById = useMemo(
|
|
1230
1276
|
() => getRcsValueMap(templateTitle, TITLE_TEXT),
|
|
1231
|
-
[templateTitle, cardVarMapped, isEditFlow, isFullMode],
|
|
1277
|
+
[templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1232
1278
|
);
|
|
1233
1279
|
const descriptionVarSegmentValueMapById = useMemo(
|
|
1234
1280
|
() => getRcsValueMap(templateDesc, MESSAGE_TEXT),
|
|
1235
|
-
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode],
|
|
1281
|
+
[templateDesc, templateTitle, cardVarMapped, isEditFlow, isFullMode, rcsSpanningSemanticVarNames],
|
|
1236
1282
|
);
|
|
1237
1283
|
|
|
1238
|
-
const handleRcsVarChange = (
|
|
1239
|
-
const
|
|
1240
|
-
if (
|
|
1241
|
-
const
|
|
1242
|
-
const variableName = getVarNameFromToken(
|
|
1284
|
+
const handleRcsVarChange = (varSegmentCompositeDomId, value, type) => {
|
|
1285
|
+
const underscoreIndexInCompositeId = varSegmentCompositeDomId.lastIndexOf('_');
|
|
1286
|
+
if (underscoreIndexInCompositeId === -1) return;
|
|
1287
|
+
const mustacheTokenFromCompositeId = varSegmentCompositeDomId.slice(0, underscoreIndexInCompositeId);
|
|
1288
|
+
const variableName = getVarNameFromToken(mustacheTokenFromCompositeId);
|
|
1243
1289
|
if (variableName === undefined || variableName === null || variableName === '') return;
|
|
1244
1290
|
const isInvalidValue = value?.trim() === '';
|
|
1245
1291
|
const coercedSlotValue = isInvalidValue ? '' : value;
|
|
1246
|
-
const
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
if (variableName !== numericKey) {
|
|
1261
|
-
delete nextVarMap[variableName];
|
|
1262
|
-
}
|
|
1263
|
-
} else {
|
|
1264
|
-
nextVarMap[variableName] = coercedSlotValue;
|
|
1292
|
+
const templateStringForField = type === TITLE_TEXT ? templateTitle : templateDesc;
|
|
1293
|
+
const globalVarSlotIndexZeroBased = getGlobalSlotIndexForRcsFieldId(
|
|
1294
|
+
varSegmentCompositeDomId,
|
|
1295
|
+
templateStringForField,
|
|
1296
|
+
type,
|
|
1297
|
+
);
|
|
1298
|
+
setCardVarMapped((previousCardVarMapped) => {
|
|
1299
|
+
const updatedCardVarMapped = { ...previousCardVarMapped };
|
|
1300
|
+
// Remove stale semantic key: keeping it causes every other slot sharing the same
|
|
1301
|
+
// variable name (e.g. {{adv}} in both title and description) to read the same value
|
|
1302
|
+
// via the semantic-key fallback in resolveCardVarMappedSlotValue.
|
|
1303
|
+
delete updatedCardVarMapped[variableName];
|
|
1304
|
+
if (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined) {
|
|
1305
|
+
updatedCardVarMapped[String(globalVarSlotIndexZeroBased + 1)] = coercedSlotValue;
|
|
1265
1306
|
}
|
|
1266
|
-
return
|
|
1307
|
+
return updatedCardVarMapped;
|
|
1267
1308
|
});
|
|
1268
1309
|
};
|
|
1269
1310
|
|
|
@@ -1381,6 +1422,7 @@ const onTitleAddVar = () => {
|
|
|
1381
1422
|
</CapRow>
|
|
1382
1423
|
<CapRow className="rcs-create-template-message-input">
|
|
1383
1424
|
<div className="rcs_text_area_wrapper">
|
|
1425
|
+
{/* Edit/library: segmented inputs (split on {{…}}). Full-mode create: single TextArea below — manual entry there never hits segment split. TagList replaces {{n}} in template string here. */}
|
|
1384
1426
|
{(isEditFlow || !isFullMode)
|
|
1385
1427
|
? (
|
|
1386
1428
|
<VarSegmentMessageEditor
|
|
@@ -1971,7 +2013,6 @@ const onTitleAddVar = () => {
|
|
|
1971
2013
|
content: {
|
|
1972
2014
|
RCS: {
|
|
1973
2015
|
rcsContent: {
|
|
1974
|
-
...(rcsAccount && !isFullMode && { accountId: rcsAccount }),
|
|
1975
2016
|
cardType: STANDALONE,
|
|
1976
2017
|
cardSettings: {
|
|
1977
2018
|
cardOrientation: isMediaTypeImage ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.orientation || VERTICAL : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.orientation || VERTICAL,
|
|
@@ -1997,23 +2038,25 @@ const onTitleAddVar = () => {
|
|
|
1997
2038
|
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
1998
2039
|
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
1999
2040
|
];
|
|
2041
|
+
const cardVarMappedForRcsCardOnly = pickRcsCardVarMappedEntries(
|
|
2042
|
+
cardVarMapped,
|
|
2043
|
+
);
|
|
2044
|
+
// Persist numeric slot keys only ("1","2",…) — avoids duplicating the same value under
|
|
2045
|
+
// semantic names (e.g. both "1" and dynamic_expiry_date_after_3_days.FORMAT_1). Hydration
|
|
2046
|
+
// and coalesceCardVarMappedToTemplate still resolve from numeric keys + template tokens.
|
|
2000
2047
|
const persistedSlotVarMap = {};
|
|
2001
|
-
const seenSemanticVarNames = new Set();
|
|
2002
2048
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
2003
2049
|
const varName = getVarNameFromToken(token);
|
|
2004
2050
|
if (!varName) return;
|
|
2005
2051
|
const resolvedRawValue = resolveCardVarMappedSlotValue(
|
|
2006
|
-
|
|
2052
|
+
cardVarMappedForRcsCardOnly,
|
|
2007
2053
|
varName,
|
|
2008
2054
|
slotIndexZeroBased,
|
|
2009
2055
|
isSlotMappingMode,
|
|
2056
|
+
rcsSpanningSemanticVarNames.has(varName),
|
|
2010
2057
|
);
|
|
2011
2058
|
const sanitizedSlotValue = sanitizeCardVarMappedValue(resolvedRawValue);
|
|
2012
2059
|
persistedSlotVarMap[String(slotIndexZeroBased + 1)] = sanitizedSlotValue;
|
|
2013
|
-
if (!seenSemanticVarNames.has(varName)) {
|
|
2014
|
-
seenSemanticVarNames.add(varName);
|
|
2015
|
-
persistedSlotVarMap[varName] = sanitizedSlotValue;
|
|
2016
|
-
}
|
|
2017
2060
|
});
|
|
2018
2061
|
return { cardVarMapped: persistedSlotVarMap };
|
|
2019
2062
|
})()),
|
|
@@ -2030,6 +2073,53 @@ const onTitleAddVar = () => {
|
|
|
2030
2073
|
|| smsFallbackForPayload.message
|
|
2031
2074
|
|| smsFallbackForPayload.smsContent
|
|
2032
2075
|
|| '';
|
|
2076
|
+
/**
|
|
2077
|
+
* Campaigns `getTraiSenderIds` / Iris read `smsFallBackContent.templateConfigs.registeredSenderIds`.
|
|
2078
|
+
* Library `smsFallbackForPayload` omits ids — use merged state (`smsFallbackMerged`) like test preview.
|
|
2079
|
+
*/
|
|
2080
|
+
const m = smsFallbackMerged || {};
|
|
2081
|
+
const tcSibling = m.templateConfigs && typeof m.templateConfigs === 'object'
|
|
2082
|
+
? m.templateConfigs
|
|
2083
|
+
: {};
|
|
2084
|
+
const smsFallbackTemplateId =
|
|
2085
|
+
(m.smsTemplateId != null && String(m.smsTemplateId).trim() !== ''
|
|
2086
|
+
? String(m.smsTemplateId)
|
|
2087
|
+
: '')
|
|
2088
|
+
|| (tcSibling.templateId != null && String(tcSibling.templateId).trim() !== ''
|
|
2089
|
+
? String(tcSibling.templateId)
|
|
2090
|
+
: '');
|
|
2091
|
+
const smsFallbackTemplateStr =
|
|
2092
|
+
pickFirstSmsFallbackTemplateString(m)
|
|
2093
|
+
|| (typeof m.templateContent === 'string' ? m.templateContent : '')
|
|
2094
|
+
|| (typeof tcSibling.template === 'string' ? tcSibling.template : '')
|
|
2095
|
+
|| '';
|
|
2096
|
+
const smsFallbackTemplateName =
|
|
2097
|
+
m.templateName
|
|
2098
|
+
|| m.smsTemplateName
|
|
2099
|
+
|| tcSibling.templateName
|
|
2100
|
+
|| tcSibling.name
|
|
2101
|
+
|| '';
|
|
2102
|
+
const registeredSenderIdsForPayload = Array.isArray(m.registeredSenderIds)
|
|
2103
|
+
? m.registeredSenderIds
|
|
2104
|
+
: Array.isArray(tcSibling.registeredSenderIds)
|
|
2105
|
+
? tcSibling.registeredSenderIds
|
|
2106
|
+
: Array.isArray(tcSibling.header)
|
|
2107
|
+
? tcSibling.header
|
|
2108
|
+
: null;
|
|
2109
|
+
const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
|
|
2110
|
+
const smsFallbackTemplateConfigs =
|
|
2111
|
+
smsFallbackTemplateId || hasRegisteredSenderIds
|
|
2112
|
+
? {
|
|
2113
|
+
...(smsFallbackTemplateId && { templateId: smsFallbackTemplateId }),
|
|
2114
|
+
...(smsFallbackTemplateStr && { template: smsFallbackTemplateStr }),
|
|
2115
|
+
...(smsFallbackTemplateName && {
|
|
2116
|
+
templateName: smsFallbackTemplateName,
|
|
2117
|
+
}),
|
|
2118
|
+
...(hasRegisteredSenderIds && {
|
|
2119
|
+
registeredSenderIds: registeredSenderIdsForPayload,
|
|
2120
|
+
}),
|
|
2121
|
+
}
|
|
2122
|
+
: null;
|
|
2033
2123
|
return {
|
|
2034
2124
|
smsFallBackContent: {
|
|
2035
2125
|
smsTemplateName: smsFallbackForPayload.templateName || '',
|
|
@@ -2043,6 +2133,9 @@ const onTitleAddVar = () => {
|
|
|
2043
2133
|
&& Object.keys(smsFallbackForPayload.rcsSmsFallbackVarMapped).length > 0 && {
|
|
2044
2134
|
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: smsFallbackForPayload.rcsSmsFallbackVarMapped,
|
|
2045
2135
|
}),
|
|
2136
|
+
...(smsFallbackTemplateConfigs && {
|
|
2137
|
+
templateConfigs: smsFallbackTemplateConfigs,
|
|
2138
|
+
}),
|
|
2046
2139
|
},
|
|
2047
2140
|
};
|
|
2048
2141
|
})()),
|
|
@@ -2065,13 +2158,36 @@ const onTitleAddVar = () => {
|
|
|
2065
2158
|
(wecrmAccountId != null && String(wecrmAccountId).trim() !== '')
|
|
2066
2159
|
? String(wecrmAccountId)
|
|
2067
2160
|
: accountId;
|
|
2068
|
-
const
|
|
2161
|
+
const isSlotMappingModeForPreview = isEditFlow || !isFullMode;
|
|
2162
|
+
let rcsForTest = {
|
|
2069
2163
|
...rcs,
|
|
2070
2164
|
rcsContent: {
|
|
2071
2165
|
...rcs.rcsContent,
|
|
2072
2166
|
...(accountIdForCreateMessageMeta ? { accountId: accountIdForCreateMessageMeta } : {}),
|
|
2073
2167
|
},
|
|
2074
2168
|
};
|
|
2169
|
+
/** Approval payload keeps numeric-only `cardVarMapped`; preview APIs still need semantic keys. */
|
|
2170
|
+
if (isSlotMappingModeForPreview) {
|
|
2171
|
+
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2172
|
+
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2173
|
+
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2174
|
+
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2175
|
+
templateTitle,
|
|
2176
|
+
templateDesc,
|
|
2177
|
+
rcsVarRegex,
|
|
2178
|
+
);
|
|
2179
|
+
rcsForTest = {
|
|
2180
|
+
...rcsForTest,
|
|
2181
|
+
rcsContent: {
|
|
2182
|
+
...rcsForTest.rcsContent,
|
|
2183
|
+
cardContent: [
|
|
2184
|
+
{ ...cardContent[0], cardVarMapped: fullCardVarMapped },
|
|
2185
|
+
...cardContent.slice(1),
|
|
2186
|
+
],
|
|
2187
|
+
},
|
|
2188
|
+
};
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2075
2191
|
const out = {
|
|
2076
2192
|
versions: {
|
|
2077
2193
|
base: {
|
|
@@ -2242,7 +2358,13 @@ const onTitleAddVar = () => {
|
|
|
2242
2358
|
return true;
|
|
2243
2359
|
}
|
|
2244
2360
|
return orderedVarNames.some((name, globalIdx) => {
|
|
2245
|
-
const v = resolveCardVarMappedSlotValue(
|
|
2361
|
+
const v = resolveCardVarMappedSlotValue(
|
|
2362
|
+
cardVarMapped,
|
|
2363
|
+
name,
|
|
2364
|
+
globalIdx,
|
|
2365
|
+
true,
|
|
2366
|
+
rcsSpanningSemanticVarNames.has(name),
|
|
2367
|
+
);
|
|
2246
2368
|
const s = v == null ? '' : String(v);
|
|
2247
2369
|
return s.trim() === '';
|
|
2248
2370
|
});
|
|
@@ -2561,13 +2683,19 @@ const onTitleAddVar = () => {
|
|
|
2561
2683
|
content={testAndPreviewContent}
|
|
2562
2684
|
currentChannel={RCS}
|
|
2563
2685
|
orgUnitId={orgUnitId}
|
|
2686
|
+
rcsTestPreviewOptions={{ isLibraryMode: !isFullMode }}
|
|
2564
2687
|
smsFallbackContent={
|
|
2565
2688
|
smsFallbackData && (smsFallbackData.templateContent || smsFallbackData.content)
|
|
2566
2689
|
? {
|
|
2567
2690
|
templateContent:
|
|
2568
2691
|
smsFallbackData.templateContent || smsFallbackData.content || '',
|
|
2569
2692
|
templateName: smsFallbackData.templateName || '',
|
|
2570
|
-
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]:
|
|
2693
|
+
[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: !isFullMode
|
|
2694
|
+
? mergeRcsSmsFallbackVarMapLayers(
|
|
2695
|
+
getLibrarySmsFallbackApiBaselineFromTemplateData(templateData),
|
|
2696
|
+
smsFallbackData,
|
|
2697
|
+
)
|
|
2698
|
+
: mergeRcsSmsFallbackVarMapLayers({}, smsFallbackData),
|
|
2571
2699
|
}
|
|
2572
2700
|
: null
|
|
2573
2701
|
}
|