@capillarytech/creatives-library 8.0.238 → 8.0.239

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.238",
4
+ "version": "8.0.239",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -183,8 +183,12 @@ export const Rcs = (props) => {
183
183
  const [templateHeaderError, setTemplateHeaderError] = useState('');
184
184
  const [templateMessageError, setTemplateMessageError] = useState('');
185
185
  const validVarRegex = /\{\{(\d+)\}\}/g;
186
- const [rcsTextVariables, setRcsTextVariables] = useState({});
187
- const [focusedVarId, setFocusedVarId] = useState("");
186
+ const [updatedTitleData, setUpdatedTitleData] = useState([]);
187
+ const [updatedDescData, setUpdatedDescData] = useState([]);
188
+ const [titleVarMappedData, setTitleVarMappedData] = useState({});
189
+ const [descVarMappedData, setDescVarMappedData] = useState({});
190
+ const [titleTextAreaId, setTitleTextAreaId] = useState();
191
+ const [descTextAreaId, setDescTextAreaId] = useState();
188
192
  const [assetList, setAssetList] = useState({});
189
193
  const [assetListImage, setAssetListImage] = useState('');
190
194
  const [rcsImageSrc, updateRcsImageSrc] = useState('');
@@ -193,7 +197,6 @@ export const Rcs = (props) => {
193
197
  const [selectedDimension, setSelectedDimension] = useState(RCS_IMAGE_DIMENSIONS.MEDIUM_HEIGHT.type);
194
198
  const [imageError, setImageError] = useState(null);
195
199
  const [templateTitleError, setTemplateTitleError] = useState(false);
196
- const [focusedVarType, setFocusedVarType] = useState(MESSAGE_TEXT);
197
200
  const [cardVarMapped, setCardVarMapped] = useState({});
198
201
 
199
202
  const tempMsg = dltPreviewData === '' ? fallbackMessage : dltPreviewData;
@@ -269,83 +272,27 @@ export const Rcs = (props) => {
269
272
  });
270
273
  };
271
274
 
