@capillarytech/creatives-library 9.0.38 → 9.0.39

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.
Files changed (39) hide show
  1. package/package.json +1 -1
  2. package/translations/en.json +2 -2
  3. package/utils/permissions.js +59 -0
  4. package/utils/tests/permissions.test.js +116 -0
  5. package/v2Components/AccessForbidden/_accessForbidden.scss +14 -0
  6. package/v2Components/AccessForbidden/index.js +5 -3
  7. package/v2Components/AccessForbidden/messages.js +3 -2
  8. package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
  9. package/v2Components/NoPermissionWrapper/index.js +26 -0
  10. package/v2Containers/App/constants.js +5 -0
  11. package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
  12. package/v2Containers/Assets/Gallery/index.js +34 -10
  13. package/v2Containers/Cap/index.js +5 -2
  14. package/v2Containers/Cap/messages.js +4 -0
  15. package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
  16. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
  17. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
  18. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
  19. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
  20. package/v2Containers/Rcs/carouselUtils.js +46 -73
  21. package/v2Containers/Rcs/components/CarouselCard.js +6 -9
  22. package/v2Containers/Rcs/constants.js +2 -7
  23. package/v2Containers/Rcs/index.js +72 -126
  24. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +5 -42
  25. package/v2Containers/Rcs/tests/CarouselCard.test.js +1 -2
  26. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
  27. package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
  28. package/v2Containers/Rcs/tests/index.test.js +10 -232
  29. package/v2Containers/Rcs/tests/utils.test.js +0 -17
  30. package/v2Containers/Rcs/utils.js +12 -51
  31. package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
  32. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
  33. package/v2Containers/Templates/ChannelTypeIllustration.js +13 -2
  34. package/v2Containers/Templates/_templates.scss +6 -0
  35. package/v2Containers/Templates/index.js +141 -100
  36. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +1 -1
  37. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +174 -121
  38. package/v2Containers/TemplatesV2/index.js +16 -5
  39. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
@@ -15,7 +15,6 @@ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
15
15
  import TagList from '../../TagList';
16
16
  import globalMessages from '../../Cap/messages';
17
17
  import messages from '../messages';
18
- import { RCS_TAG_AREA_FIELD_TITLE, RCS_TAG_AREA_FIELD_DESC } from '../constants';
19
18
  import CarouselCardMedia from './CarouselCardMedia';
20
19
  import CarouselCardButtons from './CarouselCardButtons';
21
20
  import CarouselCharacterCount from './CarouselCharacterCount';
