@capillarytech/creatives-library 9.0.36-alpha.0 → 9.0.37-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/translations/en.json +2 -2
- package/utils/permissions.js +59 -0
- package/utils/tests/permissions.test.js +116 -0
- package/v2Components/AccessForbidden/_accessForbidden.scss +14 -0
- package/v2Components/AccessForbidden/index.js +5 -3
- package/v2Components/AccessForbidden/messages.js +3 -2
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberPreviewContent.js +15 -108
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +1 -139
- package/v2Components/CommonTestAndPreview/index.js +26 -240
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/ViberPreviewContent.test.js +0 -364
- package/v2Components/NoPermissionWrapper/_noPermissionWrapper.scss +12 -0
- package/v2Components/NoPermissionWrapper/index.js +26 -0
- package/v2Containers/App/constants.js +5 -0
- package/v2Containers/Assets/Gallery/_gallery.scss +6 -0
- package/v2Containers/Assets/Gallery/index.js +34 -10
- package/v2Containers/Cap/index.js +5 -2
- package/v2Containers/Cap/messages.js +4 -0
- package/v2Containers/Cap/tests/__snapshots__/index.test.js.snap +2 -2
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +6 -6
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +4 -4
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +32 -32
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +36 -36
- package/v2Containers/Rcs/carouselUtils.js +73 -46
- package/v2Containers/Rcs/components/CarouselCard.js +9 -6
- package/v2Containers/Rcs/constants.js +7 -2
- package/v2Containers/Rcs/index.js +126 -72
- package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +43 -5
- package/v2Containers/Rcs/tests/CarouselCard.test.js +2 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +118 -118
- package/v2Containers/Rcs/tests/carouselUtils.test.js +15 -15
- package/v2Containers/Rcs/tests/index.test.js +232 -10
- package/v2Containers/Rcs/tests/utils.test.js +17 -0
- package/v2Containers/Rcs/utils.js +51 -12
- package/v2Containers/SmsTrai/Create/tests/__snapshots__/index.test.js.snap +8 -8
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -18
- package/v2Containers/Templates/ChannelTypeIllustration.js +10 -1
- package/v2Containers/Templates/_templates.scss +6 -114
- package/v2Containers/Templates/index.js +151 -190
- package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +7 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +173 -120
- package/v2Containers/TemplatesV2/index.js +16 -5
- package/v2Containers/Viber/constants.js +0 -21
- package/v2Containers/Viber/index.js +49 -719
- package/v2Containers/Viber/index.scss +0 -175
- package/v2Containers/Viber/messages.js +0 -121
- package/v2Containers/Viber/tests/index.test.js +0 -80
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +268 -268
- package/v2Components/CommonTestAndPreview/UnifiedPreview/ViberCarouselPreviewCards.js +0 -132
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_viberCarouselPreviewCards.scss +0 -131
|
@@ -15,6 +15,7 @@ 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';
|
|
18
19
|
import CarouselCardMedia from './CarouselCardMedia';
|
|
19
20
|
import CarouselCardButtons from './CarouselCardButtons';
|
|
20
21
|
import CarouselCharacterCount from './CarouselCharacterCount';
|
|
@@ -50,7 +51,8 @@ const CarouselCard = ({
|
|
|
50
51
|
onDescriptionChange,
|
|
51
52
|
onTitleAddVar,
|
|
52
53
|
onDescriptionAddVar,
|
|
53
|
-
|
|
54
|
+
onTitleTagSelect,
|
|
55
|
+
onDescTagSelect,
|
|
54
56
|
onContextChange,
|
|
55
57
|
renderEditMessage,
|
|
56
58
|
onFieldChange,
|
|
@@ -137,7 +139,7 @@ const CarouselCard = ({
|
|
|
137
139
|
(isEditFlow || !isFullMode) ? (
|
|
138
140
|
<TagList
|
|
139
141
|
label={formatMessage(globalMessages.addLabels)}
|
|
140
|
-
onTagSelect={
|
|
142
|
+
onTagSelect={onTitleTagSelect}
|
|
141
143
|
location={location}
|
|
142
144
|
tags={tagsForPicker}
|
|
143
145
|
onContextChange={onContextChange}
|
|
@@ -159,7 +161,7 @@ const CarouselCard = ({
|
|
|
159
161
|
/>
|
|
160
162
|
<CapRow className="rcs_text_area_wrapper">
|
|
161
163
|
{(isEditFlow || !isFullMode) ? (
|
|
162
|
-
renderEditMessage(card.title || '', cardIndex)
|
|
164
|
+
renderEditMessage(card.title || '', cardIndex, RCS_TAG_AREA_FIELD_TITLE)
|
|
163
165
|
) : (
|
|
164
166
|
<CapInput
|
|
165
167
|
value={card.title || ''}
|
|
@@ -189,7 +191,7 @@ const CarouselCard = ({
|
|
|
189
191
|
(isEditFlow || !isFullMode) ? (
|
|
190
192
|
<TagList
|
|
191
193
|
label={formatMessage(globalMessages.addLabels)}
|
|
192
|
-
onTagSelect={
|
|
194
|
+
onTagSelect={onDescTagSelect}
|
|
193
195
|
location={location}
|
|
194
196
|
tags={tagsForPicker}
|
|
195
197
|
onContextChange={onContextChange}
|
|
@@ -211,7 +213,7 @@ const CarouselCard = ({
|
|
|
211
213
|
/>
|
|
212
214
|
<CapRow className="rcs_text_area_wrapper">
|
|
213
215
|
{(isEditFlow || !isFullMode) ? (
|
|
214
|
-
renderEditMessage(card.description || '', cardIndex)
|
|
216
|
+
renderEditMessage(card.description || '', cardIndex, RCS_TAG_AREA_FIELD_DESC)
|
|
215
217
|
) : (
|
|
216
218
|
<TextArea
|
|
217
219
|
autosize={{ minRows: 3, maxRows: 5 }}
|
|
@@ -286,7 +288,8 @@ CarouselCard.propTypes = {
|
|
|
286
288
|
onDescriptionChange: PropTypes.func.isRequired,
|
|
287
289
|
onTitleAddVar: PropTypes.func.isRequired,
|
|
288
290
|
onDescriptionAddVar: PropTypes.func.isRequired,
|
|
289
|
-
|
|
291
|
+
onTitleTagSelect: PropTypes.func.isRequired,
|
|
292
|
+
onDescTagSelect: PropTypes.func.isRequired,
|
|
290
293
|
onContextChange: PropTypes.func,
|
|
291
294
|
renderEditMessage: PropTypes.func.isRequired,
|
|
292
295
|
onFieldChange: PropTypes.func.isRequired,
|
|
@@ -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,7 +175,7 @@ import {
|
|
|
175
175
|
getCarouselDescriptionCharacterCount,
|
|
176
176
|
buildCarouselCardsForPreview as buildCarouselCardsForPreviewUtil,
|
|
177
177
|
buildCarouselCardContentForPayload,
|
|
178
|
-
|
|
178
|
+
computeCarouselVarSlotKeys,
|
|
179
179
|
} from './carouselUtils';
|
|
180
180
|
import CarouselDimensionSelection from './components/CarouselDimensionSelection';
|
|
181
181
|
import CarouselCard from './components/CarouselCard';
|
|
@@ -271,7 +271,12 @@ 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
|
-
|
|
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('');
|
|
275
280
|
const [imageError, setImageError] = useState(null);
|
|
276
281
|
const [templateTitleError, setTemplateTitleError] = useState(false);
|
|
277
282
|
const [cardVarMapped, setCardVarMapped] = useState({});
|
|
@@ -597,7 +602,8 @@ export const Rcs = (props) => {
|
|
|
597
602
|
}}
|
|
598
603
|
onTitleAddVar={() => appendVarToCarouselField(index, 'title')}
|
|
599
604
|
onDescriptionAddVar={() => appendVarToCarouselField(index, 'description')}
|
|
600
|
-
|
|
605
|
+
onTitleTagSelect={onCarouselTitleTagSelect}
|
|
606
|
+
onDescTagSelect={onCarouselDescTagSelect}
|
|
601
607
|
onContextChange={handleOnTagsContextChange}
|
|
602
608
|
renderEditMessage={renderCarouselEditMessage}
|
|
603
609
|
onFieldChange={(fieldName, value) => handleCarouselValueChange(index, [{ fieldName, value }])}
|
|
@@ -656,7 +662,8 @@ export const Rcs = (props) => {
|
|
|
656
662
|
onChange={(key) => {
|
|
657
663
|
setActiveCarouselIndex(`${key}`);
|
|
658
664
|
// reset focused var when switching cards (as per requirement)
|
|
659
|
-
|
|
665
|
+
setCarouselTitleFocusedVarId('');
|
|
666
|
+
setCarouselDescFocusedVarId('');
|
|
660
667
|
}}
|
|
661
668
|
panes={getCarouselTabPanes()}
|
|
662
669
|
/>
|
|
@@ -776,7 +783,7 @@ export const Rcs = (props) => {
|
|
|
776
783
|
['title', 'description'].forEach((field) => {
|
|
777
784
|
const templateStr = card?.[field] || '';
|
|
778
785
|
if (!templateStr) return;
|
|
779
|
-
const resolved = resolveCarouselTemplateWithMap(templateStr, idx);
|
|
786
|
+
const resolved = resolveCarouselTemplateWithMap(templateStr, idx, field === 'description');
|
|
780
787
|
if (!resolved) {
|
|
781
788
|
updateCarouselErrors(idx, { [field]: false });
|
|
782
789
|
return;
|
|
@@ -869,17 +876,22 @@ export const Rcs = (props) => {
|
|
|
869
876
|
};
|
|
870
877
|
|
|
871
878
|
/**
|
|
872
|
-
* Carousel resolve: each
|
|
873
|
-
*
|
|
874
|
-
*
|
|
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.
|
|
875
882
|
*/
|
|
876
|
-
const resolveCarouselTemplateWithMap = (str = '', cardIndex) => {
|
|
883
|
+
const resolveCarouselTemplateWithMap = (str = '', cardIndex, isDescField = false) => {
|
|
877
884
|
if (!str) return '';
|
|
878
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;
|
|
879
890
|
return arr.map((elem) => {
|
|
880
891
|
if (rcsVarTestRegex.test(elem)) {
|
|
881
|
-
const
|
|
882
|
-
|
|
892
|
+
const slotKey = fieldKeys[varOrdinal];
|
|
893
|
+
varOrdinal += 1;
|
|
894
|
+
const slotValue = cardVarMapped?.[slotKey];
|
|
883
895
|
if (isNil(slotValue) || String(slotValue)?.trim?.() === '') return elem;
|
|
884
896
|
return String(slotValue);
|
|
885
897
|
}
|
|
@@ -1237,35 +1249,15 @@ export const Rcs = (props) => {
|
|
|
1237
1249
|
setActiveCarouselIndex('0');
|
|
1238
1250
|
|
|
1239
1251
|
if (cards.length > 1) {
|
|
1240
|
-
|
|
1241
|
-
|
|
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 = {}) => {
|
|
1242
1258
|
const rawCardMap = c?.cardVarMapped;
|
|
1243
1259
|
if (rawCardMap == null || typeof rawCardMap !== 'object') return merged;
|
|
1244
|
-
|
|
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 };
|
|
1260
|
+
return { ...merged, ...rawCardMap };
|
|
1269
1261
|
}, { ...(previousVarMap || {}) }));
|
|
1270
1262
|
}
|
|
1271
1263
|
|
|
@@ -1606,25 +1598,51 @@ export const Rcs = (props) => {
|
|
|
1606
1598
|
|
|
1607
1599
|
const onDescTagSelect = (tagName) => onTagSelect(tagName, descTextAreaId, RCS_TAG_AREA_FIELD_DESC);
|
|
1608
1600
|
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
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;
|
|
1616
1625
|
const activeIdx = parseInt(activeCarouselIndex, 10);
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
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
|
+
}));
|
|
1626
1640
|
};
|
|
1627
1641
|
|
|
1642
|
+
const onCarouselTitleTagSelect = (data) => onCarouselTagSelect(data, carouselTitleFocusedVarId, false);
|
|
1643
|
+
|
|
1644
|
+
const onCarouselDescTagSelect = (data) => onCarouselTagSelect(data, carouselDescFocusedVarId, true);
|
|
1645
|
+
|
|
1628
1646
|
//removing optout tag for rcs
|
|
1629
1647
|
const getRcsTags = () => {
|
|
1630
1648
|
const tempTags = cloneDeep(tags);
|
|
@@ -1710,7 +1728,7 @@ export const Rcs = (props) => {
|
|
|
1710
1728
|
key={`${elem}_${index}`}
|
|
1711
1729
|
placeholder={`enter the value for ${elem}`}
|
|
1712
1730
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1713
|
-
onChange={e => textAreaValueChange(e
|
|
1731
|
+
onChange={e => textAreaValueChange(e)}
|
|
1714
1732
|
value={textAreaValue(index, type)}
|
|
1715
1733
|
onFocus={(e) => setTextAreaId(e, type)}
|
|
1716
1734
|
/>
|
|
@@ -1930,16 +1948,21 @@ const onTitleAddVar = () => {
|
|
|
1930
1948
|
|
|
1931
1949
|
// Carousel: render variable-value editor for a given template string (title/description) belonging
|
|
1932
1950
|
// to `cardIndex`. This matches rich-card/text edit behavior: static pieces are read-only, variable
|
|
1933
|
-
// tokens are editable. Each
|
|
1934
|
-
//
|
|
1935
|
-
const renderCarouselEditMessage = (templateStr, cardIndex) => {
|
|
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) => {
|
|
1936
1954
|
const renderArray = [];
|
|
1937
1955
|
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;
|
|
1938
1960
|
if (templateArr?.length) {
|
|
1939
1961
|
templateArr.forEach((elem, index) => {
|
|
1940
1962
|
if (rcsVarTestRegex.test(elem)) {
|
|
1941
1963
|
const varName = getVarNameFromToken(elem);
|
|
1942
|
-
const mapKey =
|
|
1964
|
+
const mapKey = fieldKeys[varOrdinal];
|
|
1965
|
+
varOrdinal += 1;
|
|
1943
1966
|
const computedValue = varName ? ((cardVarMapped?.[mapKey] ?? '').toString()) : '';
|
|
1944
1967
|
renderArray.push(
|
|
1945
1968
|
<div
|
|
@@ -1950,11 +1973,15 @@ const onTitleAddVar = () => {
|
|
|
1950
1973
|
id={`${elem}_${index}`}
|
|
1951
1974
|
placeholder={`enter the value for ${elem}`}
|
|
1952
1975
|
autosize={{ minRows: 1, maxRows: 3 }}
|
|
1953
|
-
onChange={(e) =>
|
|
1976
|
+
onChange={(e) => carouselTextAreaValueChange(e, cardIndex, fieldType)}
|
|
1954
1977
|
value={computedValue}
|
|
1955
1978
|
onFocus={(e) => {
|
|
1956
1979
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1957
|
-
|
|
1980
|
+
if (fieldType === RCS_TAG_AREA_FIELD_DESC) {
|
|
1981
|
+
setCarouselDescFocusedVarId(id);
|
|
1982
|
+
} else {
|
|
1983
|
+
setCarouselTitleFocusedVarId(id);
|
|
1984
|
+
}
|
|
1958
1985
|
}}
|
|
1959
1986
|
/>
|
|
1960
1987
|
</div>
|
|
@@ -1975,7 +2002,7 @@ const onTitleAddVar = () => {
|
|
|
1975
2002
|
return <CapRow className="rcs-edit-template-message-input">{renderArray}</CapRow>;
|
|
1976
2003
|
};
|
|
1977
2004
|
|
|
1978
|
-
const textAreaValueChange = (e
|
|
2005
|
+
const textAreaValueChange = (e) => {
|
|
1979
2006
|
const value = e?.target?.value ?? '';
|
|
1980
2007
|
const id = e?.target?.id || e?.currentTarget?.id || '';
|
|
1981
2008
|
if (!id) return;
|
|
@@ -1986,14 +2013,38 @@ const onTitleAddVar = () => {
|
|
|
1986
2013
|
const variableName = getVarNameFromToken(token);
|
|
1987
2014
|
|
|
1988
2015
|
if (variableName) {
|
|
1989
|
-
const
|
|
2016
|
+
const nextVal = isInvalidValue ? "" : value;
|
|
1990
2017
|
setCardVarMapped((prev) => ({
|
|
1991
2018
|
...prev,
|
|
1992
|
-
[
|
|
2019
|
+
[variableName]: nextVal,
|
|
1993
2020
|
}));
|
|
1994
2021
|
}
|
|
1995
2022
|
};
|
|
1996
2023
|
|
|
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
|
+
|
|
1997
2048
|
const setTextAreaId = (e, type) => {
|
|
1998
2049
|
// VarSegmentMessageEditor calls onFocus(id) with a plain string; DOM events
|
|
1999
2050
|
// have an `.target.id` shape. Support both.
|
|
@@ -2844,9 +2895,8 @@ const onTitleAddVar = () => {
|
|
|
2844
2895
|
// the user mapped via cardVarMapped. Run this for ALL modes (create, edit, consumer) so that:
|
|
2845
2896
|
// - buildRcsTestMessagePayload sends real Capillary tag names to the test API, and
|
|
2846
2897
|
// - prepareTagExtractionPayload can extract tag metadata from the card content.
|
|
2847
|
-
// Each
|
|
2848
|
-
//
|
|
2849
|
-
// a key, so there is nothing to accidentally collide).
|
|
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.
|
|
2850
2900
|
rcsForTest = {
|
|
2851
2901
|
...rcsForTest,
|
|
2852
2902
|
rcsContent: {
|
|
@@ -2854,8 +2904,8 @@ const onTitleAddVar = () => {
|
|
|
2854
2904
|
cardContent: cardContent.map((card, cardIndex) => {
|
|
2855
2905
|
const rawTitle = card.title || '';
|
|
2856
2906
|
const rawDesc = card.description || '';
|
|
2857
|
-
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex);
|
|
2858
|
-
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex);
|
|
2907
|
+
const resolvedTitle = resolveCarouselTemplateWithMap(rawTitle, cardIndex, false);
|
|
2908
|
+
const resolvedDesc = resolveCarouselTemplateWithMap(rawDesc, cardIndex, true);
|
|
2859
2909
|
return { ...card, title: resolvedTitle, description: resolvedDesc };
|
|
2860
2910
|
}),
|
|
2861
2911
|
},
|
|
@@ -3066,12 +3116,16 @@ const onTitleAddVar = () => {
|
|
|
3066
3116
|
['title', 'description'].some((field) => !(card?.[field] || '').trim())
|
|
3067
3117
|
);
|
|
3068
3118
|
if (hasEmptyField) return true;
|
|
3119
|
+
const slotKeysByCard = computeCarouselVarSlotKeys(carouselData, rcsVarRegex);
|
|
3069
3120
|
const unfilledVar = (carouselData || []).some((card, cardIndex) =>
|
|
3070
3121
|
['title', 'description'].some((field) => {
|
|
3122
|
+
const isDescField = field === 'description';
|
|
3071
3123
|
const tokens = splitTemplateVarStringRcs(card?.[field] || '').filter((token) => rcsVarTestRegex.test(token));
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
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];
|
|
3075
3129
|
return slotValue == null || String(slotValue).trim() === '';
|
|
3076
3130
|
});
|
|
3077
3131
|
})
|
|
@@ -447,6 +447,7 @@ export function syncCardVarMappedSemanticsFromSlots(
|
|
|
447
447
|
templateDesc,
|
|
448
448
|
rcsVarRegex,
|
|
449
449
|
) {
|
|
450
|
+
console.log('syncCardVarMappedSemanticsFromSlots', cardVarMappedInput, templateTitle, templateDesc);
|
|
450
451
|
const cardVarMappedSynced =
|
|
451
452
|
cardVarMappedInput != null && typeof cardVarMappedInput === 'object'
|
|
452
453
|
? { ...cardVarMappedInput }
|
|
@@ -459,11 +460,48 @@ export function syncCardVarMappedSemanticsFromSlots(
|
|
|
459
460
|
templateVarTokens.forEach((token, slotIndexZeroBased) => {
|
|
460
461
|
const semanticVarName = getVarNameFromToken(token);
|
|
461
462
|
if (!semanticVarName) return;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
463
|
+
|
|
464
|
+
if (!RCS_NUMERIC_VAR_NAME_REGEX.test(semanticVarName)) {
|
|
465
|
+
// A numeric slot may already self-reference this exact tag (e.g. "4" -> "{{last_name}}") when
|
|
466
|
+
// the template's own numeric placeholder has since been resolved to this tag in the text —
|
|
467
|
+
// that numeric key is the source of truth, not this token's position.
|
|
468
|
+
const mustacheSelfReference = `{{${semanticVarName}}}`;
|
|
469
|
+
const existingNumericKeyForSemantic = Object.keys(cardVarMappedSynced).find((key) => (
|
|
470
|
+
RCS_NUMERIC_VAR_NAME_REGEX.test(key) && cardVarMappedSynced[key] === mustacheSelfReference
|
|
471
|
+
));
|
|
472
|
+
if (existingNumericKeyForSemantic) {
|
|
473
|
+
const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
474
|
+
const numericValueTrimmed = String(cardVarMappedSynced[existingNumericKeyForSemantic] ?? '').trim();
|
|
475
|
+
if (!semanticValueTrimmed && numericValueTrimmed) {
|
|
476
|
+
cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[existingNumericKeyForSemantic];
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Legacy: template embeds the semantic token directly ({{user_name}}), but older payloads
|
|
482
|
+
// stored the resolved value under a position-based numeric key ("1", "2", …).
|
|
483
|
+
const numericSlotKey = String(slotIndexZeroBased + 1);
|
|
484
|
+
const semanticValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
485
|
+
const numericSlotValueTrimmed = String(cardVarMappedSynced[numericSlotKey] ?? '').trim();
|
|
486
|
+
if (!semanticValueTrimmed && numericSlotValueTrimmed) {
|
|
487
|
+
cardVarMappedSynced[semanticVarName] = cardVarMappedSynced[numericSlotKey];
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Carousel numeric placeholder ({{3}}, {{4}}, …): the slot key IS the token digit itself —
|
|
493
|
+
// numbers are allocated globally across the whole carousel (getNextCarouselVarToken), not
|
|
494
|
+
// re-numbered per card, so a position-based key here would target the wrong slot. Its value
|
|
495
|
+
// may be a mustache-wrapped tag reference (e.g. "3" -> "{{last_name}}") — mirror that onto the
|
|
496
|
+
// bare semantic key too so VarSegment editors keyed by tag name also prepopulate.
|
|
497
|
+
const slotValueTrimmed = String(cardVarMappedSynced[semanticVarName] ?? '').trim();
|
|
498
|
+
if (!slotValueTrimmed) return;
|
|
499
|
+
const mustacheInnerMatch = slotValueTrimmed.match(/^\{\{([^}]+)\}\}$/);
|
|
500
|
+
const innerSemanticName = mustacheInnerMatch?.[1]?.trim();
|
|
501
|
+
if (!innerSemanticName || innerSemanticName === semanticVarName) return;
|
|
502
|
+
const existingSemanticValue = String(cardVarMappedSynced[innerSemanticName] ?? '').trim();
|
|
503
|
+
if (!existingSemanticValue) {
|
|
504
|
+
cardVarMappedSynced[innerSemanticName] = cardVarMappedSynced[semanticVarName];
|
|
467
505
|
}
|
|
468
506
|
});
|
|
469
507
|
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(),
|