272
- // Check if all variables are resolved (no empty variables left)
273
- const areAllVariablesResolved = () => {
274
- // Get all variables from template content
275
- const titleVars = splitTemplateVarString(templateTitle).filter(elem => rcsVarTestRegex.test(elem));
276
- const descVars = splitTemplateVarString(templateDesc).filter(elem => rcsVarTestRegex.test(elem));
277
- const allVars = [...titleVars, ...descVars];
278
-
279
- // If no variables, consider resolved
280
- if (allVars.length === 0) return true;
281
-
282
- // Check if all variables have values in cardVarMapped
283
- for (const variable of allVars) {
284
- const variableName = variable.replace(/^\{\{|\}\}$/g, '');
285
- const mappedValue = cardVarMapped[variableName] || cardVarMapped[variable];
286
- if (!mappedValue || mappedValue.trim() === '') {
287
- return false; // Found empty variable
288
- }
289
- }
290
-
291
- return true;
292
- };
293
-
294
- const tagValidation = (content, type) => {
295
- // Only validate tags in edit mode (!isFullMode)
296
- if (isFullMode) {
275
+ const validateResolvedTagsForType = (type) => {
276
+ if (isFullMode) return;
277
+ if (loadingTags || !tags || tags.length === 0) return;
278
+ const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
279
+ const resolved = resolveTemplateWithMap(templateStr); // placeholders -> mapped value (or '')
280
+ if (!resolved) {
281
+ if (type === TITLE_TEXT) setTemplateTitleError(false);
282
+ if (type === MESSAGE_TEXT) setTemplateDescError(false);
297
283
  return;
298
284
  }
299
-
300
- // Only show unsupported tags error after all variables are resolved
301
- if (!areAllVariablesResolved()) {
302
- // Clear any existing unsupported tags errors while variables are still empty
303
- if (type === titletype && templateTitleError && templateTitleError.includes('Unsupported tags:')) {
304
- setTemplateTitleError(false);
305
- } else if (type === descType && templateDescError && templateDescError.includes('Unsupported tags:')) {
306
- setTemplateDescError(false);
307
- }
308
- return;
309
- }
310
-
311
- const validationResponse = validateTags({
312
- content: Array.isArray(content) ? content.join('') : content,
285
+ const validationResponse =
286
+ validateTags({
287
+ content: resolved,
313
288
  tagsParam: tags,
314
289
  injectedTagsParams: injectedTags,
315
290
  location,
316
291
  tagModule: getDefaultTags,
317
292
  eventContextTags,
318
293
  }) || {};
319
- const unsupportedTagsLengthCheck = validationResponse?.unsupportedTags?.length > 0;
320
- // if (type === 'title') {
321
- // setTemplateTitleError(
322
- // unsupportedTagsLengthCheck
323
- // ? formatMessage(globalMessages.unsupportedTagsValidationError, { unsupportedTags: validationResponse.unsupportedTags })
324
- // : validationResponse.isBraceError
325
- // ? formatMessage(globalMessages.unbalanacedCurlyBraces)
326
- // : false
327
- // );
328
- // } else if (type === 'description') {
329
- // setTemplateDescError(
330
- // unsupportedTagsLengthCheck
331
- // ? formatMessage(globalMessages.unsupportedTagsValidationError, { unsupportedTags: validationResponse.unsupportedTags })
332
- // : validationResponse.isBraceError
333
- // ? formatMessage(globalMessages.unbalanacedCurlyBraces)
334
- // : false
335
- // );
336
- // }
337
- if (type === titletype) {
338
- const errorMsg =
339
- (unsupportedTagsLengthCheck &&
340
- formatMessage(globalMessages.unsupportedTagsValidationError, {
341
- unsupportedTags: validationResponse.unsupportedTags,
342
- })) ||
343
- (validationResponse.isBraceError &&
344
- formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
345
- false;
346
- // Only update if changed to prevent flicker
347
- setTemplateTitleError((prev) => (prev !== errorMsg ? errorMsg : prev));
348
- } else if (type === descType) {
294
+ const unsupportedTagsLengthCheck =
295
+ validationResponse?.unsupportedTags?.length > 0;
349
296
  const errorMsg =
350
297
  (unsupportedTagsLengthCheck &&
351
298
  formatMessage(globalMessages.unsupportedTagsValidationError, {
@@ -354,55 +301,54 @@ export const Rcs = (props) => {
354
301
  (validationResponse.isBraceError &&
355
302
  formatMessage(globalMessages.unbalanacedCurlyBraces)) ||
356
303
  false;
357
- setTemplateDescError((prev) => (prev !== errorMsg ? errorMsg : prev));
358
- }
304
+ if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
305
+ if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
359
306
  };
360
307
 
361
308
  useEffect(() => {
362
- if (isFullMode) {
363
- return;
364
- }
365
- const hasInteraction = !isEmpty(cardVarMapped) || Object.values(rcsTextVariables || {}).some(group => Object.values(group || {}).some(v => (v || '').trim() !== ''));
366
- if (!hasInteraction) {
367
- setTemplateTitleError(false);
368
- return;
369
- }
370
- // If any current title variable input has unbalanced braces, show that error and skip tag validation
371
- const titleInputs = Object.values((rcsTextVariables || {})[TITLE_TEXT] || {});
372
- for (let i = 0; i < titleInputs.length; i += 1) {
373
- const v = titleInputs[i];
374
- const varErr = variableErrorHandling(v);
375
- if (varErr) {
376
- setTemplateTitleError(varErr);
377
- return;
378
- }
379
- }
380
- const mapped = getMappedDesc(templateTitle, cardVarMapped);
381
- tagValidation(mapped, titletype);
382
- }, [templateTitle, tags, injectedTags, cardVarMapped, rcsTextVariables, isFullMode]);
383
-
309
+ validateResolvedTagsForType(TITLE_TEXT);
310
+ }, [cardVarMapped, templateTitle, tags, injectedTags, loadingTags]);
311
+
384
312
  useEffect(() => {
385
- if (isFullMode) {
386
- return;
387
- }
388
- const hasInteraction = !isEmpty(cardVarMapped) || Object.values(rcsTextVariables || {}).some(group => Object.values(group || {}).some(v => (v || '').trim() !== ''));
389
- if (!hasInteraction) {
390
- setTemplateDescError(false);
391
- return;
392
- }
393
- // If any current message variable input has unbalanced braces, show that error and skip tag validation
394
- const msgInputs = Object.values((rcsTextVariables || {})[MESSAGE_TEXT] || {});
395
- for (let i = 0; i < msgInputs.length; i += 1) {
396
- const v = msgInputs[i];
397
- const varErr = variableErrorHandling(v);
398
- if (varErr) {
399
- setTemplateDescError(varErr);
400
- return;
313
+ validateResolvedTagsForType(MESSAGE_TEXT);
314
+ }, [cardVarMapped, templateDesc, tags, injectedTags, loadingTags]);
315
+
316
+ const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
317
+
318
+ const resolveTemplateWithMap = (str = '') => {
319
+ if (!str) return '';
320
+ const arr = splitTemplateVarString(str);
321
+ return arr.map((elem) => {
322
+ if (rcsVarTestRegex.test(elem)) {
323
+ const key = getVarNameFromToken(elem);
324
+ const v = cardVarMapped?.[key];
325
+ return (v ?? '').toString();
401
326
  }
402
- }
403
- const mapped = getMappedDesc(templateDesc, cardVarMapped);
404
- tagValidation(mapped, descType);
405
- }, [templateDesc, tags, injectedTags, cardVarMapped, rcsTextVariables, isFullMode]);
327
+ return elem;
328
+ }).join('');
329
+ };
330
+
331
+
332
+ useEffect(() => {
333
+ if (isFullMode) return;
334
+ const tokens = [
335
+ ...(templateTitle ? (templateTitle.match(rcsVarRegex) || []) : []),
336
+ ...(templateDesc ? (templateDesc.match(rcsVarRegex) || []) : []),
337
+ ];
338
+ if (!tokens.length) return;
339
+ setCardVarMapped((prev) => {
340
+ const next = { ...(prev || {}) };
341
+ let changed = false;
342
+ tokens.forEach((t) => {
343
+ const name = getVarNameFromToken(t);
344
+ if (name && !Object.prototype.hasOwnProperty.call(next, name)) {
345
+ next[name] = '';
346
+ changed = true;
347
+ }
348
+ });
349
+ return changed ? next : prev;
350
+ });
351
+ }, [isFullMode, templateTitle, templateDesc]);
406
352
 
407
353
 
408
354
  const RcsLabel = styled.div`
@@ -422,84 +368,38 @@ export const Rcs = (props) => {
422
368
  }, [paramObj.id]);
423
369
 
424
370
  useEffect(() => {
425
- // Clean up rcsTextVariables when templateDesc changes
426
- const descArray = splitTemplateVarString(templateDesc);
427
- const validIndexes = descArray
428
- .map((elem, idx) => (rcsVarTestRegex.test(elem) ? idx : null))
429
- .filter(idx => idx !== null);
430
- setRcsTextVariables(prev => {
431
- const updated = { ...prev };
432
- if (updated[MESSAGE_TEXT]) {
433
- Object.keys(updated[MESSAGE_TEXT]).forEach(idx => {
434
- if (!validIndexes.includes(Number(idx))) {
435
- delete updated[MESSAGE_TEXT][idx];
436
- }
437
- });
438
- }
439
- return updated;
440
- });
441
- }, [templateDesc]);
442
-
443
- useEffect(() => {
444
- // Clean up rcsTextVariables when templateTitle changes
445
- const titleArray = splitTemplateVarString(templateTitle);
446
- const validIndexes = titleArray
447
- .map((elem, idx) => (rcsVarTestRegex.test(elem) ? idx : null))
448
- .filter(idx => idx !== null);
449
- setRcsTextVariables(prev => {
450
- const updated = { ...prev };
451
- if (updated[TITLE_TEXT]) {
452
- Object.keys(updated[TITLE_TEXT]).forEach(idx => {
453
- if (!validIndexes.includes(Number(idx))) {
454
- delete updated[TITLE_TEXT][idx];
455
- }
456
- });
457
- }
458
- return updated;
459
- });
460
- }, [templateTitle]);
371
+ if (!(isEditFlow || !isFullMode)) return;
461
372
 
462
- // Prefill variable inputs from existing mapping on load/edit
463
- useEffect(() => {
464
- if (!templateTitle && !templateDesc) return;
465
- const buildPrefill = (targetString) => {
373
+ const initField = (targetString, currentVarMap, setVarMap, setUpdated) => {
466
374
  const arr = splitTemplateVarString(targetString);
467
- const pre = {};
375
+ if (!arr?.length) {
376
+ setVarMap({});
377
+ setUpdated([]);
378
+ return;
379
+ }
380
+ const nextVarMap = {};
381
+ const nextUpdated = [...arr];
468
382
  arr.forEach((elem, idx) => {
383
+ // RCS placeholders are alphanumeric/underscore (e.g. {{user_name}}), not numeric-only
469
384
  if (rcsVarTestRegex.test(elem)) {
470
- const varName = elem.replace(/^\{\{|\}\}$/g, '');
471
- const mapped = cardVarMapped[varName] ?? cardVarMapped[elem];
472
- if (mapped !== undefined && mapped.trim() !== '') {
473
- const value = /^\{\{[\s\S]*\}\}$/.test(mapped) ? mapped : `{{${mapped}}}`;
474
- pre[idx] = value;
385
+ const id = `${elem}_${idx}`;
386
+ const varName = getVarNameFromToken(elem);
387
+ const mappedValue = (cardVarMapped?.[varName] ?? '').toString();
388
+ nextVarMap[id] = mappedValue;
389
+ if (mappedValue !== '') {
390
+ nextUpdated[idx] = mappedValue;
391
+ } else {
392
+ nextUpdated[idx] = elem;
475
393
  }
476
394
  }
477
395
  });
478
- return pre;
396
+ setVarMap(nextVarMap);
397
+ setUpdated(nextUpdated);
479
398
  };
480
- const prefilledTitle = buildPrefill(templateTitle);
481
- const prefilledDesc = buildPrefill(templateDesc);
482
- if (Object.keys(prefilledTitle).length === 0 && Object.keys(prefilledDesc).length === 0) return;
483
- setRcsTextVariables(prev => {
484
- const next = { ...prev };
485
- // Only prefill if that slot is currently empty to avoid overwriting user typing
486
- next[TITLE_TEXT] = { ...(prev[TITLE_TEXT] || {}) };
487
- Object.keys(prefilledTitle).forEach((idx) => {
488
- const current = next[TITLE_TEXT][idx];
489
- if (!current || String(current).trim() === '') {
490
- next[TITLE_TEXT][idx] = prefilledTitle[idx];
491
- }
492
- });
493
- next[MESSAGE_TEXT] = { ...(prev[MESSAGE_TEXT] || {}) };
494
- Object.keys(prefilledDesc).forEach((idx) => {
495
- const current = next[MESSAGE_TEXT][idx];
496
- if (!current || String(current).trim() === '') {
497
- next[MESSAGE_TEXT][idx] = prefilledDesc[idx];
498
- }
499
- });
500
- return next;
501
- });
502
- }, [templateTitle, templateDesc, cardVarMapped]);
399
+
400
+ initField(templateTitle, titleVarMappedData, setTitleVarMappedData, setUpdatedTitleData);
401
+ initField(templateDesc, descVarMappedData, setDescVarMappedData, setUpdatedDescData);
402
+ }, [templateTitle, templateDesc, cardVarMapped, isEditFlow, isFullMode]);
503
403
 
504
404
  useEffect(() => {
505
405
  if(!isEditFlow && isFullMode){
@@ -685,106 +585,29 @@ export const Rcs = (props) => {
685
585
  globalActions.fetchSchemaForEntity(query);
686
586
  };
687
587
 
688
- const onTagSelect = (tag) => {
689
- // Determine target input (index and type). If nothing focused, pick the first available variable slot.
690
- let targetType = focusedVarType;
691
- let targetIndex;
692
- let tokenAtIndex = '';
693
- if (!focusedVarId) {
694
- // Try description first
695
- const descArray = splitTemplateVarString(templateDesc);
696
- let found = false;
697
- for (let i = 0; i < descArray.length; i += 1) {
698
- const elem = descArray[i];
699
- if (rcsVarTestRegex.test(elem)) {
700
- const currentVal = (rcsTextVariables?.[MESSAGE_TEXT]?.[i]) || '';
701
- if (!currentVal) {
702
- targetType = MESSAGE_TEXT;
703
- targetIndex = i;
704
- tokenAtIndex = elem;
705
- found = true;
706
- break;
707
- }
708
- }
709
- }
710
- // If no empty slot in description, try title
711
- if (!found) {
712
- const titleArray = splitTemplateVarString(templateTitle);
713
- for (let i = 0; i < titleArray.length; i += 1) {
714
- const elem = titleArray[i];
715
- if (rcsVarTestRegex.test(elem)) {
716
- const currentVal = (rcsTextVariables?.[TITLE_TEXT]?.[i]) || '';
717
- if (!currentVal) {
718
- targetType = TITLE_TEXT;
719
- targetIndex = i;
720
- tokenAtIndex = elem;
721
- found = true;
722
- break;
723
- }
724
- }
725
- }
726
- }
727
- if (!targetType || targetIndex === undefined) {
728
- CapNotification.error({
729
- message: formatMessage(messages.noVariableFocused) || "Please select a variable to insert the label.",
730
- });
731
- return;
732
- }
733
- } else {
734
- targetIndex = parseInt(focusedVarId.split('_').pop(), 10);
735
- const arr = splitTemplateVarString(targetType === TITLE_TEXT ? templateTitle : templateDesc);
736
- tokenAtIndex = arr[targetIndex] || '';
737
- }
738
-
739
- // Update the visual input field
740
- // First-time select from label: insert with braces
741
- setRcsTextVariables((prev) => {
742
- const next = {
743
- ...prev,
744
- [targetType]: {
745
- ...(prev[targetType] || {}),
746
- [targetIndex]: `{{${tag}}}`,
747
- },
748
- };
749
- return next;
750
- });
751
-
752
- // Store the variable-to-tag mapping
588
+ const onTagSelect = (data, areaId) => {
589
+ if (!areaId) return;
590
+ const sep = areaId.lastIndexOf('_');
591
+ if (sep === -1) return;
592
+ const numId = Number(areaId.slice(sep + 1));
593
+ if (isNaN(numId)) return;
594
+ const token = areaId.slice(0, sep);
595
+ const variableName = getVarNameFromToken(token);
596
+ if (!variableName) return;
753
597
  setCardVarMapped((prev) => {
754
- // Determine stable variable name
755
- const defaultVarName = (focusedVarId ? focusedVarId.split('_')[0] : tokenAtIndex).replace(/^\{\{|\}\}$/g, '');
756
- const tokenName = tokenAtIndex.replace(/^\{\{|\}\}$/g, '');
757
-
758
- // If this template had been saved with resolved tags, infer original key via inverse lookup
759
- let variableName = defaultVarName;
760
- if (prev && Object.keys(prev).length > 0) {
761
- const entry = Object.entries(prev).find(([, v]) => {
762
- const val = v || '';
763
- const braced = /^\{\{[\s\S]*\}\}$/.test(val) ? val : `{{${val}}}`;
764
- return braced === `{{${tokenName}}}`;
765
- });
766
- if (entry) {
767
- const [keyFromMap] = entry;
768
- variableName = keyFromMap;
769
- }
770
- }
771
-
772
- const mappedValue = isFullMode ? tag : `{{${tag}}}`; // brace only on select
773
-
774
- // Clean up any accidental keys equal to the tag token name (e.g., 'gt') to avoid duplicates
775
- const cleaned = { ...prev };
776
- if (tokenName && Object.prototype.hasOwnProperty.call(cleaned, tokenName)) {
777
- delete cleaned[tokenName];
778
- }
779
-
598
+ const base = (prev?.[variableName] ?? '').toString();
599
+ const nextVal = `${base}{{${data}}}`;
780
600
  return {
781
- ...cleaned,
782
- [variableName]: mappedValue,
601
+ ...(prev || {}),
602
+ [variableName]: nextVal,
783
603
  };
784
604
  });
785
-
786
605
  };
787
606
 
607
+ const onTitleTagSelect = (data) => onTagSelect(data, titleTextAreaId);
608
+
609
+ const onDescTagSelect = (data) => onTagSelect(data, descTextAreaId);
610
+
788
611
  const onTagSelectFallback = (data) => {
789
612
  const tempMsg = `${fallbackMessage}{{${data}}}`;
790
613
  const error = fallbackMessageErrorHandler(tempMsg);
@@ -1097,79 +920,43 @@ const splitTemplateVarString = (str) => {
1097
920
  return templateVarArray.filter(Boolean);
1098
921
  };
1099
922
 
1100
- const textAreaValue = (index, type) => {
1101
- return rcsTextVariables?.[type]?.[index] || "";
923
+ const textAreaValue = (idValue, type) => {
924
+ if (idValue >= 0) {
925
+ const templateStr = type === TITLE_TEXT ? templateTitle : templateDesc;
926
+ const templateArr = splitTemplateVarString(templateStr);
927
+ const token = templateArr?.[idValue] || "";
928
+ if (token && rcsVarTestRegex.test(token)) {
929
+ const varName = getVarNameFromToken(token);
930
+ return (cardVarMapped?.[varName] ?? '').toString();
931
+ }
932
+ return "";
933
+ }
934
+ return "";
1102
935
  };
1103
936
 
1104
937
  const textAreaValueChange = (e, type) => {
1105
- const value = e.target.value;
1106
- const inputId = e.target.id;
1107
- const index = parseInt(inputId.split("_").pop(), 10);
1108
-
1109
- setRcsTextVariables((prev) => {
1110
- const updated = {
1111
- ...prev,
1112
- [type]: {
1113
- ...(prev[type] || {}),
1114
- [index]: value,
1115
- },
1116
- };
1117
-
1118
- let reconstructed = '';
1119
- let varCount = 0;
1120
- const targetString = type === TITLE_TEXT ? templateTitle : templateDesc;
1121
- splitTemplateVarString(targetString).forEach((elem, idx) => {
1122
- if (rcsVarTestRegex.test(elem)) {
1123
- varCount += 1;
1124
- reconstructed += updated[type]?.[idx] || '';
1125
- } else {
1126
- reconstructed += elem;
1127
- }
1128
- });
1129
-
1130
- if (isFullMode) {
1131
- // Creation flow: validate only variables using variableErrorHandling
1132
- if (type === TITLE_TEXT) {
1133
- const err = variableErrorHandling(reconstructed) || templateHeaderErrorHandler(reconstructed);
1134
- setTemplateTitleError(err);
1135
- } else {
1136
- const err = variableErrorHandling(reconstructed) || templateDescErrorHandler(reconstructed);
1137
- setTemplateDescError(err);
1138
- }
1139
- } else {
1140
- // Edit/library flow: validate only tags
1141
- if (type === TITLE_TEXT) {
1142
- tagValidation(reconstructed, titletype);
1143
- } else {
1144
- tagValidation(reconstructed, descType);
1145
- }
938
+ const value = e?.target?.value ?? '';
939
+ const id = e?.target?.id || e?.currentTarget?.id || '';
940
+ if (!id) return;
941
+ const sep = id.lastIndexOf('_');
942
+ if (sep === -1) return;
943
+ const isInvalidValue = value?.trim() === "";
944
+ const token = id.slice(0, sep);
945
+ const variableName = getVarNameFromToken(token);
946
+
947
+ if (variableName) {
948
+ setCardVarMapped((prev) => ({
949
+ ...prev,
950
+ [variableName]: isInvalidValue ? "" : value,
951
+ }));
1146
952
  }
1147
-
1148
- return updated;
1149
- });
1150
-
1151
- // Keep mapping in sync with what user types in the variable input
1152
- const targetStringForMap = type === TITLE_TEXT ? templateTitle : templateDesc;
1153
- const arrForMap = splitTemplateVarString(targetStringForMap);
1154
- const token = arrForMap[index] || '';
1155
- if (rcsVarTestRegex.test(token)) {
1156
- const variableName = token.replace(/^\{\{|\}\}$/g, '');
1157
- // In !isFullMode, do not auto-wrap while typing; store raw input as-is
1158
- const normalized = isFullMode
1159
- ? (value ? (/^\{\{[\s\S]*\}\}$/.test(value) ? value : `{{${value}}}`) : '')
1160
- : (value || '');
1161
- setCardVarMapped(prev => {
1162
- const next = { ...prev };
1163
- // Keep the variable key even when empty; store empty string as value
1164
- next[variableName] = (normalized && normalized.trim() !== '') ? normalized : '';
1165
- return next;
1166
- });
1167
- }
1168
953
  };
1169
954
 
1170
955
  const setTextAreaId = (e, type) => {
1171
- setFocusedVarId(e.target.id);
1172
- setFocusedVarType(type);
956
+ const id = e?.target?.id || e?.currentTarget?.id || '';
957
+ if (!id) return;
958
+ if (type === TITLE_TEXT) setTitleTextAreaId(id);
959
+ else setDescTextAreaId(id);
1173
960
  };
1174
961
 
1175
962
  const renderButtonComponent = () => {
@@ -1257,7 +1044,7 @@ const splitTemplateVarString = (str) => {
1257
1044
  {(isEditFlow || !isFullMode) ? (
1258
1045
  <TagList
1259
1046
  label={formatMessage(globalMessages.addLabels)}
1260
- onTagSelect={onTagSelect}
1047
+ onTagSelect={onTitleTagSelect}
1261
1048
  location={location}
1262
1049
  tags={getRcsTags()}
1263
1050
  onContextChange={handleOnTagsContextChange}
@@ -1319,7 +1106,7 @@ const splitTemplateVarString = (str) => {
1319
1106
  (isEditFlow || !isFullMode) ? (
1320
1107
  <TagList
1321
1108
  label={formatMessage(globalMessages.addLabels)}
1322
- onTagSelect={onTagSelect}
1109
+ onTagSelect={onDescTagSelect}
1323
1110
  location={location}
1324
1111
  tags={getRcsTags()}
1325
1112
  onContextChange={handleOnTagsContextChange}
@@ -1484,12 +1271,13 @@ const splitTemplateVarString = (str) => {
1484
1271
  if (hasTagInMapped) return true;
1485
1272
  }
1486
1273
 
1487
- // Check rcsTextVariables values for tags
1488
- if (rcsTextVariables && Object.keys(rcsTextVariables).length > 0) {
1489
- const hasTagInVariables = Object.values(rcsTextVariables).some(typeGroup =>
1490
- Object.values(typeGroup || {}).some(value => isTagIncluded(value))
1491
- );
1492
- if (hasTagInVariables) return true;
1274
+ if (titleVarMappedData && Object.keys(titleVarMappedData).length > 0) {
1275
+ const hasTagInTitle = Object.values(titleVarMappedData).some((value) => isTagIncluded(value));
1276
+ if (hasTagInTitle) return true;
1277
+ }
1278
+ if (descVarMappedData && Object.keys(descVarMappedData).length > 0) {
1279
+ const hasTagInDesc = Object.values(descVarMappedData).some((value) => isTagIncluded(value));
1280
+ if (hasTagInDesc) return true;
1493
1281
  }
1494
1282
 
1495
1283
  return false;
@@ -1942,6 +1730,8 @@ const splitTemplateVarString = (str) => {
1942
1730
  return (
1943
1731
  <>
1944
1732
  {renderLabel('cardOrientationLabel')}
1733
+ {/* Match image behavior: allow changing dimensions only in full-mode create flow */}
1734
+ {!isEditFlow && isFullMode && (
1945
1735
  <CapSelect
1946
1736
  id="rcs-dimension-select"
1947
1737
  value={currentDimension}
@@ -1955,6 +1745,7 @@ const splitTemplateVarString = (str) => {
1955
1745
  }))}
1956
1746
  style={{ marginBottom: '20px' }}
1957
1747
  />
1748
+ )}
1958
1749
  {(isEditFlow || !isFullMode) ? (
1959
1750
  <div className="rcs-video-preview">
1960
1751
  <CapImage
@@ -2012,9 +1803,8 @@ const splitTemplateVarString = (str) => {
2012
1803
  const getRcsPreview = () => {
2013
1804
 
2014
1805
  const dimensionObj = RCS_IMAGE_DIMENSIONS[selectedDimension];
2015
- // Always show resolved values in preview
2016
- const mappedDesc = getMappedDesc(templateDesc, cardVarMapped);
2017
- const mappedTitle = getMappedDesc(templateTitle, cardVarMapped);
1806
+ const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1807
+ const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
2018
1808
  return (
2019
1809
  <TemplatePreview
2020
1810
  channel={RCS}
@@ -2022,8 +1812,8 @@ const splitTemplateVarString = (str) => {
2022
1812
  rcsOrientation={dimensionObj?.orientation}
2023
1813
  content={{
2024
1814
  rcsPreviewContent: {
2025
- rcsTitle: mappedTitle,
2026
- rcsDesc: mappedDesc,
1815
+ rcsTitle: resolvedTitle,
1816
+ rcsDesc: resolvedDesc,
2027
1817
  ...(!isMediaTypeText && {
2028
1818
  rcsImageSrc,
2029
1819
  rcsVideoSrc: rcsThumbnailSrc,
@@ -2034,26 +1824,6 @@ const splitTemplateVarString = (str) => {
2034
1824
  />
2035
1825
  );
2036
1826
  };
2037
-
2038
- const getMappedDesc = (str, cardVarMapped) => {
2039
- if (!str) return '';
2040
- const descArray = splitTemplateVarString(str);
2041
- return descArray.map((elem, index) => {
2042
- if (rcsVarTestRegex.test(elem)) {
2043
- // Extract just the variable name (e.g., "{{1}}" -> "1")
2044
- const variableName = elem.replace(/^\{\{|\}\}$/g, '');
2045
- // Support both unbraced and braced keys
2046
- const mapped = cardVarMapped[variableName] ?? cardVarMapped[elem];
2047
- // In edit mode (!isFullMode), if mapped value is empty, return original variable placeholder
2048
- if (!mapped || mapped.trim() === '') {
2049
- return elem; // Return original {{1}} instead of {{}}
2050
- }
2051
- const hasBraces = /^\{\{[\s\S]*\}\}$/.test(mapped);
2052
- return hasBraces ? mapped : `{{${mapped}}}`;
2053
- }
2054
- return elem;
2055
- }).join('');
2056
- };
2057
1827
 
2058
1828
  const getUnmappedDesc = (str, mapping) => {
2059
1829
  if (!str) return '';
@@ -2078,8 +1848,8 @@ const splitTemplateVarString = (str) => {
2078
1848
  'sms-editor': template = '',
2079
1849
  header: registeredSenderIds = [],
2080
1850
  } = base;
2081
- const mappedDesc = getMappedDesc(templateDesc, cardVarMapped);
2082
- const mappedTitle = getMappedDesc(templateTitle, cardVarMapped);
1851
+ const resolvedTitle = !isFullMode ? resolveTemplateWithMap(templateTitle) : templateTitle;
1852
+ const resolvedDesc = !isFullMode ? resolveTemplateWithMap(templateDesc) : templateDesc;
2083
1853
  const alignment = isMediaTypeImage
2084
1854
  ? RCS_IMAGE_DIMENSIONS[selectedDimension]?.alignment
2085
1855
  : RCS_VIDEO_THUMBNAIL_DIMENSIONS[selectedDimension]?.alignment;
@@ -2099,8 +1869,8 @@ const splitTemplateVarString = (str) => {
2099
1869
  },
2100
1870
  cardContent: [
2101
1871
  {
2102
- title: mappedTitle,
2103
- description: mappedDesc,
1872
+ title: resolvedTitle,
1873
+ description: resolvedDesc,
2104
1874
  mediaType: templateMediaType,
2105
1875
  ...(!isMediaTypeText && {media: {
2106
1876
  mediaUrl: rcsImageSrc || rcsVideoSrc.videoSrc || '',
@@ -800,6 +800,174 @@ describe('RCS createPayload', () => {
800
800
  // cardVarMapped should exist on non-full mode (may be empty)
801
801
  expect(Object.prototype.hasOwnProperty.call(card, 'cardVarMapped')).toBe(true);
802
802
  });
803
+
804
+ it('should sync duplicate placeholders via cardVarMapped in non-full mode (typing once updates all occurrences)', () => {
805
+ const templateData = {
806
+ name: 'DupVarsTemplate',
807
+ versions: {
808
+ base: {
809
+ content: {
810
+ RCS: {
811
+ rcsContent: {
812
+ cardType: 'STANDALONE',
813
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
814
+ cardContent: [
815
+ {
816
+ // same placeholder appears twice
817
+ title: 'Hello {{user_name}} and {{user_name}}',
818
+ description: 'Hi {{user_name}}',
819
+ mediaType: 'NONE',
820
+ cardVarMapped: {}, // empty on load
821
+ suggestions: [],
822
+ },
823
+ ],
824
+ contentType: 'RICHCARD',
825
+ },
826
+ },
827
+ },
828
+ },
829
+ },
830
+ type: 'RCS',
831
+ };
832
+
833
+ const wrapper = mountWithIntl(
834
+ <Provider store={store}>
835
+ <Rcs
836
+ actions={{
837
+ clearCreateResponse: jest.fn(),
838
+ getTemplateDetails: jest.fn(),
839
+ uploadRcsAsset: jest.fn(),
840
+ clearRcsMediaAsset: jest.fn(),
841
+ editTemplate: jest.fn(),
842
+ clearEditResponse: jest.fn(),
843
+ }}
844
+ globalActions={{ fetchSchemaForEntity }}
845
+ onCreateComplete={onCreateComplete}
846
+ handleClose={handleClose}
847
+ intl={{ formatMessage }}
848
+ location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
849
+ params={params}
850
+ templateData={templateData}
851
+ rcsData={{}}
852
+ isFullMode={false}
853
+ isEditFlow={false}
854
+ loadingTags={false}
855
+ metaEntities={[]}
856
+ isDltEnabled={false}
857
+ smsRegister={'DLT'}
858
+ getFormData={jest.fn()}
859
+ />
860
+ </Provider>,
861
+ );
862
+
863
+ // Find both Title variable textareas for {{user_name}} and ensure they stay in sync
864
+ const titleVarAreas = wrapper.find('TextArea').filterWhere((n) => {
865
+ const id = n.prop('id') || '';
866
+ return id.includes('{{user_name}}_');
867
+ });
868
+ expect(titleVarAreas.length).toBeGreaterThanOrEqual(2);
869
+
870
+ act(() => {
871
+ const id = titleVarAreas.at(0).prop('id');
872
+ titleVarAreas.at(0).props().onChange({ target: { id, value: 'Alice' } });
873
+ });
874
+ wrapper.update();
875
+
876
+ const updatedTitleVarAreas = wrapper.find('TextArea').filterWhere((n) => {
877
+ const id = n.prop('id') || '';
878
+ return id.includes('{{user_name}}_');
879
+ });
880
+
881
+ // Both occurrences should show the same value (from cardVarMapped.user_name)
882
+ expect(updatedTitleVarAreas.at(0).prop('value')).toBe('Alice');
883
+ expect(updatedTitleVarAreas.at(1).prop('value')).toBe('Alice');
884
+ });
885
+
886
+ it('should insert TagList label into focused placeholder and sync duplicates in non-full mode', () => {
887
+ const templateData = {
888
+ name: 'DupVarsTemplate2',
889
+ versions: {
890
+ base: {
891
+ content: {
892
+ RCS: {
893
+ rcsContent: {
894
+ cardType: 'STANDALONE',
895
+ cardSettings: { cardOrientation: 'VERTICAL', cardWidth: 'SMALL' },
896
+ cardContent: [
897
+ {
898
+ title: 'Hello {{user_name}} and {{user_name}}',
899
+ description: 'Hi {{user_name}}',
900
+ mediaType: 'NONE',
901
+ cardVarMapped: {},
902
+ suggestions: [],
903
+ },
904
+ ],
905
+ contentType: 'RICHCARD',
906
+ },
907
+ },
908
+ },
909
+ },
910
+ },
911
+ type: 'RCS',
912
+ };
913
+
914
+ const wrapper = mountWithIntl(
915
+ <Provider store={store}>
916
+ <Rcs
917
+ actions={{
918
+ clearCreateResponse: jest.fn(),
919
+ getTemplateDetails: jest.fn(),
920
+ uploadRcsAsset: jest.fn(),
921
+ clearRcsMediaAsset: jest.fn(),
922
+ editTemplate: jest.fn(),
923
+ clearEditResponse: jest.fn(),
924
+ }}
925
+ globalActions={{ fetchSchemaForEntity }}
926
+ onCreateComplete={onCreateComplete}
927
+ handleClose={handleClose}
928
+ intl={{ formatMessage }}
929
+ location={{ pathname: '/rcs/create', query: { type: false, module: 'default' }, search: '' }}
930
+ params={params}
931
+ templateData={templateData}
932
+ rcsData={{}}
933
+ isFullMode={false}
934
+ isEditFlow={false}
935
+ loadingTags={false}
936
+ metaEntities={[]}
937
+ isDltEnabled={false}
938
+ smsRegister={'DLT'}
939
+ getFormData={jest.fn()}
940
+ />
941
+ </Provider>,
942
+ );
943
+
944
+ const titleVarAreas = wrapper.find('TextArea').filterWhere((n) => {
945
+ const id = n.prop('id') || '';
946
+ return id.includes('{{user_name}}_');
947
+ });
948
+ expect(titleVarAreas.length).toBeGreaterThanOrEqual(2);
949
+
950
+ // Focus the first variable textarea so TagList knows where to insert
951
+ act(() => {
952
+ const id = titleVarAreas.at(0).prop('id');
953
+ titleVarAreas.at(0).props().onFocus({ target: { id } });
954
+ });
955
+ wrapper.update();
956
+
957
+ // Trigger TagList selection (mocked TagList calls onTagSelect with string)
958
+ const tagList = wrapper.find('.tag-mock').at(0);
959
+ act(() => {
960
+ tagList.props().onTagSelect('first_name');
961
+ });
962
+ wrapper.update();
963
+
964
+ const updatedTitleVarAreas = wrapper.find('TextArea').filterWhere((n) => {
965
+ const id = n.prop('id') || '';
966
+ return id.includes('{{user_name}}_');
967
+ });
968
+ expect(updatedTitleVarAreas.at(0).prop('value')).toBe('{{first_name}}');
969
+ expect(updatedTitleVarAreas.at(1).prop('value')).toBe('{{first_name}}');
970
+ });
803
971
  });
804
972
 
805
973
  describe('Character Counting Functions', () => {