@@ -51,8 +50,7 @@ const CarouselCard = ({
51
50
  onDescriptionChange,
52
51
  onTitleAddVar,
53
52
  onDescriptionAddVar,
54
- onTitleTagSelect,
55
- onDescTagSelect,
53
+ onTagSelect,
56
54
  onContextChange,
57
55
  renderEditMessage,
58
56
  onFieldChange,
@@ -139,7 +137,7 @@ const CarouselCard = ({
139
137
  (isEditFlow || !isFullMode) ? (
140
138
  <TagList
141
139
  label={formatMessage(globalMessages.addLabels)}
142
- onTagSelect={onTitleTagSelect}
140
+ onTagSelect={onTagSelect}
143
141
  location={location}
144
142
  tags={tagsForPicker}
145
143
  onContextChange={onContextChange}
@@ -161,7 +159,7 @@ const CarouselCard = ({
161
159
  />
162
160
  <CapRow className="rcs_text_area_wrapper">
163
161
  {(isEditFlow || !isFullMode) ? (
164
- renderEditMessage(card.title || '', cardIndex, RCS_TAG_AREA_FIELD_TITLE)
162
+ renderEditMessage(card.title || '', cardIndex)
165
163
  ) : (
166
164
  <CapInput
167
165
  value={card.title || ''}
@@ -191,7 +189,7 @@ const CarouselCard = ({
191
189
  (isEditFlow || !isFullMode) ? (
192
190
  <TagList
193
191
  label={formatMessage(globalMessages.addLabels)}
194
- onTagSelect={onDescTagSelect}
192
+ onTagSelect={onTagSelect}
195
193
  location={location}
196
194
  tags={tagsForPicker}
197
195
  onContextChange={onContextChange}
@@ -213,7 +211,7 @@ const CarouselCard = ({
213
211
  />
214
212
  <CapRow className="rcs_text_area_wrapper">
215
213
  {(isEditFlow || !isFullMode) ? (
216
- renderEditMessage(card.description || '', cardIndex, RCS_TAG_AREA_FIELD_DESC)
214
+ renderEditMessage(card.description || '', cardIndex)
217
215
  ) : (
218
216
  <TextArea
219
217
  autosize={{ minRows: 3, maxRows: 5 }}
@@ -288,8 +286,7 @@ CarouselCard.propTypes = {
288
286
  onDescriptionChange: PropTypes.func.isRequired,
289
287
  onTitleAddVar: PropTypes.func.isRequired,
290
288
  onDescriptionAddVar: PropTypes.func.isRequired,
291
- onTitleTagSelect: PropTypes.func.isRequired,
292
- onDescTagSelect: PropTypes.func.isRequired,
289
+ onTagSelect: PropTypes.func.isRequired,
293
290
  onContextChange: PropTypes.func,
294
291
  renderEditMessage: PropTypes.func.isRequired,
295
292
  onFieldChange: PropTypes.func.isRequired,
@@ -78,13 +78,8 @@ 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
- * 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.()]+$/;
81
+ /** Semantic Liquid-style keys on RCS `cardVarMapped` (same class as `{{…}}` inner names in the editor). */
82
+ export const RCS_CARD_VAR_MAPPED_SEMANTIC_KEY_REGEX = /^[\w.()]+$/;
88
83
  /** Escape all special RegExp characters in an arbitrary string before using it in `new RegExp(...)`. */
89
84
  export const REGEX_SPECIAL_CHARS_ESCAPE_PATTERN = /[-/\\^$*+?.()|[\]{}]/g;
90
85
  /** Entire string is wrapped in `{{…}}` mustache delimiters (allows any content including `}`). */
@@ -175,7 +175,7 @@ import {
175
175
  getCarouselDescriptionCharacterCount,
176
176
  buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
177
177
  buildCarouselCardContentForPayload,
178
- computeCarouselVarSlotKeys,
178
+ getCarouselVarMapKey,
179
179
  } from './carouselUtils';
180
180
  import CarouselDimensionSelection from './components/CarouselDimensionSelection';
181
181
  import CarouselCard from './components/CarouselCard';
@@ -271,12 +271,7 @@ export const Rcs = (props) => {
271
271
  const [carouselErrors, setCarouselErrors] = useState([]); // [{ title: string|false, description: string|false }]
272
272
  const [activeCarouselIndex, setActiveCarouselIndex] = useState('0');
273
273
  const [carouselResetNonce, setCarouselResetNonce] = useState(0);
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
+ const [carouselFocusedVarId, setCarouselFocusedVarId] = useState('');
280
275
  const [imageError, setImageError] = useState(null);
281
276
  const [templateTitleError, setTemplateTitleError] = useState(false);
282
277
  const [cardVarMapped, setCardVarMapped] = useState({});
@@ -602,8 +597,7 @@ export const Rcs = (props) => {
602
597
  }}
603
598
  onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
604
599
  onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
605
- onTitleTagSelect={onCarouselTitleTagSelect}
606
- onDescTagSelect={onCarouselDescTagSelect}
600
+ onTagSelect={onCarouselTagSelect}
607
601
  onContextChange={handleOnTagsContextChange}
608
602
  renderEditMessage={renderCarouselEditMessage}
609
603
  onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
@@ -662,8 +656,7 @@ export const Rcs = (props) => {
662
656
  onChange={(key) => {
663
657
  setActiveCarouselIndex(`${key}`);
664
658
  // reset focused var when switching cards (as per requirement)
665
- setCarouselTitleFocusedVarId('');
666
- setCarouselDescFocusedVarId('');
659
+ setCarouselFocusedVarId('');
667
660
  }}
668
661
  panes={getCarouselTabPanes()}
669
662
  />
@@ -783,7 +776,7 @@ export const Rcs = (props) => {
783
776
  ['title', 'description'].forEach((field) => {
784
777
  const templateStr = card?.[field] || '';
785
778
  if (!templateStr) return;
786
- const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
779
+ const resolved = resolveCarouselTemplateWithMap(templateStr, idx);
787
780
  if (!resolved) {
788
781
  updateCarouselErrors(idx, { [field]: false });
789
782
  return;
@@ -876,22 +869,17 @@ export const Rcs = (props) => {
876
869
  };
877
870
 
878
871
  /**
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.
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.
882
875
  */
883
- const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
876
+ const resolveCarouselTemplateWithMap = (str = '', cardIndex) => {
884
877
  if (!str) return '';
885
878
  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
879
  return arr.map((elem) => {
891
880
  if (rcsVarTestRegex.test(elem)) {
892
- const slotKey = fieldKeys[varOrdinal];
893
- varOrdinal += 1;
894
- const slotValue = cardVarMapped?.[slotKey];
881
+ const varName = getVarNameFromToken(elem);
882
+ const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, varName)];
895
883
  if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
896
884
  return String(slotValue);
897
885
  }
@@ -1249,15 +1237,35 @@ export const Rcs = (props) => {
1249
1237
  setActiveCarouselIndex('0');
1250
1238
 
1251
1239
  if (cards.length > 1) {
1252
- // Each card's own `cardVarMapped` is already keyed by global var position (see
1253
- // computeCarouselVarSlotKeys) a slot's key is assigned once, from where its token
1254
- // sits in the template text, and never depends on which tag fills it. So merging every
1255
- // card's own map directly is correct and collision-free: no per-card coalescing,
1256
- // normalizing, or re-scoping needed (unlike the old name-based scoped-key scheme).
1257
- setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}) => {
1240
+
1241
+ setCardVarMapped((previousVarMap) => cards.reduce((merged, c = {}, idx) => {
1258
1242
  const rawCardMap = c?.cardVarMapped;
1259
1243
  if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
1260
- return { ...merged, ...rawCardMap };
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 };
1261
1269
  }, { ...(previousVarMap || {}) }));
1262
1270
  }
1263
1271
 
@@ -1598,51 +1606,25 @@ export const Rcs = (props) => {
1598
1606
 
1599
1607
  const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
1600
1608
 
1601
- /** Field-local var ordinal (0-based, vars only) for a `{{tok}}_segIdx` composite id — scoped to
1602
- * a single field's own template string (title and description are tracked independently). */
1603
- const getCarouselFieldLocalVarOrdinal = (compositeId, fieldTemplateStr) => {
1604
- const segments = splitTemplateVarStringRcs(fieldTemplateStr ?? '');
1605
- let varOrdinal = 0;
1606
- for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
1607
- if (rcsVarTestRegex.test(segments[segmentIndex])) {
1608
- if (`${segments[segmentIndex]}_${segmentIndex}` === compositeId) return varOrdinal;
1609
- varOrdinal += 1;
1610
- }
1611
- }
1612
- return null;
1613
- };
1614
-
1615
- // The card's placeholder (numeric {{N}} or a bare semantic token like {{first_name}}) is locked
1616
- // here — picking a tag only supplies/replaces the *value* behind that slot in cardVarMapped, it
1617
- // never rewrites the card's title/description text. `focusedVarId` is passed in by the caller
1618
- // (title vs description each track their own last-focused slot, like titleTextAreaId/
1619
- // descTextAreaId) so a click on one field's "Add Labels" can never apply to the other field's
1620
- // last-focused slot. The slot's key is its global position across the whole carousel — assigned
1621
- // once from where its token sits in the template text, never from whatever tag currently fills
1622
- // it — so swapping tags never changes the key and never collides with another card's slot.
1623
- const onCarouselTagSelect = (data, focusedVarId, isDescField) => {
1624
- if (!focusedVarId) return;
1609
+ const onCarouselTagSelect = (data) => {
1610
+ if (!carouselFocusedVarId) return;
1611
+ const sep = carouselFocusedVarId.lastIndexOf('_');
1612
+ if (sep === -1) return;
1613
+ const token = carouselFocusedVarId.slice(0, sep);
1614
+ const variableName = getVarNameFromToken(token);
1615
+ if (!variableName) return;
1625
1616
  const activeIdx = parseInt(activeCarouselIndex, 10);
1626
- const cardIndex = isNaN(activeIdx) ? 0 : activeIdx;
1627
- const fieldTemplateStr = (isDescField
1628
- ? carouselData?.[cardIndex]?.description
1629
- : carouselData?.[cardIndex]?.title) || '';
1630
- const localVarOrdinal = getCarouselFieldLocalVarOrdinal(focusedVarId, fieldTemplateStr);
1631
- if (localVarOrdinal == null) return;
1632
- const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1633
- const slotKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
1634
- if (!slotKey) return;
1635
-
1636
- setCardVarMapped((prev) => ({
1637
- ...(prev || {}),
1638
- [slotKey]: `{{${data}}}`,
1639
- }));
1617
+ const mapKey = getCarouselVarMapKey(isNaN(activeIdx) ? 0 : activeIdx, variableName);
1618
+ setCardVarMapped((prev) => {
1619
+ const base = (prev?.[mapKey] ?? '').toString();
1620
+ const nextVal = `${base}{{${data}}}`;
1621
+ return {
1622
+ ...(prev || {}),
1623
+ [mapKey]: nextVal,
1624
+ };
1625
+ });
1640
1626
  };
1641
1627
 
1642
- const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
1643
-
1644
- const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
1645
-
1646
1628
  //removing optout tag for rcs
1647
1629
  const getRcsTags = () => {
1648
1630
  const tempTags = cloneDeep(tags);
@@ -1728,7 +1710,7 @@ export const Rcs = (props) => {
1728
1710
  key={`${elem}_${index}`}
1729
1711
  placeholder={`enter the value for ${elem}`}
1730
1712
  autosize={{ minRows: 1, maxRows: 3 }}
1731
- onChange={e => textAreaValueChange(e)}
1713
+ onChange={e => textAreaValueChange(e, type)}
1732
1714
  value={textAreaValue(index, type)}
1733
1715
  onFocus={(e) => setTextAreaId(e, type)}
1734
1716
  />
@@ -1948,21 +1930,16 @@ const onTitleAddVar = () => {
1948
1930
 
1949
1931
  // Carousel: render variable-value editor for a given template string (title/description) belonging
1950
1932
  // to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
1951
- // tokens are editable. Each slot's key comes from `computeCarouselVarSlotKeys` stable regardless
1952
- // of which tag fills it, and independent even from another occurrence of the same tag name.
1953
- const renderCarouselEditMessage = (templateStr, cardIndex, fieldType) => {
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) => {
1954
1936
  const renderArray = [];
1955
1937
  const templateArr = splitTemplateVarString(templateStr);
1956
- const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
1957
- const slotKeysForCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
1958
- const fieldKeys = (isDescField ? slotKeysForCard?.description : slotKeysForCard?.title) ?? [];
1959
- let varOrdinal = 0;
1960
1938
  if (templateArr?.length) {
1961
1939
  templateArr.forEach((elem, index) => {
1962
1940
  if (rcsVarTestRegex.test(elem)) {
1963
1941
  const varName = getVarNameFromToken(elem);
1964
- const mapKey = fieldKeys[varOrdinal];
1965
- varOrdinal += 1;
1942
+ const mapKey = getCarouselVarMapKey(cardIndex, varName);
1966
1943
  const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
1967
1944
  renderArray.push(
1968
1945
  <div
@@ -1973,15 +1950,11 @@ const onTitleAddVar = () => {
1973
1950
  id={`${elem}_${index}`}
1974
1951
  placeholder={`enter the value for ${elem}`}
1975
1952
  autosize={{ minRows: 1, maxRows: 3 }}
1976
- onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
1953
+ onChange={(e) => textAreaValueChange(e, cardIndex)}
1977
1954
  value={computedValue}
1978
1955
  onFocus={(e) => {
1979
1956
  const id = e?.target?.id || e?.currentTarget?.id || '';
1980
- if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
1981
- setCarouselDescFocusedVarId(id);
1982
- } else {
1983
- setCarouselTitleFocusedVarId(id);
1984
- }
1957
+ setCarouselFocusedVarId(id);
1985
1958
  }}
1986
1959
  />
1987
1960
  </div>
@@ -2002,7 +1975,7 @@ const onTitleAddVar = () => {
2002
1975
  return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
2003
1976
  };
2004
1977
 
2005
- const textAreaValueChange = (e) => {
1978
+ const textAreaValueChange = (e, cardIndex) => {
2006
1979
  const value = e?.target?.value ?? '';
2007
1980
  const id = e?.target?.id || e?.currentTarget?.id || '';
2008
1981
  if (!id) return;
@@ -2013,38 +1986,14 @@ const onTitleAddVar = () => {
2013
1986
  const variableName = getVarNameFromToken(token);
2014
1987
 
2015
1988
  if (variableName) {
2016
- const nextVal = isInvalidValue ? "" : value;
1989
+ const mapKey = getCarouselVarMapKey(cardIndex, variableName);
2017
1990
  setCardVarMapped((prev) => ({
2018
1991
  ...prev,
2019
- [variableName]: nextVal,
1992
+ [mapKey]: isInvalidValue ? "" : value,
2020
1993
  }));
2021
1994
  }
2022
1995
  };
2023
1996
 
2024
- // Carousel: typing directly into a slot's value box (as opposed to picking a tag) — same global
2025
- // positional key as onCarouselTagSelect/renderCarouselEditMessage, so both ways of filling a
2026
- // slot land on the identical key.
2027
- const carouselTextAreaValueChange = (e, cardIndex, fieldType) => {
2028
- const value = e?.target?.value ?? '';
2029
- const id = e?.target?.id || e?.currentTarget?.id || '';
2030
- if (!id) return;
2031
- const isDescField = fieldType === RCS_TAG_AREA_FIELD_DESC;
2032
- const fieldTemplateStr = (isDescField
2033
- ? carouselData?.[cardIndex]?.description
2034
- : carouselData?.[cardIndex]?.title) || '';
2035
- const localVarOrdinal = getCarouselFieldLocalVarOrdinal(id, fieldTemplateStr);
2036
- if (localVarOrdinal == null) return;
2037
- const slotKeys = computeCarouselVarSlotKeys(carouselData, rcsVarRegex)[cardIndex];
2038
- const mapKey = (isDescField ? slotKeys?.description : slotKeys?.title)?.[localVarOrdinal];
2039
- if (!mapKey) return;
2040
- const isInvalidValue = value?.trim() === "";
2041
- const nextVal = isInvalidValue ? "" : value;
2042
- setCardVarMapped((prev) => ({
2043
- ...prev,
2044
- [mapKey]: nextVal,
2045
- }));
2046
- };
2047
-
2048
1997
  const setTextAreaId = (e, type) => {
2049
1998
  // VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
2050
1999
  // have an `.target.id` shape. Support both.
@@ -2895,8 +2844,9 @@ const onTitleAddVar = () => {
2895
2844
  // the user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
2896
2845
  // - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
2897
2846
  // - prepareTagExtractionPayload can extract tag metadata from the card content.
2898
- // Each variable slot's key comes from computeCarouselVarSlotKeys, so two cards' variables
2899
- // never share a key even when they pick the same tag name for their own slot.
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).
2900
2850
  rcsForTest = {
2901
2851
  ...rcsForTest,
2902
2852
  rcsContent: {
@@ -2904,8 +2854,8 @@ const onTitleAddVar = () => {
2904
2854
  cardContent: cardContent.map((card, cardIndex) => {
2905
2855
  const rawTitle = card.title || '';
2906
2856
  const rawDesc = card.description || '';
2907
- const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
2908
- const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
2857
+ const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
2858
+ const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
2909
2859
  return { ...card, title: resolvedTitle, description: resolvedDesc };
2910
2860
  }),
2911
2861
  },
@@ -3116,16 +3066,12 @@ const onTitleAddVar = () => {
3116
3066
  ['title', 'description'].some((field) => !(card?.[field] || '').trim())
3117
3067
  );
3118
3068
  if (hasEmptyField) return true;
3119
- const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
3120
3069
  const unfilledVar = (carouselData || []).some((card, cardIndex) =>
3121
3070
  ['title', 'description'].some((field) => {
3122
- const isDescField = field === 'description';
3123
3071
  const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
3124
- if (tokens.length === 0) return false;
3125
- const fieldKeys = (isDescField ? slotKeysByCard[cardIndex]?.description : slotKeysByCard[cardIndex]?.title) ?? [];
3126
- return tokens.some((token, localVarOrdinal) => {
3127
- const slotKey = fieldKeys[localVarOrdinal];
3128
- const slotValue = cardVarMapped?.[slotKey];
3072
+ return tokens.some((token) => {
3073
+ const name = token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
3074
+ const slotValue = cardVarMapped?.[getCarouselVarMapKey(cardIndex, name)];
3129
3075
  return slotValue == null || String(slotValue).trim() === '';
3130
3076
  });
3131
3077
  })
@@ -459,48 +459,11 @@ export function syncCardVarMappedSemanticsFromSlots(
459
459
  templateVarTokens.forEach((token, slotIndexZeroBased) => {
460
460
  const semanticVarName = getVarNameFromToken(token);
461
461
  if (!semanticVarName) return;
462
-
463
- if (!RCS_NUMERIC_VAR_NAME_REGEX.test(semanticVarName)) {
464
- // A numeric slot may already self-reference this exact tag (e.g. "4" -> "{{last_name}}") when
465
- // the template's own numeric placeholder has since been resolved to this tag in the text —
466
- // that numeric key is the source of truth, not this token's position.
467
- const mustacheSelfReference = `{{${semanticVarName}}}`;
468
- const existingNumericKeyForSemantic = Object.keys(cardVarMappedSynced).find((key) => (
469
- RCS_NUMERIC_VAR_NAME_REGEX.test(key) && cardVarMappedSynced[key] === mustacheSelfReference
470
- ));
471
- if (existingNumericKeyForSemantic) {
472
- const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
473
- const numericValueTrimmed = String(cardVarMappedSynced[existingNumericKeyForSemantic] ?? '').trim();
474
- if (!semanticValueTrimmed && numericValueTrimmed) {
475
- cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[existingNumericKeyForSemantic];
476
- }
477
- return;
478
- }
479
-
480
- // Legacy: template embeds the semantic token directly ({{user_name}}), but older payloads
481
- // stored the resolved value under a position-based numeric key ("1", "2", …).
482
- const numericSlotKey = String(slotIndexZeroBased + 1);
483
- const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
484
- const numericSlotValueTrimmed = String(cardVarMappedSynced[numericSlotKey] ?? '').trim();
485
- if (!semanticValueTrimmed && numericSlotValueTrimmed) {
486
- cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[numericSlotKey];
487
- }
488
- return;
489
- }
490
-
491
- // Carousel numeric placeholder ({{3}}, {{4}}, …): the slot key IS the token digit itself —
492
- // numbers are allocated globally across the whole carousel (getNextCarouselVarToken), not
493
- // re-numbered per card, so a position-based key here would target the wrong slot. Its value
494
- // may be a mustache-wrapped tag reference (e.g. "3" -> "{{last_name}}") — mirror that onto the
495
- // bare semantic key too so VarSegment editors keyed by tag name also prepopulate.
496
- const slotValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
497
- if (!slotValueTrimmed) return;
498
- const mustacheInnerMatch = slotValueTrimmed.match(/^\{\{([^}]+)\}\}$/);
499
- const innerSemanticName = mustacheInnerMatch?.[1]?.trim();
500
- if (!innerSemanticName || innerSemanticName === semanticVarName) return;
501
- const existingSemanticValue = String(cardVarMappedSynced[innerSemanticName] ?? '').trim();
502
- if (!existingSemanticValue) {
503
- cardVarMappedSynced[innerSemanticName] = cardVarMappedSynced[semanticVarName];
462
+ const numericSlotKey = String(slotIndexZeroBased + 1);
463
+ const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
464
+ const numericSlotValueTrimmed = String(cardVarMappedSynced[numericSlotKey] ?? '').trim();
465
+ if (!semanticValueTrimmed && numericSlotValueTrimmed) {
466
+ cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[numericSlotKey];
504
467
  }
505
468
  });
506
469
  return cardVarMappedSynced;
@@ -220,8 +220,7 @@ const baseProps = {
220
220
  onDescriptionChange: jest.fn(),
221
221
  onTitleAddVar: jest.fn(),
222
222
  onDescriptionAddVar: jest.fn(),
223
- onTitleTagSelect: jest.fn(),
224
- onDescTagSelect: jest.fn(),
223
+ onTagSelect: jest.fn(),
225
224
  onContextChange: jest.fn(),
226
225
  renderEditMessage: (val) => <div data-testid="render-edit-message">{val}</div>,
227
226
  onFieldChange: jest.fn(),