@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 +1 -1
- package/v2Containers/Rcs/index.js +153 -383
- package/v2Containers/Rcs/tests/index.test.js +168 -0
package/package.json
CHANGED
|
@@ -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 [
|
|
187
|
-
const [
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
301
|
-
|
|
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 =
|
|
320
|
-
|
|
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
|
-
|
|
358
|
-
|
|
304
|
+
if (type === TITLE_TEXT) setTemplateTitleError(errorMsg);
|
|
305
|
+
if (type === MESSAGE_TEXT) setTemplateDescError(errorMsg);
|
|
359
306
|
};
|
|
360
307
|
|
|
361
308
|
useEffect(() => {
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
471
|
-
const
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
396
|
+
setVarMap(nextVarMap);
|
|
397
|
+
setUpdated(nextUpdated);
|
|
479
398
|
};
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
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 = (
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
if (
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
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
|
-
|
|
755
|
-
const
|
|
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
|
-
...
|
|
782
|
-
[variableName]:
|
|
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 = (
|
|
1101
|
-
|
|
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
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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={
|
|
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={
|
|
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
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
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
|
-
|
|
2016
|
-
const
|
|
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:
|
|
2026
|
-
rcsDesc:
|
|
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
|
|
2082
|
-
const
|
|
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:
|
|
2103
|
-
description:
|
|
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', () => {
|