@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
|
@@ -71,6 +71,7 @@ import {
|
|
|
71
71
|
mergeSmsFallbackForLibrary,
|
|
72
72
|
buildSmsFallBackContentForPayload,
|
|
73
73
|
pickRcsCardVarMappedEntries,
|
|
74
|
+
extractRegisteredSenderIdsFromSmsFallbackRecord,
|
|
74
75
|
} from './rcsLibraryHydrationUtils';
|
|
75
76
|
import {
|
|
76
77
|
RCS,
|
|
@@ -152,7 +153,6 @@ import { isTagIncluded } from '../../utils/commonUtils';
|
|
|
152
153
|
import injectReducer from '../../utils/injectReducer';
|
|
153
154
|
import v2RcsReducer from './reducer';
|
|
154
155
|
import {
|
|
155
|
-
buildRcsNumericMustachePlaceholderRegex,
|
|
156
156
|
getTemplateStatusType,
|
|
157
157
|
normalizeCardVarMapped,
|
|
158
158
|
coalesceCardVarMappedToTemplate,
|
|
@@ -175,6 +175,7 @@ import {
|
|
|
175
175
|
getCarouselDescriptionCharacterCount,
|
|
176
176
|
buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
|
|
177
177
|
buildCarouselCardContentForPayload,
|
|
178
|
+
getCarouselVarMapKey,
|
|
178
179
|
} from './carouselUtils';
|
|
179
180
|
import CarouselDimensionSelection from './components/CarouselDimensionSelection';
|
|
180
181
|
import CarouselCard from './components/CarouselCard';
|
|
@@ -775,7 +776,7 @@ export const Rcs = (props) => {
|
|
|
775
776
|
['title', 'description'].forEach((field) => {
|
|
776
777
|
const templateStr = card?.[field] || '';
|
|
777
778
|
if (!templateStr) return;
|
|
778
|
-
const resolved =
|
|
779
|
+
const resolved = resolveCarouselTemplateWithMap(templateStr, idx);
|
|
779
780
|
if (!resolved) {
|
|
780
781
|
updateCarouselErrors(idx, { [field]: false });
|
|
781
782
|
return;
|
|
@@ -867,8 +868,27 @@ export const Rcs = (props) => {
|
|
|
867
868
|
}).join('');
|
|
868
869
|
};
|
|
869
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Carousel resolve: each card's variables live under their own `getCarouselVarMapKey(cardIndex, name)`
|
|
873
|
+
* key in `cardVarMapped` (see that helper's doc) — no global slot-offset arithmetic needed since
|
|
874
|
+
* cards can no longer collide with each other's entries.
|
|
875
|
+
*/
|
|
876
|
+
const resolveCarouselTemplateWithMap = (str = '', cardIndex) => {
|
|
877
|
+
if (!str) return '';
|
|
878
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
879
|
+
return arr.map((elem) => {
|
|
880
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
881
|
+
const varName = getVarNameFromToken(elem);
|
|
882
|
+
const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)];
|
|
883
|
+
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
884
|
+
return String(slotValue);
|
|
885
|
+
}
|
|
886
|
+
return elem;
|
|
887
|
+
}).join('');
|
|
888
|
+
};
|
|
889
|
+
|
|
870
890
|
const buildCarouselCardsForPreview = (cards = []) =>
|
|
871
|
-
buildCarouselCardsForPreviewUtil(cards, { isFullMode,
|
|
891
|
+
buildCarouselCardsForPreviewUtil(cards, { isFullMode, resolveCarouselTemplateWithMap });
|
|
872
892
|
|
|
873
893
|
/**
|
|
874
894
|
* Content for TestAndPreviewSlidebox — apply cardVarMapped whenever the slot editor is shown
|
|
@@ -1196,28 +1216,56 @@ export const Rcs = (props) => {
|
|
|
1196
1216
|
|
|
1197
1217
|
setTemplateType(contentType.carousel);
|
|
1198
1218
|
setTemplateMediaType(RCS_MEDIA_TYPES.NONE);
|
|
1199
|
-
|
|
1200
|
-
// `rcsContent` instead of nested under versions.base.content.RCS (same fallback already
|
|
1201
|
-
// used for the rich_card media lookup below, line ~1793). Without it, cardWidth/height
|
|
1202
|
-
// silently fall back to SMALL/MEDIUM, mismatching the dimensions the thumbnail was
|
|
1203
|
-
// actually uploaded at and causing object-fit:cover to crop the preview image.
|
|
1219
|
+
|
|
1204
1220
|
const cardSettings = rcsContent?.cardSettings
|
|
1205
1221
|
|| get(details, 'rcsContent.cardSettings', {});
|
|
1206
1222
|
const cardWidth = cardSettings?.cardWidth || SMALL;
|
|
1207
1223
|
setSelectedCarouselWidth(cardWidth);
|
|
1208
1224
|
|
|
1209
|
-
const
|
|
1210
|
-
|
|
1211
|
-
|
|
1225
|
+
const nestedCards = Array.isArray(rcsContent?.cardContent) ? rcsContent.cardContent : [];
|
|
1226
|
+
const topCardsRaw = get(details, 'rcsContent.cardContent', []);
|
|
1227
|
+
const topCards = Array.isArray(topCardsRaw) ? topCardsRaw : [];
|
|
1228
|
+
const cardCount = Math.max(nestedCards.length, topCards.length);
|
|
1229
|
+
const mergedCards = Array.from({ length: cardCount }, (_, idx) => ({
|
|
1230
|
+
...(topCards[idx] || {}),
|
|
1231
|
+
...(nestedCards[idx] || {}),
|
|
1232
|
+
}));
|
|
1233
|
+
const cards = cardCount > 0 ? mergedCards : topCards;
|
|
1212
1234
|
const firstHeight = cards?.[0]?.media?.height || MEDIUM;
|
|
1213
1235
|
setSelectedCarouselHeight(firstHeight);
|
|
1214
1236
|
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1215
1237
|
setActiveCarouselIndex('0');
|
|
1216
1238
|
|
|
1217
1239
|
if (cards.length > 1) {
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1240
|
+
|
|
1241
|
+
setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}, idx) => {
|
|
1242
|
+
const rawCardMap = c?.cardVarMapped;
|
|
1243
|
+
if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
|
|
1244
|
+
if (idx === 0) return { ...merged, ...rawCardMap };
|
|
1245
|
+
const cardTitle = c?.title != null ? String(c.title) : '';
|
|
1246
|
+
const cardDesc = c?.description != null ? String(c.description) : '';
|
|
1247
|
+
const cardTokens = [
|
|
1248
|
+
...(cardTitle ? cardTitle.match(rcsVarRegex) ?? [] : []),
|
|
1249
|
+
...(cardDesc ? cardDesc.match(rcsVarRegex) ?? [] : []),
|
|
1250
|
+
];
|
|
1251
|
+
const cardOrderedTagNames = cardTokens.map((token) => getVarNameFromToken(token)).filter(Boolean);
|
|
1252
|
+
const cardMapBeforeCoalesce = isFullMode
|
|
1253
|
+
? normalizeCardVarMapped(rawCardMap, cardOrderedTagNames)
|
|
1254
|
+
: { ...rawCardMap };
|
|
1255
|
+
const cardMapAfterCoalesce = coalesceCardVarMappedToTemplate(
|
|
1256
|
+
cardMapBeforeCoalesce,
|
|
1257
|
+
cardTitle,
|
|
1258
|
+
cardDesc,
|
|
1259
|
+
rcsVarRegex,
|
|
1260
|
+
);
|
|
1261
|
+
const cardMapAfterSlotSync = !isFullMode
|
|
1262
|
+
? syncCardVarMappedSemanticsFromSlots(cardMapAfterCoalesce, cardTitle, cardDesc, rcsVarRegex)
|
|
1263
|
+
: cardMapAfterCoalesce;
|
|
1264
|
+
const namespacedCardMap = {};
|
|
1265
|
+
Object.entries(cardMapAfterSlotSync).forEach(([key, value]) => {
|
|
1266
|
+
namespacedCardMap[getCarouselVarMapKey(idx, key)] = value;
|
|
1267
|
+
});
|
|
1268
|
+
return { ...merged, ...namespacedCardMap };
|
|
1221
1269
|
}, { ...(previousVarMap || {}) }));
|
|
1222
1270
|
}
|
|
1223
1271
|
|
|
@@ -1426,10 +1474,29 @@ export const Rcs = (props) => {
|
|
|
1426
1474
|
globalActions.fetchSchemaForEntity(query);
|
|
1427
1475
|
};
|
|
1428
1476
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1477
|
+
/**
|
|
1478
|
+
* Replaces only the var token at `slotOrdinalZeroBased` (the field-local ordinal among
|
|
1479
|
+
* `{{...}}` tokens, left to right) with `{{tagName}}`. Unlike a name-based regex replace,
|
|
1480
|
+
* this targets a single slot even when the same tag name appears in other slots of the field.
|
|
1481
|
+
*/
|
|
1482
|
+
const replaceTagAtFieldSlotOrdinal = (templateStr, slotOrdinalZeroBased, tagName) => {
|
|
1483
|
+
if (!templateStr || slotOrdinalZeroBased === null || slotOrdinalZeroBased === undefined || !tagName) {
|
|
1484
|
+
return templateStr;
|
|
1485
|
+
}
|
|
1486
|
+
let varOrdinal = 0;
|
|
1487
|
+
let replaced = false;
|
|
1488
|
+
const updatedSegments = splitTemplateVarStringRcs(templateStr).map((segment) => {
|
|
1489
|
+
if (rcsVarTestRegex.test(segment)) {
|
|
1490
|
+
const currentOrdinal = varOrdinal;
|
|
1491
|
+
varOrdinal += 1;
|
|
1492
|
+
if (currentOrdinal === slotOrdinalZeroBased) {
|
|
1493
|
+
replaced = true;
|
|
1494
|
+
return `{{${tagName}}}`;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return segment;
|
|
1498
|
+
});
|
|
1499
|
+
return replaced ? updatedSegments.join('') : templateStr;
|
|
1433
1500
|
};
|
|
1434
1501
|
|
|
1435
1502
|
const onTagSelect = (selectedTagNameFromPicker, varSegmentCompositeDomId, tagAreaField) => {
|
|
@@ -1459,16 +1526,15 @@ export const Rcs = (props) => {
|
|
|
1459
1526
|
setCardVarMapped((previousCardVarMapped) => {
|
|
1460
1527
|
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1461
1528
|
if (isNumericPlaceholderSlot) {
|
|
1529
|
+
// Numeric slot ({{1}}, {{2}}, …) is the persisted API contract — keep the numeric key
|
|
1530
|
+
// itself (do not fork off a semantic-name key); the template text stays {{N}} below.
|
|
1462
1531
|
const existingValueBeforeAppend = (
|
|
1463
1532
|
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1464
1533
|
).toString();
|
|
1465
1534
|
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1466
|
-
|
|
1467
|
-
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1535
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1468
1536
|
} else {
|
|
1469
|
-
|
|
1470
|
-
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1471
|
-
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1537
|
+
|
|
1472
1538
|
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1473
1539
|
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1474
1540
|
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
@@ -1497,36 +1563,40 @@ export const Rcs = (props) => {
|
|
|
1497
1563
|
return updatedCardVarMapped;
|
|
1498
1564
|
});
|
|
1499
1565
|
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
)
|
|
1566
|
+
// Numeric slot ({{1}}, {{2}}, …): the token is already the persisted API contract — leave
|
|
1567
|
+
// templateTitle/templateDesc untouched. The chip label re-resolves via titleVarSegmentValueMapById
|
|
1568
|
+
// / descriptionVarSegmentValueMapById, both keyed off cardVarMapped (updated above).
|
|
1569
|
+
if (isNumericPlaceholderSlot) return;
|
|
1570
|
+
|
|
1571
|
+
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
|
|
1572
|
+
// Legacy slot that already holds a named tag (e.g. {{first_name}}) is replaced by
|
|
1573
|
+
// field-local position, since a name-based replace would also overwrite other slots
|
|
1574
|
+
// sharing that same tag name.
|
|
1575
|
+
const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
|
|
1576
|
+
? 0
|
|
1577
|
+
: (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
1578
|
+
const localSlotOrdinal = (globalVarSlotIndexZeroBased !== null && globalVarSlotIndexZeroBased !== undefined)
|
|
1579
|
+
? globalVarSlotIndexZeroBased - fieldOffsetForSlot
|
|
1580
|
+
: null;
|
|
1581
|
+
|
|
1504
1582
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1505
1583
|
setTemplateTitle((previousTitle) => {
|
|
1506
|
-
const
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
selectedTagNameFromPicker,
|
|
1510
|
-
);
|
|
1511
|
-
if (titleAfterReplacingNumericPlaceholder === previousTitle) return previousTitle;
|
|
1512
|
-
setTemplateTitleError(variableErrorHandling(titleAfterReplacingNumericPlaceholder));
|
|
1584
|
+
const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1585
|
+
if (titleAfterReplacingTag === previousTitle) return previousTitle;
|
|
1586
|
+
setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
|
|
1513
1587
|
// 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
|
|
1514
1588
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1515
|
-
return
|
|
1589
|
+
return titleAfterReplacingTag;
|
|
1516
1590
|
});
|
|
1517
1591
|
} else {
|
|
1518
1592
|
setTemplateDesc((previousDescription) => {
|
|
1519
|
-
const
|
|
1520
|
-
|
|
1521
|
-
semanticOrNumericVarName,
|
|
1522
|
-
selectedTagNameFromPicker,
|
|
1523
|
-
);
|
|
1524
|
-
if (descriptionAfterReplacingNumericPlaceholder === previousDescription) {
|
|
1593
|
+
const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1594
|
+
if (descriptionAfterReplacingTag === previousDescription) {
|
|
1525
1595
|
return previousDescription;
|
|
1526
1596
|
}
|
|
1527
|
-
setTemplateDescError(
|
|
1597
|
+
setTemplateDescError(computeTemplateDescError(descriptionAfterReplacingTag));
|
|
1528
1598
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1529
|
-
return
|
|
1599
|
+
return descriptionAfterReplacingTag;
|
|
1530
1600
|
});
|
|
1531
1601
|
}
|
|
1532
1602
|
}
|
|
@@ -1543,12 +1613,14 @@ export const Rcs = (props) => {
|
|
|
1543
1613
|
const token = carouselFocusedVarId.slice(0, sep);
|
|
1544
1614
|
const variableName = getVarNameFromToken(token);
|
|
1545
1615
|
if (!variableName) return;
|
|
1616
|
+
const activeIdx = parseInt(activeCarouselIndex, 10);
|
|
1617
|
+
const mapKey = getCarouselVarMapKey(isNaN(activeIdx) ? 0 : activeIdx, variableName);
|
|
1546
1618
|
setCardVarMapped((prev) => {
|
|
1547
|
-
const base = (prev?.[
|
|
1619
|
+
const base = (prev?.[mapKey] ?? '').toString();
|
|
1548
1620
|
const nextVal = `${base}{{${data}}}`;
|
|
1549
1621
|
return {
|
|
1550
1622
|
...(prev || {}),
|
|
1551
|
-
[
|
|
1623
|
+
[mapKey]: nextVal,
|
|
1552
1624
|
};
|
|
1553
1625
|
});
|
|
1554
1626
|
};
|
|
@@ -1660,31 +1732,13 @@ export const Rcs = (props) => {
|
|
|
1660
1732
|
return renderArray;
|
|
1661
1733
|
};
|
|
1662
1734
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
1663
|
-
let errorMessage = false;
|
|
1664
|
-
if (templateType === contentType.rich_card && !value.trim()) {
|
|
1665
|
-
errorMessage = formatMessage(messages.emptyTemplateTitleErrorMessage);
|
|
1666
|
-
} else if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
1667
|
-
errorMessage = formatMessage(messages.templateHeaderLengthError);
|
|
1668
|
-
} else {
|
|
1669
|
-
errorMessage = variableErrorHandling(value);
|
|
1670
|
-
}
|
|
1671
1735
|
setTemplateTitle(value);
|
|
1672
|
-
setTemplateTitleError(
|
|
1736
|
+
setTemplateTitleError(computeTemplateTitleError(value));
|
|
1673
1737
|
};
|
|
1674
1738
|
|
|
1675
1739
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
1676
|
-
let errorMessage = false;
|
|
1677
|
-
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
1678
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1679
|
-
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
1680
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1681
|
-
} else {
|
|
1682
|
-
errorMessage = false;
|
|
1683
|
-
}
|
|
1684
|
-
const varError = variableErrorHandling(value);
|
|
1685
|
-
const error = errorMessage || varError;
|
|
1686
1740
|
setTemplateDesc(value);
|
|
1687
|
-
setTemplateDescError(
|
|
1741
|
+
setTemplateDescError(computeTemplateDescError(value));
|
|
1688
1742
|
};
|
|
1689
1743
|
|
|
1690
1744
|
|
|
@@ -1767,6 +1821,29 @@ export const Rcs = (props) => {
|
|
|
1767
1821
|
return false;
|
|
1768
1822
|
};
|
|
1769
1823
|
|
|
1824
|
+
// Shared with onTemplateTitleChange so tag-picker inserts enforce the same length limit as manual typing.
|
|
1825
|
+
const computeTemplateTitleError = (value) => {
|
|
1826
|
+
if (templateType === contentType.rich_card && !value.trim()) {
|
|
1827
|
+
return formatMessage(messages.emptyTemplateTitleErrorMessage);
|
|
1828
|
+
}
|
|
1829
|
+
if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
1830
|
+
return formatMessage(messages.templateHeaderLengthError);
|
|
1831
|
+
}
|
|
1832
|
+
return variableErrorHandling(value);
|
|
1833
|
+
};
|
|
1834
|
+
|
|
1835
|
+
// Shared with onTemplateDescChange so tag-picker inserts enforce the same length limit as manual typing.
|
|
1836
|
+
const computeTemplateDescError = (value) => {
|
|
1837
|
+
let errorMessage = false;
|
|
1838
|
+
if (templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)) {
|
|
1839
|
+
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1840
|
+
} else if (templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH) {
|
|
1841
|
+
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1842
|
+
}
|
|
1843
|
+
const varError = variableErrorHandling(value);
|
|
1844
|
+
return errorMessage || varError;
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1770
1847
|
const onMessageAddVar = () => {
|
|
1771
1848
|
onAddVar(templateDesc);
|
|
1772
1849
|
};
|
|
@@ -1851,23 +1928,30 @@ const onTitleAddVar = () => {
|
|
|
1851
1928
|
return "";
|
|
1852
1929
|
};
|
|
1853
1930
|
|
|
1854
|
-
// Carousel: render variable-value editor for a given template string (title/description)
|
|
1855
|
-
// This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1856
|
-
|
|
1931
|
+
// Carousel: render variable-value editor for a given template string (title/description) belonging
|
|
1932
|
+
// to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1933
|
+
// tokens are editable. Each card's values live under their own `getCarouselVarMapKey(cardIndex, name)`
|
|
1934
|
+
// key in `cardVarMapped` so two cards using the same variable/tag name never collide.
|
|
1935
|
+
const renderCarouselEditMessage = (templateStr, cardIndex) => {
|
|
1857
1936
|
const renderArray = [];
|
|
1858
1937
|
const templateArr = splitTemplateVarString(templateStr);
|
|
1859
1938
|
if (templateArr?.length) {
|
|
1860
1939
|
templateArr.forEach((elem, index) => {
|
|
1861
1940
|
if (rcsVarTestRegex.test(elem)) {
|
|
1862
1941
|
const varName = getVarNameFromToken(elem);
|
|
1942
|
+
const mapKey = getCarouselVarMapKey(cardIndex, varName);
|
|
1943
|
+
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1863
1944
|
renderArray.push(
|
|
1864
|
-
<div
|
|
1945
|
+
<div
|
|
1946
|
+
key={`${elem}_${index}_${rcsVarSegmentEditorRemountKey}`}
|
|
1947
|
+
className="var-segment-message-editor__var-slot"
|
|
1948
|
+
>
|
|
1865
1949
|
<TextArea
|
|
1866
1950
|
id={`${elem}_${index}`}
|
|
1867
1951
|
placeholder={`enter the value for ${elem}`}
|
|
1868
1952
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1869
|
-
onChange={(e) => textAreaValueChange(e,
|
|
1870
|
-
value={
|
|
1953
|
+
onChange={(e) => textAreaValueChange(e, cardIndex)}
|
|
1954
|
+
value={computedValue}
|
|
1871
1955
|
onFocus={(e) => {
|
|
1872
1956
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1873
1957
|
setCarouselFocusedVarId(id);
|
|
@@ -1891,7 +1975,7 @@ const onTitleAddVar = () => {
|
|
|
1891
1975
|
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1892
1976
|
};
|
|
1893
1977
|
|
|
1894
|
-
const textAreaValueChange = (e,
|
|
1978
|
+
const textAreaValueChange = (e, cardIndex) => {
|
|
1895
1979
|
const value = e?.target?.value ?? '';
|
|
1896
1980
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1897
1981
|
if (!id) return;
|
|
@@ -1902,9 +1986,10 @@ const onTitleAddVar = () => {
|
|
|
1902
1986
|
const variableName = getVarNameFromToken(token);
|
|
1903
1987
|
|
|
1904
1988
|
if (variableName) {
|
|
1989
|
+
const mapKey = getCarouselVarMapKey(cardIndex, variableName);
|
|
1905
1990
|
setCardVarMapped((prev) => ({
|
|
1906
1991
|
...prev,
|
|
1907
|
-
[
|
|
1992
|
+
[mapKey]: isInvalidValue ? "" : value,
|
|
1908
1993
|
}));
|
|
1909
1994
|
}
|
|
1910
1995
|
};
|
|
@@ -2572,8 +2657,6 @@ const onTitleAddVar = () => {
|
|
|
2572
2657
|
const carouselVidDims =
|
|
2573
2658
|
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
2574
2659
|
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2575
|
-
// Debug log for embedded/library mode preview payload (carousel)
|
|
2576
|
-
// eslint-disable-next-line no-console
|
|
2577
2660
|
return (
|
|
2578
2661
|
<UnifiedPreview
|
|
2579
2662
|
channel={RCS}
|
|
@@ -2757,46 +2840,55 @@ const onTitleAddVar = () => {
|
|
|
2757
2840
|
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2758
2841
|
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2759
2842
|
if (isCarouselType) {
|
|
2760
|
-
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values
|
|
2761
|
-
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2843
|
+
// Carousel: resolve {{N}}/named slot tokens to the actual tag expressions / static values
|
|
2844
|
+
// the user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2762
2845
|
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2763
2846
|
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2764
|
-
//
|
|
2765
|
-
//
|
|
2766
|
-
//
|
|
2767
|
-
// mappings, causing tags from card 1+ to be missing or wrong.
|
|
2768
|
-
let carouselSlotOffset = 0;
|
|
2847
|
+
// Each card's variables live under their own getCarouselVarMapKey(cardIndex, name) slot, so
|
|
2848
|
+
// no cross-card slot-offset bookkeeping is needed (or safe — two cards' variables never share
|
|
2849
|
+
// a key, so there is nothing to accidentally collide).
|
|
2769
2850
|
rcsForTest = {
|
|
2770
2851
|
...rcsForTest,
|
|
2771
2852
|
rcsContent: {
|
|
2772
2853
|
...rcsForTest.rcsContent,
|
|
2773
|
-
cardContent: cardContent.map((card) => {
|
|
2854
|
+
cardContent: cardContent.map((card, cardIndex) => {
|
|
2774
2855
|
const rawTitle = card.title || '';
|
|
2775
2856
|
const rawDesc = card.description || '';
|
|
2776
|
-
const
|
|
2777
|
-
const
|
|
2778
|
-
const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
|
|
2779
|
-
const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
|
|
2780
|
-
carouselSlotOffset += titleVarCount + descVarCount;
|
|
2857
|
+
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
|
|
2858
|
+
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
|
|
2781
2859
|
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2782
2860
|
}),
|
|
2783
2861
|
},
|
|
2784
2862
|
};
|
|
2785
2863
|
} else if (isSlotMappingModeForPreview) {
|
|
2786
2864
|
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
2787
|
-
// API receives a correctly-keyed var map for non-carousel templates.
|
|
2865
|
+
// API receives a correctly-keyed var map for non-carousel templates. Also resolve the
|
|
2866
|
+
// {{N}} slot tokens in title/description themselves (mirrors the carousel branch above) —
|
|
2867
|
+
// otherwise prepareTagExtractionPayload/getRcsPrimaryTagExtractionText send the raw "{{1}}"
|
|
2868
|
+
// token as template text, which the extract-tags API doesn't recognize, so the
|
|
2869
|
+
// synthetic-tag fallback names the row "1" instead of the real tag.
|
|
2788
2870
|
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2789
2871
|
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2790
2872
|
templateTitle,
|
|
2791
2873
|
templateDesc,
|
|
2792
2874
|
rcsVarRegex,
|
|
2793
2875
|
);
|
|
2876
|
+
const rawStandaloneTitle = cardContent[0].title || '';
|
|
2877
|
+
const rawStandaloneDesc = cardContent[0].description || '';
|
|
2878
|
+
const standaloneTitleVarCount = (rawStandaloneTitle.match(rcsVarRegex) || []).length;
|
|
2879
|
+
const resolvedStandaloneTitle = resolveTemplateWithMap(rawStandaloneTitle, 0);
|
|
2880
|
+
const resolvedStandaloneDesc = resolveTemplateWithMap(rawStandaloneDesc, standaloneTitleVarCount);
|
|
2794
2881
|
rcsForTest = {
|
|
2795
2882
|
...rcsForTest,
|
|
2796
2883
|
rcsContent: {
|
|
2797
2884
|
...rcsForTest.rcsContent,
|
|
2798
2885
|
cardContent: [
|
|
2799
|
-
{
|
|
2886
|
+
{
|
|
2887
|
+
...cardContent[0],
|
|
2888
|
+
title: resolvedStandaloneTitle,
|
|
2889
|
+
description: resolvedStandaloneDesc,
|
|
2890
|
+
cardVarMapped: fullCardVarMapped,
|
|
2891
|
+
},
|
|
2800
2892
|
...cardContent.slice(1),
|
|
2801
2893
|
],
|
|
2802
2894
|
},
|
|
@@ -2812,16 +2904,23 @@ const onTitleAddVar = () => {
|
|
|
2812
2904
|
},
|
|
2813
2905
|
},
|
|
2814
2906
|
};
|
|
2815
|
-
|
|
2816
|
-
//
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2819
|
-
//
|
|
2820
|
-
|
|
2821
|
-
|
|
2907
|
+
|
|
2908
|
+
// buildSmsFallBackContentForPayload nests registeredSenderIds under smsFallBackContent.templateConfigs
|
|
2909
|
+
// only for full mode, or for campaign mode when isDltCampaign (smsRegister === 'DLT') is true — but it
|
|
2910
|
+
// always sets the flat smsFallBackContent.registeredSenderIds whenever the fallback template has them.
|
|
2911
|
+
// Extract via both so Test & Preview's DLT sender-id filter still sees real ids (and actually filters)
|
|
2912
|
+
// even when isDltCampaign was false at payload-build time.
|
|
2913
|
+
const smsFallbackRecordForTest = rcs?.rcsContent?.smsFallBackContent;
|
|
2914
|
+
const smsFallbackTcFromPayload = smsFallbackRecordForTest?.templateConfigs;
|
|
2915
|
+
const registeredSenderIdsForTest = extractRegisteredSenderIdsFromSmsFallbackRecord(smsFallbackRecordForTest);
|
|
2916
|
+
const hasRegisteredSenderIdsForTest =
|
|
2917
|
+
Array.isArray(registeredSenderIdsForTest) && registeredSenderIdsForTest.length > 0;
|
|
2918
|
+
if ((smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object') || hasRegisteredSenderIdsForTest) {
|
|
2822
2919
|
out.templateConfigs = {
|
|
2823
|
-
...smsFallbackTcFromPayload,
|
|
2824
|
-
|
|
2920
|
+
...(smsFallbackTcFromPayload && typeof smsFallbackTcFromPayload === 'object' ? smsFallbackTcFromPayload : {}),
|
|
2921
|
+
...(hasRegisteredSenderIdsForTest && { registeredSenderIds: registeredSenderIdsForTest }),
|
|
2922
|
+
// Real registered sender IDs mean filtering should happen regardless of the smsRegister heuristic.
|
|
2923
|
+
traiDltEnabled: isTraiDLTEnable(isFullMode, smsRegister) || hasRegisteredSenderIdsForTest,
|
|
2825
2924
|
};
|
|
2826
2925
|
}
|
|
2827
2926
|
return out;
|
|
@@ -2967,12 +3066,12 @@ const onTitleAddVar = () => {
|
|
|
2967
3066
|
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
2968
3067
|
);
|
|
2969
3068
|
if (hasEmptyField) return true;
|
|
2970
|
-
const unfilledVar = (carouselData || []).some((card) =>
|
|
3069
|
+
const unfilledVar = (carouselData || []).some((card, cardIndex) =>
|
|
2971
3070
|
['title', 'description'].some((field) => {
|
|
2972
3071
|
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
2973
3072
|
return tokens.some((token) => {
|
|
2974
3073
|
const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
2975
|
-
const slotValue = cardVarMapped?.[name];
|
|
3074
|
+
const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, name)];
|
|
2976
3075
|
return slotValue == null || String(slotValue).trim() === '';
|
|
2977
3076
|
});
|
|
2978
3077
|
})
|
|
@@ -217,6 +217,24 @@ export function buildSmsFallBackContentForPayload({
|
|
|
217
217
|
? tcSibling.header
|
|
218
218
|
: null;
|
|
219
219
|
const hasRegisteredSenderIds = Array.isArray(registeredSenderIdsForPayload);
|
|
220
|
+
// `rcsSmsFallbackVarMapped` (root of smsFallBackContent) is a custom field that
|
|
221
|
+
// normalizeRcsMessageContentForApi strips before the real API call (see comment below) — it
|
|
222
|
+
// only survives for same-session local re-hydration. templateConfigs.templateVariableMapping is
|
|
223
|
+
// the field the backend actually persists (same shape TRAI/DLT registration uses), so mirror the
|
|
224
|
+
// var map there too — otherwise slot values saved through campaigns/library mode are lost on reopen.
|
|
225
|
+
const varMapForTemplateConfigs =
|
|
226
|
+
smsFallbackForPayload?.[RCS_SMS_FALLBACK_VAR_MAPPED_PROP]
|
|
227
|
+
|| mergedFallback[RCS_SMS_FALLBACK_VAR_MAPPED_PROP];
|
|
228
|
+
const templateVariableMappingForPayload =
|
|
229
|
+
varMapForTemplateConfigs && typeof varMapForTemplateConfigs === 'object'
|
|
230
|
+
? Object.entries(varMapForTemplateConfigs).reduce((acc, [slotKey, slotValue], index) => {
|
|
231
|
+
if (slotValue == null || slotValue === '') return acc;
|
|
232
|
+
acc[slotKey] = { data: slotValue, count: index + 1 };
|
|
233
|
+
return acc;
|
|
234
|
+
}, {})
|
|
235
|
+
: null;
|
|
236
|
+
const hasTemplateVariableMapping =
|
|
237
|
+
templateVariableMappingForPayload && Object.keys(templateVariableMappingForPayload).length > 0;
|
|
220
238
|
const smsFallbackTemplateConfigs =
|
|
221
239
|
smsFallbackTemplateId || hasRegisteredSenderIds
|
|
222
240
|
? {
|
|
@@ -228,6 +246,9 @@ export function buildSmsFallBackContentForPayload({
|
|
|
228
246
|
...(hasRegisteredSenderIds && {
|
|
229
247
|
registeredSenderIds: registeredSenderIdsForPayload,
|
|
230
248
|
}),
|
|
249
|
+
...(hasTemplateVariableMapping && {
|
|
250
|
+
templateVariableMapping: templateVariableMappingForPayload,
|
|
251
|
+
}),
|
|
231
252
|
}
|
|
232
253
|
: null;
|
|
233
254
|
const isDltCampaign = !isFullMode && isTraiDLTEnable(isFullMode, smsRegister);
|
|
@@ -295,17 +316,37 @@ export function resolveSmsFallbackHydrationFromDetails(details) {
|
|
|
295
316
|
const fromNested = Array.isArray(updatedEditor)
|
|
296
317
|
? updatedEditor.join('')
|
|
297
318
|
: (typeof updatedEditor === 'string' ? updatedEditor : (smsEditor || ''));
|
|
298
|
-
const
|
|
319
|
+
const templateConfigsRecord =
|
|
320
|
+
smsFallbackContent.templateConfigs && typeof smsFallbackContent.templateConfigs === 'object'
|
|
321
|
+
? smsFallbackContent.templateConfigs
|
|
322
|
+
: {};
|
|
323
|
+
// For DLT/TRAI fallbacks, smsContent/smsTemplateContent/message are the send-ready text —
|
|
324
|
+
// {#var#} slots are already baked into a literal value there (required to match the
|
|
325
|
+
// TRAI-approved template). Only templateConfigs.template keeps {#var#} intact, so prefer it
|
|
326
|
+
// here — otherwise reopening the editor can never re-render that slot as its own box again.
|
|
327
|
+
const rawDltTemplate =
|
|
328
|
+
typeof templateConfigsRecord.template === 'string' && templateConfigsRecord.template.trim() !== ''
|
|
329
|
+
? templateConfigsRecord.template
|
|
330
|
+
: '';
|
|
331
|
+
const fallbackMessage = rawDltTemplate
|
|
332
|
+
|| smsFallbackContent.smsContent
|
|
299
333
|
|| smsFallbackContent.smsTemplateContent
|
|
300
334
|
|| smsFallbackContent.message
|
|
301
335
|
|| fromNested
|
|
302
336
|
|| '';
|
|
303
|
-
const
|
|
337
|
+
const varMapFromTemplateVariableMapping =
|
|
338
|
+
templateConfigsRecord.templateVariableMapping && typeof templateConfigsRecord.templateVariableMapping === 'object'
|
|
339
|
+
? Object.entries(templateConfigsRecord.templateVariableMapping).reduce((acc, [slotKey, slotEntry]) => {
|
|
340
|
+
const slotValue = slotEntry && typeof slotEntry === 'object' ? slotEntry.data : slotEntry;
|
|
341
|
+
if (slotValue != null) acc[slotKey] = slotValue;
|
|
342
|
+
return acc;
|
|
343
|
+
}, {})
|
|
344
|
+
: null;
|
|
345
|
+
const varMappedFromPayload = {
|
|
346
|
+
...(varMapFromTemplateVariableMapping || {}),
|
|
347
|
+
...(smsFallbackContent[RCS_SMS_FALLBACK_VAR_MAPPED_PROP] || {}),
|
|
348
|
+
};
|
|
304
349
|
const hasVarMapped = Object.keys(varMappedFromPayload).length > 0;
|
|
305
|
-
const templateConfigsRecord =
|
|
306
|
-
smsFallbackContent.templateConfigs && typeof smsFallbackContent.templateConfigs === 'object'
|
|
307
|
-
? smsFallbackContent.templateConfigs
|
|
308
|
-
: {};
|
|
309
350
|
const smsFallbackTemplateName =
|
|
310
351
|
smsFallbackContent.templateName
|
|
311
352
|
|| smsFallbackContent.smsTemplateName
|