@capillarytech/creatives-library 9.0.35-alpha.0 → 9.0.35
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 -28
- 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 +165 -102
- 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,12 +1474,6 @@ export const Rcs = (props) => {
|
|
|
1426
1474
|
globalActions.fetchSchemaForEntity(query);
|
|
1427
1475
|
};
|
|
1428
1476
|
|
|
1429
|
-
const replaceNumericPlaceholderWithTagInTemplate = (templateStr, numericVarName, tagName) => {
|
|
1430
|
-
if (!templateStr || !numericVarName || !tagName) return templateStr;
|
|
1431
|
-
const placeholderRegex = buildRcsNumericMustachePlaceholderRegex(numericVarName);
|
|
1432
|
-
return templateStr.replace(placeholderRegex, `{{${tagName}}}`);
|
|
1433
|
-
};
|
|
1434
|
-
|
|
1435
1477
|
/**
|
|
1436
1478
|
* Replaces only the var token at `slotOrdinalZeroBased` (the field-local ordinal among
|
|
1437
1479
|
* `{{...}}` tokens, left to right) with `{{tagName}}`. Unlike a name-based regex replace,
|
|
@@ -1484,16 +1526,15 @@ export const Rcs = (props) => {
|
|
|
1484
1526
|
setCardVarMapped((previousCardVarMapped) => {
|
|
1485
1527
|
const updatedCardVarMapped = { ...(previousCardVarMapped || {}) };
|
|
1486
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.
|
|
1487
1531
|
const existingValueBeforeAppend = (
|
|
1488
1532
|
previousCardVarMapped?.[semanticOrNumericVarName] ?? ''
|
|
1489
1533
|
).toString();
|
|
1490
1534
|
const mappedValueAfterAppendingTag = `${existingValueBeforeAppend}{{${selectedTagNameFromPicker}}}`;
|
|
1491
|
-
|
|
1492
|
-
updatedCardVarMapped[selectedTagNameFromPicker] = mappedValueAfterAppendingTag;
|
|
1535
|
+
updatedCardVarMapped[semanticOrNumericVarName] = mappedValueAfterAppendingTag;
|
|
1493
1536
|
} else {
|
|
1494
|
-
|
|
1495
|
-
// "existing value" — that appends the new tag onto the other field. Match handleRcsVarChange:
|
|
1496
|
-
// read/write the global numeric slot only and drop the shared semantic key.
|
|
1537
|
+
|
|
1497
1538
|
const existingValueBeforeAppend = cardVarMappedNumericSlotKey
|
|
1498
1539
|
? String(previousCardVarMapped?.[cardVarMappedNumericSlotKey] ?? '')
|
|
1499
1540
|
: String(previousCardVarMapped?.[semanticOrNumericVarName] ?? '');
|
|
@@ -1522,10 +1563,15 @@ export const Rcs = (props) => {
|
|
|
1522
1563
|
return updatedCardVarMapped;
|
|
1523
1564
|
});
|
|
1524
1565
|
|
|
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
|
+
|
|
1525
1571
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE || tagAreaField === RCS_TAG_AREA_FIELD_DESC) {
|
|
1526
|
-
//
|
|
1527
|
-
//
|
|
1528
|
-
//
|
|
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.
|
|
1529
1575
|
const fieldOffsetForSlot = tagAreaField === RCS_TAG_AREA_FIELD_TITLE
|
|
1530
1576
|
? 0
|
|
1531
1577
|
: (templateTitle?.match(rcsVarRegex) ?? []).length;
|
|
@@ -1535,32 +1581,20 @@ export const Rcs = (props) => {
|
|
|
1535
1581
|
|
|
1536
1582
|
if (tagAreaField === RCS_TAG_AREA_FIELD_TITLE) {
|
|
1537
1583
|
setTemplateTitle((previousTitle) => {
|
|
1538
|
-
const titleAfterReplacingTag =
|
|
1539
|
-
? replaceNumericPlaceholderWithTagInTemplate(
|
|
1540
|
-
previousTitle || '',
|
|
1541
|
-
semanticOrNumericVarName,
|
|
1542
|
-
selectedTagNameFromPicker,
|
|
1543
|
-
)
|
|
1544
|
-
: replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1584
|
+
const titleAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousTitle || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1545
1585
|
if (titleAfterReplacingTag === previousTitle) return previousTitle;
|
|
1546
|
-
setTemplateTitleError(
|
|
1586
|
+
setTemplateTitleError(computeTemplateTitleError(titleAfterReplacingTag));
|
|
1547
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
|
|
1548
1588
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1549
1589
|
return titleAfterReplacingTag;
|
|
1550
1590
|
});
|
|
1551
1591
|
} else {
|
|
1552
1592
|
setTemplateDesc((previousDescription) => {
|
|
1553
|
-
const descriptionAfterReplacingTag =
|
|
1554
|
-
? replaceNumericPlaceholderWithTagInTemplate(
|
|
1555
|
-
previousDescription || '',
|
|
1556
|
-
semanticOrNumericVarName,
|
|
1557
|
-
selectedTagNameFromPicker,
|
|
1558
|
-
)
|
|
1559
|
-
: replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1593
|
+
const descriptionAfterReplacingTag = replaceTagAtFieldSlotOrdinal(previousDescription || '', localSlotOrdinal, selectedTagNameFromPicker);
|
|
1560
1594
|
if (descriptionAfterReplacingTag === previousDescription) {
|
|
1561
1595
|
return previousDescription;
|
|
1562
1596
|
}
|
|
1563
|
-
setTemplateDescError(
|
|
1597
|
+
setTemplateDescError(computeTemplateDescError(descriptionAfterReplacingTag));
|
|
1564
1598
|
setRcsVarSegmentEditorRemountKey((remountKey) => remountKey + 1);
|
|
1565
1599
|
return descriptionAfterReplacingTag;
|
|
1566
1600
|
});
|
|
@@ -1579,12 +1613,14 @@ export const Rcs = (props) => {
|
|
|
1579
1613
|
const token = carouselFocusedVarId.slice(0, sep);
|
|
1580
1614
|
const variableName = getVarNameFromToken(token);
|
|
1581
1615
|
if (!variableName) return;
|
|
1616
|
+
const activeIdx = parseInt(activeCarouselIndex, 10);
|
|
1617
|
+
const mapKey = getCarouselVarMapKey(isNaN(activeIdx) ? 0 : activeIdx, variableName);
|
|
1582
1618
|
setCardVarMapped((prev) => {
|
|
1583
|
-
const base = (prev?.[
|
|
1619
|
+
const base = (prev?.[mapKey] ?? '').toString();
|
|
1584
1620
|
const nextVal = `${base}{{${data}}}`;
|
|
1585
1621
|
return {
|
|
1586
1622
|
...(prev || {}),
|
|
1587
|
-
[
|
|
1623
|
+
[mapKey]: nextVal,
|
|
1588
1624
|
};
|
|
1589
1625
|
});
|
|
1590
1626
|
};
|
|
@@ -1696,31 +1732,13 @@ export const Rcs = (props) => {
|
|
|
1696
1732
|
return renderArray;
|
|
1697
1733
|
};
|
|
1698
1734
|
const onTemplateTitleChange = ({ target: { value } }) => {
|
|
1699
|
-
let errorMessage = false;
|
|
1700
|
-
if (templateType === contentType.rich_card && !value.trim()) {
|
|
1701
|
-
errorMessage = formatMessage(messages.emptyTemplateTitleErrorMessage);
|
|
1702
|
-
} else if (value.length > TEMPLATE_TITLE_MAX_LENGTH) {
|
|
1703
|
-
errorMessage = formatMessage(messages.templateHeaderLengthError);
|
|
1704
|
-
} else {
|
|
1705
|
-
errorMessage = variableErrorHandling(value);
|
|
1706
|
-
}
|
|
1707
1735
|
setTemplateTitle(value);
|
|
1708
|
-
setTemplateTitleError(
|
|
1736
|
+
setTemplateTitleError(computeTemplateTitleError(value));
|
|
1709
1737
|
};
|
|
1710
1738
|
|
|
1711
1739
|
const onTemplateDescChange = ({ target: { value } }) => {
|
|
1712
|
-
let errorMessage = false;
|
|
1713
|
-
if(templateType === contentType.text_message && value?.length > (isHostInfoBip ? RCS_TEXT_MESSAGE_MAX_LENGTH_INFOBIP : RCS_TEXT_MESSAGE_MAX_LENGTH)){
|
|
1714
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1715
|
-
} else if(templateType === contentType.rich_card && value?.length > RCS_RICH_CARD_MAX_LENGTH){
|
|
1716
|
-
errorMessage = formatMessage(messages.templateMessageLengthError);
|
|
1717
|
-
} else {
|
|
1718
|
-
errorMessage = false;
|
|
1719
|
-
}
|
|
1720
|
-
const varError = variableErrorHandling(value);
|
|
1721
|
-
const error = errorMessage || varError;
|
|
1722
1740
|
setTemplateDesc(value);
|
|
1723
|
-
setTemplateDescError(
|
|
1741
|
+
setTemplateDescError(computeTemplateDescError(value));
|
|
1724
1742
|
};
|
|
1725
1743
|
|
|
1726
1744
|
|
|
@@ -1803,6 +1821,29 @@ export const Rcs = (props) => {
|
|
|
1803
1821
|
return false;
|
|
1804
1822
|
};
|
|
1805
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
|
+
|
|
1806
1847
|
const onMessageAddVar = () => {
|
|
1807
1848
|
onAddVar(templateDesc);
|
|
1808
1849
|
};
|
|
@@ -1887,23 +1928,30 @@ const onTitleAddVar = () => {
|
|
|
1887
1928
|
return "";
|
|
1888
1929
|
};
|
|
1889
1930
|
|
|
1890
|
-
// Carousel: render variable-value editor for a given template string (title/description)
|
|
1891
|
-
// This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1892
|
-
|
|
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) => {
|
|
1893
1936
|
const renderArray = [];
|
|
1894
1937
|
const templateArr = splitTemplateVarString(templateStr);
|
|
1895
1938
|
if (templateArr?.length) {
|
|
1896
1939
|
templateArr.forEach((elem, index) => {
|
|
1897
1940
|
if (rcsVarTestRegex.test(elem)) {
|
|
1898
1941
|
const varName = getVarNameFromToken(elem);
|
|
1942
|
+
const mapKey = getCarouselVarMapKey(cardIndex, varName);
|
|
1943
|
+
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1899
1944
|
renderArray.push(
|
|
1900
|
-
<div
|
|
1945
|
+
<div
|
|
1946
|
+
key={`${elem}_${index}_${rcsVarSegmentEditorRemountKey}`}
|
|
1947
|
+
className="var-segment-message-editor__var-slot"
|
|
1948
|
+
>
|
|
1901
1949
|
<TextArea
|
|
1902
1950
|
id={`${elem}_${index}`}
|
|
1903
1951
|
placeholder={`enter the value for ${elem}`}
|
|
1904
1952
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1905
|
-
onChange={(e) => textAreaValueChange(e,
|
|
1906
|
-
value={
|
|
1953
|
+
onChange={(e) => textAreaValueChange(e, cardIndex)}
|
|
1954
|
+
value={computedValue}
|
|
1907
1955
|
onFocus={(e) => {
|
|
1908
1956
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1909
1957
|
setCarouselFocusedVarId(id);
|
|
@@ -1927,7 +1975,7 @@ const onTitleAddVar = () => {
|
|
|
1927
1975
|
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1928
1976
|
};
|
|
1929
1977
|
|
|
1930
|
-
const textAreaValueChange = (e,
|
|
1978
|
+
const textAreaValueChange = (e, cardIndex) => {
|
|
1931
1979
|
const value = e?.target?.value ?? '';
|
|
1932
1980
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1933
1981
|
if (!id) return;
|
|
@@ -1938,9 +1986,10 @@ const onTitleAddVar = () => {
|
|
|
1938
1986
|
const variableName = getVarNameFromToken(token);
|
|
1939
1987
|
|
|
1940
1988
|
if (variableName) {
|
|
1989
|
+
const mapKey = getCarouselVarMapKey(cardIndex, variableName);
|
|
1941
1990
|
setCardVarMapped((prev) => ({
|
|
1942
1991
|
...prev,
|
|
1943
|
-
[
|
|
1992
|
+
[mapKey]: isInvalidValue ? "" : value,
|
|
1944
1993
|
}));
|
|
1945
1994
|
}
|
|
1946
1995
|
};
|
|
@@ -2608,8 +2657,6 @@ const onTitleAddVar = () => {
|
|
|
2608
2657
|
const carouselVidDims =
|
|
2609
2658
|
RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS[carouselDimKey]
|
|
2610
2659
|
|| RCS_CAROUSEL_VIDEO_THUMBNAIL_DIMENSIONS.MEDIUM_MEDIUM;
|
|
2611
|
-
// Debug log for embedded/library mode preview payload (carousel)
|
|
2612
|
-
// eslint-disable-next-line no-console
|
|
2613
2660
|
return (
|
|
2614
2661
|
<UnifiedPreview
|
|
2615
2662
|
channel={RCS}
|
|
@@ -2793,46 +2840,55 @@ const onTitleAddVar = () => {
|
|
|
2793
2840
|
const cardContent = rcsForTest.rcsContent?.cardContent;
|
|
2794
2841
|
if (Array.isArray(cardContent) && cardContent[0]) {
|
|
2795
2842
|
if (isCarouselType) {
|
|
2796
|
-
// Carousel: resolve {{N}} slot tokens to the actual tag expressions / static values
|
|
2797
|
-
// 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:
|
|
2798
2845
|
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2799
2846
|
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2800
|
-
//
|
|
2801
|
-
//
|
|
2802
|
-
//
|
|
2803
|
-
// mappings, causing tags from card 1+ to be missing or wrong.
|
|
2804
|
-
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).
|
|
2805
2850
|
rcsForTest = {
|
|
2806
2851
|
...rcsForTest,
|
|
2807
2852
|
rcsContent: {
|
|
2808
2853
|
...rcsForTest.rcsContent,
|
|
2809
|
-
cardContent: cardContent.map((card) => {
|
|
2854
|
+
cardContent: cardContent.map((card, cardIndex) => {
|
|
2810
2855
|
const rawTitle = card.title || '';
|
|
2811
2856
|
const rawDesc = card.description || '';
|
|
2812
|
-
const
|
|
2813
|
-
const
|
|
2814
|
-
const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
|
|
2815
|
-
const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
|
|
2816
|
-
carouselSlotOffset += titleVarCount + descVarCount;
|
|
2857
|
+
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
|
|
2858
|
+
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
|
|
2817
2859
|
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2818
2860
|
}),
|
|
2819
2861
|
},
|
|
2820
2862
|
};
|
|
2821
2863
|
} else if (isSlotMappingModeForPreview) {
|
|
2822
2864
|
// Standalone card: coalesce cardVarMapped with the template's slot names so the preview
|
|
2823
|
-
// 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.
|
|
2824
2870
|
const fullCardVarMapped = coalesceCardVarMappedToTemplate(
|
|
2825
2871
|
pickRcsCardVarMappedEntries(cardVarMapped),
|
|
2826
2872
|
templateTitle,
|
|
2827
2873
|
templateDesc,
|
|
2828
2874
|
rcsVarRegex,
|
|
2829
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);
|
|
2830
2881
|
rcsForTest = {
|
|
2831
2882
|
...rcsForTest,
|
|
2832
2883
|
rcsContent: {
|
|
2833
2884
|
...rcsForTest.rcsContent,
|
|
2834
2885
|
cardContent: [
|
|
2835
|
-
{
|
|
2886
|
+
{
|
|
2887
|
+
...cardContent[0],
|
|
2888
|
+
title: resolvedStandaloneTitle,
|
|
2889
|
+
description: resolvedStandaloneDesc,
|
|
2890
|
+
cardVarMapped: fullCardVarMapped,
|
|
2891
|
+
},
|
|
2836
2892
|
...cardContent.slice(1),
|
|
2837
2893
|
],
|
|
2838
2894
|
},
|
|
@@ -2848,16 +2904,23 @@ const onTitleAddVar = () => {
|
|
|
2848
2904
|
},
|
|
2849
2905
|
},
|
|
2850
2906
|
};
|
|
2851
|
-
|
|
2852
|
-
//
|
|
2853
|
-
//
|
|
2854
|
-
//
|
|
2855
|
-
//
|
|
2856
|
-
|
|
2857
|
-
|
|
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) {
|
|
2858
2919
|
out.templateConfigs = {
|
|
2859
|
-
...smsFallbackTcFromPayload,
|
|
2860
|
-
|
|
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,
|
|
2861
2924
|
};
|
|
2862
2925
|
}
|
|
2863
2926
|
return out;
|
|
@@ -3003,12 +3066,12 @@ const onTitleAddVar = () => {
|
|
|
3003
3066
|
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
3004
3067
|
);
|
|
3005
3068
|
if (hasEmptyField) return true;
|
|
3006
|
-
const unfilledVar = (carouselData || []).some((card) =>
|
|
3069
|
+
const unfilledVar = (carouselData || []).some((card, cardIndex) =>
|
|
3007
3070
|
['title', 'description'].some((field) => {
|
|
3008
3071
|
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
3009
3072
|
return tokens.some((token) => {
|
|
3010
3073
|
const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
3011
|
-
const slotValue = cardVarMapped?.[name];
|
|
3074
|
+
const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, name)];
|
|
3012
3075
|
return slotValue == null || String(slotValue).trim() === '';
|
|
3013
3076
|
});
|
|
3014
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
|