@capillarytech/creatives-library 9.0.29-alpha.1 → 9.0.29-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/utils/rcsPayloadUtils.js +2 -5
- package/utils/tests/rcsPayloadUtils.test.js +2 -54
- package/v2Components/CapActionButton/constants.js +0 -1
- package/v2Components/CapActionButton/index.js +9 -76
- package/v2Components/CapActionButton/index.scss +0 -13
- package/v2Components/CapActionButton/messages.js +0 -13
- package/v2Components/CapActionButton/tests/index.test.js +1 -32
- package/v2Containers/Rcs/carouselUtils.js +79 -47
- package/v2Containers/Rcs/components/CarouselCard.js +9 -11
- package/v2Containers/Rcs/components/CarouselCardButtons.js +0 -20
- package/v2Containers/Rcs/constants.js +7 -2
- package/v2Containers/Rcs/index.js +151 -56
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +43 -5
- package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
- package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
- package/v2Containers/Rcs/tests/index.test.js +265 -84
- package/v2Containers/Rcs/tests/utils.test.js +80 -0
- package/v2Containers/Rcs/utils.js +63 -6
|
@@ -78,8 +78,13 @@ export const rcsVarTestRegex = /^\{\{[^}]+\}\}$/;
|
|
|
78
78
|
export const RCS_NUMERIC_VAR_TOKEN_REGEX = /\{\{(\d+)\}\}/g;
|
|
79
79
|
/** `cardVarMapped` slot keys that are numeric only (legacy ordering). */
|
|
80
80
|
export const RCS_NUMERIC_VAR_NAME_REGEX = /^\d+$/;
|
|
81
|
-
/**
|
|
82
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Semantic Liquid-style keys on RCS `cardVarMapped` (same class as `{{…}}` inner names in the
|
|
83
|
+
* editor). Carousel slots are now keyed by global position (see `computeCarouselVarSlotKeys`), a
|
|
84
|
+
* plain number, so this only needs to match bare semantic names — the optional `cN::` prefix is
|
|
85
|
+
* kept solely so `pickRcsCardVarMappedEntries` doesn't drop legacy scoped keys from older payloads.
|
|
86
|
+
*/
|
|
87
|
+
export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^(?:c\d+::)?[\w.()]+$/;
|
|
83
88
|
/** Escape all special RegExp characters in an arbitrary string before using it in `new RegExp(...)`. */
|
|
84
89
|
export const REGEX_SPECIAL_CHARS_ESCAPE_PATTERN = /[-/\\^$*+?.()|[\]{}]/g;
|
|
85
90
|
/** Entire string is wrapped in `{{…}}` mustache delimiters (allows any content including `}`). */
|
|
@@ -175,6 +175,7 @@ import {
|
|
|
175
175
|
getCarouselDescriptionCharacterCount,
|
|
176
176
|
buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
|
|
177
177
|
buildCarouselCardContentForPayload,
|
|
178
|
+
computeCarouselVarSlotKeys,
|
|
178
179
|
} from './carouselUtils';
|
|
179
180
|
import CarouselDimensionSelection from './components/CarouselDimensionSelection';
|
|
180
181
|
import CarouselCard from './components/CarouselCard';
|
|
@@ -270,7 +271,12 @@ export const Rcs = (props) => {
|
|
|
270
271
|
const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
|
|
271
272
|
const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
|
|
272
273
|
const [carouselResetNonce, setCarouselResetNonce] = useState(0);
|
|
273
|
-
|
|
274
|
+
// Title and description each track their own last-focused var slot (mirrors titleTextAreaId/
|
|
275
|
+
// descTextAreaId for the single-card flow) — a single shared id would let a click on the
|
|
276
|
+
// "Add Labels" picker next to one field silently apply to whichever field was focused last,
|
|
277
|
+
// if the user hadn't just clicked into that field's own var slot.
|
|
278
|
+
const [carouselTitleFocusedVarId, setCarouselTitleFocusedVarId] = useState('');
|
|
279
|
+
const [carouselDescFocusedVarId, setCarouselDescFocusedVarId] = useState('');
|
|
274
280
|
const [imageError, setImageError] = useState(null);
|
|
275
281
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
276
282
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
@@ -596,7 +602,8 @@ export const Rcs = (props) => {
|
|
|
596
602
|
}}
|
|
597
603
|
onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
|
|
598
604
|
onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
|
|
599
|
-
|
|
605
|
+
onTitleTagSelect={onCarouselTitleTagSelect}
|
|
606
|
+
onDescTagSelect={onCarouselDescTagSelect}
|
|
600
607
|
onContextChange={handleOnTagsContextChange}
|
|
601
608
|
renderEditMessage={renderCarouselEditMessage}
|
|
602
609
|
onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
|
|
@@ -655,7 +662,8 @@ export const Rcs = (props) => {
|
|
|
655
662
|
onChange={(key) => {
|
|
656
663
|
setActiveCarouselIndex(`${key}`);
|
|
657
664
|
// reset focused var when switching cards (as per requirement)
|
|
658
|
-
|
|
665
|
+
setCarouselTitleFocusedVarId('');
|
|
666
|
+
setCarouselDescFocusedVarId('');
|
|
659
667
|
}}
|
|
660
668
|
panes={getCarouselTabPanes()}
|
|
661
669
|
/>
|
|
@@ -775,7 +783,7 @@ export const Rcs = (props) => {
|
|
|
775
783
|
['title', 'description'].forEach((field) => {
|
|
776
784
|
const templateStr = card?.[field] || '';
|
|
777
785
|
if (!templateStr) return;
|
|
778
|
-
const resolved =
|
|
786
|
+
const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
|
|
779
787
|
if (!resolved) {
|
|
780
788
|
updateCarouselErrors(idx, { [field]: false });
|
|
781
789
|
return;
|
|
@@ -867,6 +875,30 @@ export const Rcs = (props) => {
|
|
|
867
875
|
}).join('');
|
|
868
876
|
};
|
|
869
877
|
|
|
878
|
+
/**
|
|
879
|
+
* Carousel resolve: each variable slot's key comes from `computeCarouselVarSlotKeys` (title,
|
|
880
|
+
* then description, per card; same name within a card shares one key; different cards never
|
|
881
|
+
* collide). A slot's key never depends on which tag currently fills it.
|
|
882
|
+
*/
|
|
883
|
+
const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
|
|
884
|
+
if (!str) return '';
|
|
885
|
+
const arr = splitTemplateVarStringRcs(str);
|
|
886
|
+
const fieldKeys = (isDescField
|
|
887
|
+
? computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.description
|
|
888
|
+
: computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex]?.title) ?? [];
|
|
889
|
+
let varOrdinal = 0;
|
|
890
|
+
return arr.map((elem) => {
|
|
891
|
+
if (rcsVarTestRegex.test(elem)) {
|
|
892
|
+
const slotKey = fieldKeys[varOrdinal];
|
|
893
|
+
varOrdinal += 1;
|
|
894
|
+
const slotValue = cardVarMapped?.[slotKey];
|
|
895
|
+
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
896
|
+
return String(slotValue);
|
|
897
|
+
}
|
|
898
|
+
return elem;
|
|
899
|
+
}).join('');
|
|
900
|
+
};
|
|
901
|
+
|
|
870
902
|
const buildCarouselCardsForPreview = (cards = []) =>
|
|
871
903
|
buildCarouselCardsForPreviewUtil(cards, { isFullMode, rcsVarRegex, resolveTemplateWithMap });
|
|
872
904
|
|
|
@@ -1214,6 +1246,19 @@ export const Rcs = (props) => {
|
|
|
1214
1246
|
setSelectedCarousel(`${firstHeight}_${cardWidth}`);
|
|
1215
1247
|
setActiveCarouselIndex('0');
|
|
1216
1248
|
|
|
1249
|
+
if (cards.length > 1) {
|
|
1250
|
+
// Each card's own `cardVarMapped` is already keyed by global var position (see
|
|
1251
|
+
// computeCarouselVarSlotKeys) — a slot's key is assigned once, from where its token
|
|
1252
|
+
// sits in the template text, and never depends on which tag fills it. So merging every
|
|
1253
|
+
// card's own map directly is correct and collision-free: no per-card coalescing,
|
|
1254
|
+
// normalizing, or re-scoping needed (unlike the old name-based scoped-key scheme).
|
|
1255
|
+
setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}) => {
|
|
1256
|
+
const rawCardMap = c?.cardVarMapped;
|
|
1257
|
+
if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
|
|
1258
|
+
return { ...merged, ...rawCardMap };
|
|
1259
|
+
}, { ...(previousVarMap || {}) }));
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1217
1262
|
const hydratedCards = cards.map((c = {}, idx) => {
|
|
1218
1263
|
const mediaType = c.mediaType;
|
|
1219
1264
|
const media = c.media || {};
|
|
@@ -1529,23 +1574,51 @@ export const Rcs = (props) => {
|
|
|
1529
1574
|
|
|
1530
1575
|
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
1531
1576
|
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1577
|
+
/** Field-local var ordinal (0-based, vars only) for a `{{tok}}_segIdx` composite id — scoped to
|
|
1578
|
+
* a single field's own template string (title and description are tracked independently). */
|
|
1579
|
+
const getCarouselFieldLocalVarOrdinal = (compositeId, fieldTemplateStr) => {
|
|
1580
|
+
const segments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
|
|
1581
|
+
let varOrdinal = 0;
|
|
1582
|
+
for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
|
|
1583
|
+
if (rcsVarTestRegex.test(segments[segmentIndex])) {
|
|
1584
|
+
if (`${segments[segmentIndex]}_${segmentIndex}` === compositeId) return varOrdinal;
|
|
1585
|
+
varOrdinal += 1;
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
return null;
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
// The card's placeholder (numeric {{N}} or a bare semantic token like {{first_name}}) is locked
|
|
1592
|
+
// here — picking a tag only supplies/replaces the *value* behind that slot in cardVarMapped, it
|
|
1593
|
+
// never rewrites the card's title/description text. `focusedVarId` is passed in by the caller
|
|
1594
|
+
// (title vs description each track their own last-focused slot, like titleTextAreaId/
|
|
1595
|
+
// descTextAreaId) so a click on one field's "Add Labels" can never apply to the other field's
|
|
1596
|
+
// last-focused slot. The slot's key is its global position across the whole carousel — assigned
|
|
1597
|
+
// once from where its token sits in the template text, never from whatever tag currently fills
|
|
1598
|
+
// it — so swapping tags never changes the key and never collides with another card's slot.
|
|
1599
|
+
const onCarouselTagSelect = (data, focusedVarId, isDescField) => {
|
|
1600
|
+
if (!focusedVarId) return;
|
|
1601
|
+
const activeIdx = parseInt(activeCarouselIndex, 10);
|
|
1602
|
+
const cardIndex = isNaN(activeIdx) ? 0 : activeIdx;
|
|
1603
|
+
const fieldTemplateStr = (isDescField
|
|
1604
|
+
? carouselData?.[cardIndex]?.description
|
|
1605
|
+
: carouselData?.[cardIndex]?.title) || '';
|
|
1606
|
+
const localVarOrdinal = getCarouselFieldLocalVarOrdinal(focusedVarId, fieldTemplateStr);
|
|
1607
|
+
if (localVarOrdinal == null) return;
|
|
1608
|
+
const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
1609
|
+
const slotKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
|
|
1610
|
+
if (!slotKey) return;
|
|
1611
|
+
|
|
1612
|
+
setCardVarMapped((prev) => ({
|
|
1613
|
+
...(prev || {}),
|
|
1614
|
+
[slotKey]: `{{${data}}}`,
|
|
1615
|
+
}));
|
|
1547
1616
|
};
|
|
1548
1617
|
|
|
1618
|
+
const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
|
|
1619
|
+
|
|
1620
|
+
const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
|
|
1621
|
+
|
|
1549
1622
|
//removing optout tag for rcs
|
|
1550
1623
|
const getRcsTags = () => {
|
|
1551
1624
|
const tempTags = cloneDeep(tags);
|
|
@@ -1631,7 +1704,7 @@ export const Rcs = (props) => {
|
|
|
1631
1704
|
key={`${elem}_${index}`}
|
|
1632
1705
|
placeholder={`enter the value for ${elem}`}
|
|
1633
1706
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1634
|
-
onChange={e => textAreaValueChange(e
|
|
1707
|
+
onChange={e => textAreaValueChange(e)}
|
|
1635
1708
|
value={textAreaValue(index, type)}
|
|
1636
1709
|
onFocus={(e) => setTextAreaId(e, type)}
|
|
1637
1710
|
/>
|
|
@@ -1844,26 +1917,39 @@ const onTitleAddVar = () => {
|
|
|
1844
1917
|
return "";
|
|
1845
1918
|
};
|
|
1846
1919
|
|
|
1847
|
-
// Carousel: render variable-value editor for a given template string (title/description)
|
|
1848
|
-
// This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1849
|
-
|
|
1920
|
+
// Carousel: render variable-value editor for a given template string (title/description) belonging
|
|
1921
|
+
// to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1922
|
+
// tokens are editable. Each slot's key comes from `computeCarouselVarSlotKeys` — stable regardless
|
|
1923
|
+
// of which tag fills it, and independent even from another occurrence of the same tag name.
|
|
1924
|
+
const renderCarouselEditMessage = (templateStr, cardIndex, fieldType) => {
|
|
1850
1925
|
const renderArray = [];
|
|
1851
1926
|
const templateArr = splitTemplateVarString(templateStr);
|
|
1927
|
+
const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
|
|
1928
|
+
const slotKeysForCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
1929
|
+
const fieldKeys = (isDescField ? slotKeysForCard?.description : slotKeysForCard?.title) ?? [];
|
|
1930
|
+
let varOrdinal = 0;
|
|
1852
1931
|
if (templateArr?.length) {
|
|
1853
1932
|
templateArr.forEach((elem, index) => {
|
|
1854
1933
|
if (rcsVarTestRegex.test(elem)) {
|
|
1855
1934
|
const varName = getVarNameFromToken(elem);
|
|
1935
|
+
const mapKey = fieldKeys[varOrdinal];
|
|
1936
|
+
varOrdinal += 1;
|
|
1937
|
+
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1856
1938
|
renderArray.push(
|
|
1857
1939
|
<div key={`${elem}_${index}`} className="var-segment-message-editor__var-slot">
|
|
1858
1940
|
<TextArea
|
|
1859
1941
|
id={`${elem}_${index}`}
|
|
1860
1942
|
placeholder={`enter the value for ${elem}`}
|
|
1861
1943
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1862
|
-
onChange={(e) =>
|
|
1863
|
-
value={
|
|
1944
|
+
onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
|
|
1945
|
+
value={computedValue}
|
|
1864
1946
|
onFocus={(e) => {
|
|
1865
1947
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1866
|
-
|
|
1948
|
+
if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
|
|
1949
|
+
setCarouselDescFocusedVarId(id);
|
|
1950
|
+
} else {
|
|
1951
|
+
setCarouselTitleFocusedVarId(id);
|
|
1952
|
+
}
|
|
1867
1953
|
}}
|
|
1868
1954
|
/>
|
|
1869
1955
|
</div>
|
|
@@ -1884,7 +1970,7 @@ const onTitleAddVar = () => {
|
|
|
1884
1970
|
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1885
1971
|
};
|
|
1886
1972
|
|
|
1887
|
-
const textAreaValueChange = (e
|
|
1973
|
+
const textAreaValueChange = (e) => {
|
|
1888
1974
|
const value = e?.target?.value ?? '';
|
|
1889
1975
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1890
1976
|
if (!id) return;
|
|
@@ -1895,13 +1981,38 @@ const onTitleAddVar = () => {
|
|
|
1895
1981
|
const variableName = getVarNameFromToken(token);
|
|
1896
1982
|
|
|
1897
1983
|
if (variableName) {
|
|
1984
|
+
const nextVal = isInvalidValue ? "" : value;
|
|
1898
1985
|
setCardVarMapped((prev) => ({
|
|
1899
1986
|
...prev,
|
|
1900
|
-
[variableName]:
|
|
1987
|
+
[variableName]: nextVal,
|
|
1901
1988
|
}));
|
|
1902
1989
|
}
|
|
1903
1990
|
};
|
|
1904
1991
|
|
|
1992
|
+
// Carousel: typing directly into a slot's value box (as opposed to picking a tag) — same global
|
|
1993
|
+
// positional key as onCarouselTagSelect/renderCarouselEditMessage, so both ways of filling a
|
|
1994
|
+
// slot land on the identical key.
|
|
1995
|
+
const carouselTextAreaValueChange = (e, cardIndex, fieldType) => {
|
|
1996
|
+
const value = e?.target?.value ?? '';
|
|
1997
|
+
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1998
|
+
if (!id) return;
|
|
1999
|
+
const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
|
|
2000
|
+
const fieldTemplateStr = (isDescField
|
|
2001
|
+
? carouselData?.[cardIndex]?.description
|
|
2002
|
+
: carouselData?.[cardIndex]?.title) || '';
|
|
2003
|
+
const localVarOrdinal = getCarouselFieldLocalVarOrdinal(id, fieldTemplateStr);
|
|
2004
|
+
if (localVarOrdinal == null) return;
|
|
2005
|
+
const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
|
|
2006
|
+
const mapKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
|
|
2007
|
+
if (!mapKey) return;
|
|
2008
|
+
const isInvalidValue = value?.trim() === "";
|
|
2009
|
+
const nextVal = isInvalidValue ? "" : value;
|
|
2010
|
+
setCardVarMapped((prev) => ({
|
|
2011
|
+
...prev,
|
|
2012
|
+
[mapKey]: nextVal,
|
|
2013
|
+
}));
|
|
2014
|
+
};
|
|
2015
|
+
|
|
1905
2016
|
const setTextAreaId = (e, type) => {
|
|
1906
2017
|
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
1907
2018
|
// have an `.target.id` shape. Support both.
|
|
@@ -2749,11 +2860,8 @@ const onTitleAddVar = () => {
|
|
|
2749
2860
|
// user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2750
2861
|
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2751
2862
|
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2752
|
-
//
|
|
2753
|
-
//
|
|
2754
|
-
// Without the offset, every card restarts at slotKey="1" and resolves against card 0's
|
|
2755
|
-
// mappings, causing tags from card 1+ to be missing or wrong.
|
|
2756
|
-
let carouselSlotOffset = 0;
|
|
2863
|
+
// Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
|
|
2864
|
+
// never share a key even when they pick the same tag name for their own slot.
|
|
2757
2865
|
rcsForTest = {
|
|
2758
2866
|
...rcsForTest,
|
|
2759
2867
|
rcsContent: {
|
|
@@ -2761,11 +2869,8 @@ const onTitleAddVar = () => {
|
|
|
2761
2869
|
cardContent: cardContent.map((card) => {
|
|
2762
2870
|
const rawTitle = card.title || '';
|
|
2763
2871
|
const rawDesc = card.description || '';
|
|
2764
|
-
const
|
|
2765
|
-
const
|
|
2766
|
-
const resolvedTitle = resolveTemplateWithMap(rawTitle, carouselSlotOffset);
|
|
2767
|
-
const resolvedDesc = resolveTemplateWithMap(rawDesc, carouselSlotOffset + titleVarCount);
|
|
2768
|
-
carouselSlotOffset += titleVarCount + descVarCount;
|
|
2872
|
+
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
|
|
2873
|
+
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
|
|
2769
2874
|
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2770
2875
|
}),
|
|
2771
2876
|
},
|
|
@@ -2955,12 +3060,16 @@ const onTitleAddVar = () => {
|
|
|
2955
3060
|
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
2956
3061
|
);
|
|
2957
3062
|
if (hasEmptyField) return true;
|
|
2958
|
-
const
|
|
3063
|
+
const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
|
|
3064
|
+
const unfilledVar = (carouselData || []).some((card, cardIndex) =>
|
|
2959
3065
|
['title', 'description'].some((field) => {
|
|
3066
|
+
const isDescField = field === 'description';
|
|
2960
3067
|
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3068
|
+
if (tokens.length === 0) return false;
|
|
3069
|
+
const fieldKeys = (isDescField ? slotKeysByCard[cardIndex]?.description : slotKeysByCard[cardIndex]?.title) ?? [];
|
|
3070
|
+
return tokens.some((token, localVarOrdinal) => {
|
|
3071
|
+
const slotKey = fieldKeys[localVarOrdinal];
|
|
3072
|
+
const slotValue = cardVarMapped?.[slotKey];
|
|
2964
3073
|
return slotValue == null || String(slotValue).trim() === '';
|
|
2965
3074
|
});
|
|
2966
3075
|
})
|
|
@@ -3053,20 +3162,6 @@ const onTitleAddVar = () => {
|
|
|
3053
3162
|
return true;
|
|
3054
3163
|
}
|
|
3055
3164
|
}
|
|
3056
|
-
// Mirror WhatsApp's isEditDoneDisabled: block Done while a CTA button's dynamic URL still
|
|
3057
|
-
// holds the unresolved {{1}} placeholder (no real personalization tag assigned yet).
|
|
3058
|
-
const hasUnresolvedDynamicCtaUrl = isCarouselType
|
|
3059
|
-
? (carouselData || []).some((card) =>
|
|
3060
|
-
(card?.suggestions || []).some((suggestion) =>
|
|
3061
|
-
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3062
|
-
)
|
|
3063
|
-
)
|
|
3064
|
-
: suggestions.some((suggestion) =>
|
|
3065
|
-
suggestion?.type === CTA && (suggestion?.url || '').includes('{{1}}')
|
|
3066
|
-
);
|
|
3067
|
-
if (hasUnresolvedDynamicCtaUrl) {
|
|
3068
|
-
return true;
|
|
3069
|
-
}
|
|
3070
3165
|
if (templateTitleError || templateDescError) {
|
|
3071
3166
|
return true;
|
|
3072
3167
|
}
|
|
@@ -396,6 +396,7 @@ export function syncCardVarMappedSemanticsFromSlots(
|
|
|
396
396
|
templateDesc,
|
|
397
397
|
rcsVarRegex,
|
|
398
398
|
) {
|
|
399
|
+
console.log('syncCardVarMappedSemanticsFromSlots', cardVarMappedInput, templateTitle, templateDesc);
|
|
399
400
|
const cardVarMappedSynced =
|
|
400
401
|
cardVarMappedInput != null && typeof cardVarMappedInput === 'object'
|
|
401
402
|
? { ...cardVarMappedInput }
|
|
@@ -408,11 +409,48 @@ export function syncCardVarMappedSemanticsFromSlots(
|
|
|
408
409
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
409
410
|
const semanticVarName = getVarNameFromToken(token);
|
|
410
411
|
if (!semanticVarName) return;
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
412
|
+
|
|
413
|
+
if (!RCS_NUMERIC_VAR_NAME_REGEX.test(semanticVarName)) {
|
|
414
|
+
// A numeric slot may already self-reference this exact tag (e.g. "4" -> "{{last_name}}") when
|
|
415
|
+
// the template's own numeric placeholder has since been resolved to this tag in the text —
|
|
416
|
+
// that numeric key is the source of truth, not this token's position.
|
|
417
|
+
const mustacheSelfReference = `{{${semanticVarName}}}`;
|
|
418
|
+
const existingNumericKeyForSemantic = Object.keys(cardVarMappedSynced).find((key) => (
|
|
419
|
+
RCS_NUMERIC_VAR_NAME_REGEX.test(key) && cardVarMappedSynced[key] === mustacheSelfReference
|
|
420
|
+
));
|
|
421
|
+
if (existingNumericKeyForSemantic) {
|
|
422
|
+
const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
423
|
+
const numericValueTrimmed = String(cardVarMappedSynced[existingNumericKeyForSemantic] ?? '').trim();
|
|
424
|
+
if (!semanticValueTrimmed && numericValueTrimmed) {
|
|
425
|
+
cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[existingNumericKeyForSemantic];
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Legacy: template embeds the semantic token directly ({{user_name}}), but older payloads
|
|
431
|
+
// stored the resolved value under a position-based numeric key ("1", "2", …).
|
|
432
|
+
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
433
|
+
const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
434
|
+
const numericSlotValueTrimmed = String(cardVarMappedSynced[numericSlotKey] ?? '').trim();
|
|
435
|
+
if (!semanticValueTrimmed && numericSlotValueTrimmed) {
|
|
436
|
+
cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[numericSlotKey];
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Carousel numeric placeholder ({{3}}, {{4}}, …): the slot key IS the token digit itself —
|
|
442
|
+
// numbers are allocated globally across the whole carousel (getNextCarouselVarToken), not
|
|
443
|
+
// re-numbered per card, so a position-based key here would target the wrong slot. Its value
|
|
444
|
+
// may be a mustache-wrapped tag reference (e.g. "3" -> "{{last_name}}") — mirror that onto the
|
|
445
|
+
// bare semantic key too so VarSegment editors keyed by tag name also prepopulate.
|
|
446
|
+
const slotValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
447
|
+
if (!slotValueTrimmed) return;
|
|
448
|
+
const mustacheInnerMatch = slotValueTrimmed.match(/^\{\{([^}]+)\}\}$/);
|
|
449
|
+
const innerSemanticName = mustacheInnerMatch?.[1]?.trim();
|
|
450
|
+
if (!innerSemanticName || innerSemanticName === semanticVarName) return;
|
|
451
|
+
const existingSemanticValue = String(cardVarMappedSynced[innerSemanticName] ?? '').trim();
|
|
452
|
+
if (!existingSemanticValue) {
|
|
453
|
+
cardVarMappedSynced[innerSemanticName] = cardVarMappedSynced[semanticVarName];
|
|
416
454
|
}
|
|
417
455
|
});
|
|
418
456
|
return cardVarMappedSynced;
|
|
@@ -220,7 +220,8 @@ const baseProps = {
|
|
|
220
220
|
onDescriptionChange: jest.fn(),
|
|
221
221
|
onTitleAddVar: jest.fn(),
|
|
222
222
|
onDescriptionAddVar: jest.fn(),
|
|
223
|
-
|
|
223
|
+
onTitleTagSelect: jest.fn(),
|
|
224
|
+
onDescTagSelect: jest.fn(),
|
|
224
225
|
onContextChange: jest.fn(),
|
|
225
226
|
renderEditMessage: (val) => <div data-testid="render-edit-message">{val}</div>,
|
|
226
227
|
onFieldChange: jest.fn(),
|
|
@@ -588,8 +588,8 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
588
588
|
expect(out[0].bodyText).toBe('Bye {{b}}');
|
|
589
589
|
});
|
|
590
590
|
|
|
591
|
-
it('resolves title/description through
|
|
592
|
-
const
|
|
591
|
+
it('resolves title/description through resolveCarouselTemplateWithMap, scoped per card index and field', () => {
|
|
592
|
+
const resolveCarouselTemplateWithMap = jest.fn((str, cardIndex, isDescField) => `R(${str}|${cardIndex}|${isDescField})`);
|
|
593
593
|
const out = buildCarouselCardsForPreview(
|
|
594
594
|
[
|
|
595
595
|
{ title: 'Hi {{a}}', description: 'Bye {{b}} {{c}}' },
|
|
@@ -597,14 +597,14 @@ describe('buildCarouselCardsForPreview', () => {
|
|
|
597
597
|
],
|
|
598
598
|
{ isFullMode: false, rcsVarRegex, resolveTemplateWithMap },
|
|
599
599
|
);
|
|
600
|
-
expect(
|
|
601
|
-
expect(
|
|
602
|
-
expect(
|
|
603
|
-
expect(
|
|
604
|
-
expect(out[0].title).toBe('R(Hi {{a}}|0)');
|
|
605
|
-
expect(out[0].bodyText).toBe('R(Bye {{b}} {{c}}|
|
|
606
|
-
expect(out[1].title).toBe('R(Solo {{d}}|
|
|
607
|
-
expect(out[1].bodyText).toBe('R(none|
|
|
600
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(1, 'Hi {{a}}', 0, false);
|
|
601
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(2, 'Bye {{b}} {{c}}', 0, true);
|
|
602
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(3, 'Solo {{d}}', 1, false);
|
|
603
|
+
expect(resolveCarouselTemplateWithMap).toHaveBeenNthCalledWith(4, 'none', 1, true);
|
|
604
|
+
expect(out[0].title).toBe('R(Hi {{a}}|0|false)');
|
|
605
|
+
expect(out[0].bodyText).toBe('R(Bye {{b}} {{c}}|0|true)');
|
|
606
|
+
expect(out[1].title).toBe('R(Solo {{d}}|1|false)');
|
|
607
|
+
expect(out[1].bodyText).toBe('R(none|1|true)');
|
|
608
608
|
});
|
|
609
609
|
|
|
610
610
|
it('defaults missing card fields, including a default {} for a missing array item', () => {
|
|
@@ -857,17 +857,17 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
857
857
|
expect(out[0].cardVarMapped).toBeUndefined();
|
|
858
858
|
});
|
|
859
859
|
|
|
860
|
-
it('builds cardVarMapped from title and description tokens, sanitizing values', () => {
|
|
860
|
+
it('builds cardVarMapped from title and description tokens, keyed by global position, sanitizing values', () => {
|
|
861
861
|
const out = buildCarouselCardContentForPayload(
|
|
862
862
|
[{ title: 'Hi {{name}}', description: 'Pts {{points}}', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
863
863
|
{
|
|
864
864
|
isSlotMappingMode: true,
|
|
865
|
-
cardVarMapped: {
|
|
865
|
+
cardVarMapped: { 1: 'Bob', 2: '{{2}}' },
|
|
866
866
|
selectedCarouselHeight: 'SHORT',
|
|
867
867
|
rcsVarRegex,
|
|
868
868
|
},
|
|
869
869
|
);
|
|
870
|
-
expect(out[0].cardVarMapped).toEqual({
|
|
870
|
+
expect(out[0].cardVarMapped).toEqual({ 1: 'Bob', 2: '' });
|
|
871
871
|
});
|
|
872
872
|
|
|
873
873
|
it('defaults an unmapped token value to empty string via sanitizeCardVarMappedValue', () => {
|
|
@@ -875,7 +875,7 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
875
875
|
[{ title: 'Hi {{missing}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
876
876
|
{ isSlotMappingMode: true, cardVarMapped: {}, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
877
877
|
);
|
|
878
|
-
expect(out[0].cardVarMapped).toEqual({
|
|
878
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
879
879
|
});
|
|
880
880
|
|
|
881
881
|
it('handles a missing cardVarMapped object entirely', () => {
|
|
@@ -883,7 +883,7 @@ describe('buildCarouselCardContentForPayload', () => {
|
|
|
883
883
|
[{ title: 'Hi {{name}}', description: 'D', mediaType: RCS_MEDIA_TYPES.NONE }],
|
|
884
884
|
{ isSlotMappingMode: true, cardVarMapped: undefined, selectedCarouselHeight: 'SHORT', rcsVarRegex },
|
|
885
885
|
);
|
|
886
|
-
expect(out[0].cardVarMapped).toEqual({
|
|
886
|
+
expect(out[0].cardVarMapped).toEqual({ 1: '' });
|
|
887
887
|
});
|
|
888
888
|
|
|
889
889
|
it('skips a matched token that strips down to an empty variable name', () => {
|