@capillarytech/creatives-library 9.0.26 → 9.0.29-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +260 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +770 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +108 -15
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +328 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/Email/index.js +3 -54
  96. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +4 -9
  97. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  98. package/v2Containers/Rcs/carouselUtils.js +224 -0
  99. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  100. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  101. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  102. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  103. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  104. package/v2Containers/Rcs/constants.js +132 -16
  105. package/v2Containers/Rcs/index.js +1808 -949
  106. package/v2Containers/Rcs/index.scss +443 -8
  107. package/v2Containers/Rcs/messages.js +45 -22
  108. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +484 -0
  109. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  110. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  111. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  112. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  113. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  114. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74176 -39409
  115. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  116. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  117. package/v2Containers/Rcs/tests/index.test.js +219 -40
  118. package/v2Containers/Rcs/tests/mockData.js +38 -0
  119. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +873 -0
  120. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  121. package/v2Containers/Rcs/utils.js +514 -12
  122. package/v2Containers/Sms/Create/index.js +115 -48
  123. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  124. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  125. package/v2Containers/SmsTrai/Create/index.js +9 -4
  126. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  127. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  128. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  129. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  130. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  131. package/v2Containers/SmsWrapper/index.js +37 -8
  132. package/v2Containers/TagList/index.js +5 -0
  133. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  134. package/v2Containers/Templates/_templates.scss +196 -12
  135. package/v2Containers/Templates/actions.js +11 -0
  136. package/v2Containers/Templates/constants.js +2 -0
  137. package/v2Containers/Templates/index.js +131 -59
  138. package/v2Containers/Templates/sagas.js +57 -13
  139. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  140. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  141. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  142. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  143. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  144. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  145. package/v2Containers/TemplatesV2/index.js +88 -25
  146. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  147. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  148. package/v2Containers/Whatsapp/index.js +3 -20
  149. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -237,39 +237,6 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
237
237
  this.props.actions.getCmsSetting(BEE_PLUGIN, dragDropId, 'create', undefined, isBEESupport, isBEEAppEnable);
238
238
  }
239
239
  }
240
- // The <Email> direct path (SlideBoxContent routes here when
241
- // templateData.base.is_drag_drop is true) bypasses EmailWrapper/useEmailWrapper,
242
- // so Templates.BEETemplate is never seeded and a created/edited drag-drop would
243
- // open as the HTML editor. Seed isDragDrop + the BEE plugin from the inline
244
- // templateData so it opens as BEE.
245
- if (_.isEmpty(this.props.Templates.BEETemplate) && !_.isEmpty(this.props.templateData)) {
246
- const td = this.props.templateData;
247
- const activeTab = _.get(td, 'versions.base.activeTab', 'en');
248
- // This path bypasses setEditData, so seed the subject from the inline
249
- // templateData too (otherwise the title is empty for a created drag-drop).
250
- const inlineSubject = _.get(td, 'base.subject')
251
- || _.get(td, `versions.base.${activeTab}.subject`)
252
- || _.get(td, 'versions.base.subject')
253
- || _.get(td, 'emailSubject')
254
- || _.get(td, 'subject');
255
- if (inlineSubject) {
256
- formData['template-subject'] = inlineSubject;
257
- }
258
- const inlineIsDragDrop = _.get(td, 'base.is_drag_drop')
259
- || _.get(td, `versions.base.${activeTab}.is_drag_drop`)
260
- || _.get(td, 'versions.base.is_drag_drop')
261
- || _.get(td, 'is_drag_drop');
262
- if ((inlineIsDragDrop === true || inlineIsDragDrop === 1) && isBEEAppEnable) {
263
- this.setState({ isDragDrop: true });
264
- const inlineDragDropId = _.get(td, 'base.drag_drop_id')
265
- || _.get(td, 'base.id')
266
- || _.get(td, `versions.base.${activeTab}.drag_drop_id`)
267
- || _.get(td, `versions.base.${activeTab}.id`)
268
- || _.get(td, 'drag_drop_id')
269
- || _.get(td, 'id');
270
- this.props.actions.getCmsSetting(BEE_PLUGIN, inlineDragDropId, this.props.params.id ? 'open' : 'create', undefined, isBEESupport, isBEEAppEnable);
271
- }
272
- }
273
240
  const hasUploadedContent = !this.props.params?.id && this.props.Templates?.selectedEmailLayout && !_.isEmpty(formData);
274
241
  this.setState({
275
242
  content: (this.props.Templates.selectedEmailLayout ? this.props.Templates.selectedEmailLayout : ''),
@@ -586,21 +553,10 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
586
553
  const beeToken = `BEEeditor${currentTab > 1 ? currentTab : ''}token`;
587
554
  let beeJsonValue = '';
588
555
  const selectedId = _.get(this.props, 'Email.templateDetails._id', '') || _.get(this.props, 'Templates.BEETemplate.versions.base.drag_drop_id', '');
589
-
590
- // Embedded journey content: the inline templateData carries the user's
591
- // edited BEE json (base.json_content), which is more current than the
592
- // reusable template fetched via getTemplateDetails. Prefer it so a
593
- // re-edit shows the saved edit instead of the original template. For
594
- // the create/gallery flow this key is absent, so we fall through to the
595
- // existing sources (template details / BEETemplate) unchanged.
596
- beeJsonValue = _.get(nextProps, 'templateData.base.json_content', '')
597
- || _.get(nextProps, 'templateData.json_content', '');
598
-
556
+
599
557
  // Get beeJsonValue from template data - check multiple sources
600
558
  // First check if it's already in formData (from setEditData)
601
- if (!beeJsonValue) {
602
- beeJsonValue = _.get(this.state, `formData[${currentTab - 1}][${langId}].json-content`, '');
603
- }
559
+ beeJsonValue = _.get(this.state, `formData[${currentTab - 1}][${langId}].json-content`, '');
604
560
 
605
561
  // Also check formData.base
606
562
  if (!beeJsonValue) {
@@ -1183,14 +1139,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1183
1139
  const type = this.props.location.query.type;
1184
1140
 
1185
1141
  formData['template-name'] = editData.name;
1186
- // Prefer the inline edited subject (embedded journey content: base.subject /
1187
- // emailSubject) over the fetched template's subject, so subject edits persist
1188
- // on re-open. setEditData runs once (guarded by editDataSet), so this is a
1189
- // one-time load-time override and won't fight typing in the subject field.
1190
- const inlineSubject = _.get(this.props, 'templateData.base.subject', '')
1191
- || _.get(this.props, 'templateData.emailSubject', '')
1192
- || _.get(this.props, 'templateData.subject', '');
1193
- const subject = inlineSubject || _.get(editData, 'versions.base.subject', '');
1142
+ const subject = _.get(editData, 'versions.base.subject', '');
1194
1143
  formData['template-subject'] = subject;
1195
1144
  formData.base = editData.versions.base;
1196
1145
 
@@ -234,18 +234,13 @@ const useEmailWrapper = ({
234
234
  // New flow: When template details are loaded and it's a BEE template, set it in Templates.BEETemplate
235
235
  // This allows Email component to properly initialize and call getCmsSetting
236
236
  const hasTemplateDetails = Email?.templateDetails && !isEmpty(Email.templateDetails);
237
- // Inline templateData (e.g. a freshly-created drag-drop content, or a
238
- // library add-creative -> select-template) is authoritative when no template
239
- // details were fetched. Without seeding BEETemplate from it, setBEETemplate
240
- // never fires for that flow and a BEE template opens as the HTML editor.
241
- const hasTemplateDataProp = templateData && !isEmpty(templateData);
242
237
  // Note: We check Email?.BEETemplate as a proxy, but the actual BEETemplate is in Templates reducer
243
238
  // The Email component will detect it via this.props.Templates.BEETemplate
244
239
  const hasBEETemplate = Email?.BEETemplate && !isEmpty(Email.BEETemplate);
245
240
 
246
- if ((hasTemplateDetails || hasTemplateDataProp) && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
241
+ if (hasTemplateDetails && !hasBEETemplate && templatesActions?.setBEETemplate && !beeTemplateSetRef.current) {
247
242
  // Check if it's a BEE template
248
- const editTemplateData = hasTemplateDetails ? Email.templateDetails : templateData;
243
+ const editTemplateData = Email.templateDetails;
249
244
  const getIsDragDrop = (data) => {
250
245
  if (!data) return false;
251
246
  const baseDragDrop = get(data, 'versions.base.is_drag_drop', false);
@@ -301,10 +296,10 @@ const useEmailWrapper = ({
301
296
  }
302
297
 
303
298
  // Reset ref when template changes (template details cleared)
304
- if (!hasTemplateDetails && !hasTemplateDataProp && beeTemplateSetRef.current) {
299
+ if (!hasTemplateDetails && beeTemplateSetRef.current) {
305
300
  beeTemplateSetRef.current = false;
306
301
  }
307
- }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode, templateData]);
302
+ }, [Email?.templateDetails, Email?.BEETemplate, Email?.getTemplateDetailsInProgress, Email?.fetchingCmsSettings, templatesActions, emailActions, params?.id, location?.query, checkBeeEditorEnabled, isFullMode]);
308
303
 
309
304
  const onChange = useCallback((e) => {
310
305
  const { target: { value } } = e;
@@ -7,12 +7,12 @@ import {
7
7
 
8
8
  describe("MPUSH Sagas", () => {
9
9
  describe("v2MobilePushCreateSagas Combined", () => {
10
- it.concurrent("should initialize all mpush-related watcher sagas without error", () => {
10
+ it("should initialize all mpush-related watcher sagas without error", () => {
11
11
  return expectSaga(v2MobilePushCreateSagas).run();
12
12
  });
13
13
  });
14
14
  describe("v2MobilePushWatchDuplicateTemplateSaga Combined", () => {
15
- it.concurrent("should initialize all mpush-related watcher sagas without error", () => {
15
+ it("should initialize all mpush-related watcher sagas without error", () => {
16
16
  return expectSaga(v2MobilePushWatchDuplicateTemplateSaga).run();
17
17
  });
18
18
  });
@@ -0,0 +1,224 @@
1
+ import cloneDeep from 'lodash/cloneDeep';
2
+ import { isUrl, isValidText } from '../Line/Container/Wrapper/utils';
3
+ import {
4
+ invalidVarRegex,
5
+ RCS_CTA_URL_TYPE,
6
+ URL_MAX_LENGTH,
7
+ } from '../../v2Components/CapActionButton/constants';
8
+ import {
9
+ RCS_MEDIA_TYPES,
10
+ RCS_BUTTON_TYPES,
11
+ TEMPLATE_TITLE_MAX_LENGTH,
12
+ RCS_RICH_CARD_MAX_LENGTH,
13
+ RCS_NUMERIC_VAR_TOKEN_REGEX,
14
+ RCS_CAROUSEL_ASSET_INDEX_BASE,
15
+ RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS,
16
+ MEDIUM,
17
+ } from './constants';
18
+ import { sanitizeCardVarMappedValue } from './utils';
19
+
20
+ const getVarNameFromToken = (token = '') => token.replace(/^\{\{|\}\}$/g, '');
21
+
22
+ export const getCarouselImageAssetIndex = (cardIndex) =>
23
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3);
24
+ export const getCarouselVideoAssetIndex = (cardIndex) =>
25
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 1;
26
+ export const getCarouselThumbnailAssetIndex = (cardIndex) =>
27
+ RCS_CAROUSEL_ASSET_INDEX_BASE + (cardIndex * 3) + 2;
28
+
29
+ export const isThumbnailAssetIndex = (index) => {
30
+ if (index === 1) return true; // standalone thumbnail
31
+ if (index >= RCS_CAROUSEL_ASSET_INDEX_BASE) {
32
+ return ((index - RCS_CAROUSEL_ASSET_INDEX_BASE) % 3) === 2; // carousel thumbnail slot
33
+ }
34
+ return false;
35
+ };
36
+
37
+ /** Carousel dimension key: `${HEIGHT}_${WIDTH}` (e.g., SHORT_SMALL) */
38
+ export const getCarouselDimensionKey = (selectedCarouselHeight, selectedCarouselWidth) =>
39
+ `${selectedCarouselHeight}_${selectedCarouselWidth}`;
40
+
41
+ export const ensureFirstCardDefaultPhoneSuggestions = (cards) => {
42
+ const next = cloneDeep(cards || []);
43
+ if (next.length === 0) return next;
44
+ const suggestions = next[0].suggestions;
45
+ if (!Array.isArray(suggestions) || suggestions.length === 0) {
46
+ next[0] = {
47
+ ...next[0],
48
+ suggestions: cloneDeep(RCS_CAROUSEL_FIRST_CARD_DEFAULT_SUGGESTIONS),
49
+ };
50
+ }
51
+ return next;
52
+ };
53
+
54
+ export const carouselButtonTextHasForbiddenChars = (value) => {
55
+ if (!value) return false;
56
+ if (value.includes('[') || value.includes(']')) return true;
57
+ const withoutValidVariables = value.replace(/\{\{[^}]*\}\}/g, '');
58
+ if (withoutValidVariables.includes('{') || withoutValidVariables.includes('}')) return true;
59
+ return false;
60
+ };
61
+
62
+ export const isCompleteSavedCarouselSuggestion = (suggestion) => {
63
+ if (!suggestion || !suggestion.isSaved) return false;
64
+ const text = (suggestion.text || '').trim();
65
+ if (!text || !isValidText(text) || carouselButtonTextHasForbiddenChars(text)) return false;
66
+ if (suggestion.type === RCS_BUTTON_TYPES.PHONE_NUMBER) {
67
+ return String(suggestion.phoneNumber || '').length >= 5;
68
+ }
69
+ if (suggestion.type === RCS_BUTTON_TYPES.CTA) {
70
+ const url = String(suggestion.url || '').trim();
71
+ if (!url || url.length > URL_MAX_LENGTH) return false;
72
+ const subtype = suggestion.urlType || RCS_CTA_URL_TYPE.STATIC;
73
+ if (subtype === RCS_CTA_URL_TYPE.DYNAMIC) {
74
+ return true;
75
+ }
76
+ if (!isUrl(url)) return false;
77
+ const varMatches = url.match(invalidVarRegex);
78
+ return !(varMatches && varMatches.length > 0);
79
+ }
80
+ if (suggestion.type === RCS_BUTTON_TYPES.QUICK_REPLY) {
81
+ return true;
82
+ }
83
+ return false;
84
+ };
85
+
86
+ export const isCarouselCardValid = (card, cardIndex) => {
87
+ if (!card) return false;
88
+ if (!card.title || !card.title.trim()) return false;
89
+ if ((card.title || '').length > TEMPLATE_TITLE_MAX_LENGTH) return false;
90
+ if (!card.description || !card.description.trim()) return false;
91
+ if ((card.description || '').length > RCS_RICH_CARD_MAX_LENGTH) return false;
92
+ let mediaOk = false;
93
+ if (card.mediaType === RCS_MEDIA_TYPES.IMAGE) {
94
+ mediaOk = !!(card.imageSrc && String(card.imageSrc).trim());
95
+ } else if (card.mediaType === RCS_MEDIA_TYPES.VIDEO) {
96
+ const hasVideo = !!(card.videoAsset && card.videoAsset.videoSrc && String(card.videoAsset.videoSrc).trim());
97
+ const hasThumb = !!(card.thumbnailSrc && String(card.thumbnailSrc).trim());
98
+ mediaOk = hasVideo && hasThumb;
99
+ } else {
100
+ return false;
101
+ }
102
+ if (!mediaOk) return false;
103
+ if (cardIndex === 0) {
104
+ const sugg = Array.isArray(card.suggestions) ? card.suggestions : [];
105
+ if (!sugg.some(isCompleteSavedCarouselSuggestion)) return false;
106
+ }
107
+ return true;
108
+ };
109
+
110
+ export const checkDisableAddCarouselButton = (carouselData, activeCarouselIndex) => {
111
+ const idx = parseInt(activeCarouselIndex, 10);
112
+ const activeCard = carouselData?.[idx];
113
+ return !isCarouselCardValid(activeCard, idx);
114
+ };
115
+
116
+ /** Carousel: global variables across the whole carousel (all cards, title+body) */
117
+ export const getNextCarouselVarToken = (carouselData) => {
118
+ const nums = [];
119
+ (carouselData || []).forEach((card = {}) => {
120
+ const titleMatches = (card.title || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
121
+ const descMatches = (card.description || '').match(RCS_NUMERIC_VAR_TOKEN_REGEX) || [];
122
+ [...titleMatches, ...descMatches].forEach((tok) => {
123
+ const num = parseInt((tok.match(/\d+/) || [])[0], 10);
124
+ if (!Number.isNaN(num)) nums.push(num);
125
+ });
126
+ });
127
+ const existing = new Set(nums);
128
+ let nextNumber = 1;
129
+ while (existing.has(nextNumber)) nextNumber++;
130
+ if (nextNumber > 19) return '';
131
+ return `{{${nextNumber}}}`;
132
+ };
133
+
134
+ /** Carousel: per-card character counts (same limits as rich card) */
135
+ export const getCarouselTitleCharacterCount = (carouselData, cardIndex) => {
136
+ const titleText = carouselData?.[cardIndex]?.title || '';
137
+ return titleText ? titleText.length : 0;
138
+ };
139
+
140
+ export const getCarouselDescriptionCharacterCount = (carouselData, cardIndex) => {
141
+ const descriptionText = carouselData?.[cardIndex]?.description || '';
142
+ return descriptionText ? descriptionText.length : 0;
143
+ };
144
+
145
+ /** Preview (TestAndPreviewSlidebox / listing preview): resolve each card's title/desc through `resolveTemplateWithMap`. */
146
+ export const buildCarouselCardsForPreview = (cards = [], { isFullMode, rcsVarRegex, resolveTemplateWithMap }) => {
147
+ // Track cumulative variable slot index across all cards so resolveTemplateWithMap
148
+ // uses the correct globalSlot → slotKey for each token (e.g. card 1's {{3}} needs
149
+ // slotOffset=2 so slotKey becomes "3", not "1").
150
+ let slotOffset = 0;
151
+ return (cards || []).map((card = {}) => {
152
+ const rawTitle = card.title || '';
153
+ const rawDesc = card.description || '';
154
+ const titleVarCount = (rawTitle.match(rcsVarRegex) || []).length;
155
+ const descVarCount = (rawDesc.match(rcsVarRegex) || []).length;
156
+ const resolvedTitle = !isFullMode
157
+ ? resolveTemplateWithMap(rawTitle, slotOffset)
158
+ : rawTitle;
159
+ const resolvedDesc = !isFullMode
160
+ ? resolveTemplateWithMap(rawDesc, slotOffset + titleVarCount)
161
+ : rawDesc;
162
+ if (!isFullMode) {
163
+ slotOffset += titleVarCount + descVarCount;
164
+ }
165
+ const videoThumb = card.thumbnailSrc || card.videoAsset?.videoThumbnail || '';
166
+ const videoSrc = card.videoAsset?.videoSrc || '';
167
+ return {
168
+ mediaType: (card.mediaType || '').toLowerCase(),
169
+ imageSrc: card.imageSrc || '',
170
+ videoSrc,
171
+ videoPreviewImg: videoThumb,
172
+ title: resolvedTitle,
173
+ bodyText: resolvedDesc,
174
+ suggestions: card.suggestions || [],
175
+ };
176
+ });
177
+ };
178
+
179
+ /** Payload (create/edit RCS template): build `cardContent` for the carousel case. */
180
+ export const buildCarouselCardContentForPayload = (carouselData, {
181
+ isSlotMappingMode,
182
+ cardVarMapped,
183
+ selectedCarouselHeight,
184
+ rcsVarRegex,
185
+ }) => (carouselData || []).map((card = {}) => {
186
+ const cardMediaType = card.mediaType;
187
+ const isCardVideo = cardMediaType === RCS_MEDIA_TYPES.VIDEO;
188
+ const isCardImage = cardMediaType === RCS_MEDIA_TYPES.IMAGE;
189
+ const mediaUrl = isCardVideo
190
+ ? (card.videoAsset?.videoSrc || '')
191
+ : (card.imageSrc || '');
192
+ const thumbnailUrl = isCardVideo
193
+ ? (card.thumbnailSrc || card.videoAsset?.videoThumbnail || '')
194
+ : '';
195
+ const cardVarTokens = [
196
+ ...((card.title || '').match(rcsVarRegex) ?? []),
197
+ ...((card.description || '').match(rcsVarRegex) ?? []),
198
+ ];
199
+ const cardVarMappedForCard = {};
200
+ if (isSlotMappingMode && cardVarTokens.length > 0) {
201
+ cardVarTokens.forEach((token) => {
202
+ const varName = getVarNameFromToken(token);
203
+ if (!varName) return;
204
+ cardVarMappedForCard[varName] = sanitizeCardVarMappedValue(cardVarMapped?.[varName] ?? '');
205
+ });
206
+ }
207
+ return {
208
+ title: card.title || '',
209
+ description: card.description || '',
210
+ mediaType: cardMediaType,
211
+ ...((isCardImage || isCardVideo) && {
212
+ media: {
213
+ mediaUrl,
214
+ thumbnailUrl,
215
+ height: selectedCarouselHeight || MEDIUM,
216
+ ...(isCardVideo && card.videoAsset?.videoName && { videoName: card.videoAsset.videoName }),
217
+ },
218
+ }),
219
+ ...(Array.isArray(card.suggestions) && card.suggestions.length > 0 && {
220
+ suggestions: card.suggestions,
221
+ }),
222
+ ...(Object.keys(cardVarMappedForCard).length > 0 && { cardVarMapped: cardVarMappedForCard }),
223
+ };
224
+ });
@@ -0,0 +1,317 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CapCard from '@capillarytech/cap-ui-library/CapCard';
4
+ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
5
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
6
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
7
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
8
+ import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
9
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
10
+ import CapRadioGroup from '@capillarytech/cap-ui-library/CapRadioGroup';
11
+ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
12
+ import CapInput from '@capillarytech/cap-ui-library/CapInput';
13
+ import CapError from '@capillarytech/cap-ui-library/CapError';
14
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
15
+ import TagList from '../../TagList';
16
+ import globalMessages from '../../Cap/messages';
17
+ import messages from '../messages';
18
+ import CarouselCardMedia from './CarouselCardMedia';
19
+ import CarouselCardButtons from './CarouselCardButtons';
20
+ import CarouselCharacterCount from './CarouselCharacterCount';
21
+
22
+ const { TextArea } = CapInput;
23
+
24
+ /** One carousel card's editor: media, title, description, buttons — used as a tab pane in the carousel editor. */
25
+ const CarouselCard = ({
26
+ cardIndex,
27
+ card,
28
+ carouselLength,
29
+ isEditFlow,
30
+ isFullMode,
31
+ mediaRadioOptions,
32
+ cardError,
33
+ location,
34
+ tagsForPicker,
35
+ injectedTags,
36
+ selectedOfferDetails,
37
+ formatMessage,
38
+ titleMaxLength,
39
+ descriptionMaxLength,
40
+ titleCharCount,
41
+ descriptionCharCount,
42
+ dimKey,
43
+ rcsData,
44
+ uploadRcsImage,
45
+ uploadRcsVideo,
46
+ hostName,
47
+ onDelete,
48
+ onMediaTypeChange,
49
+ onTitleChange,
50
+ onDescriptionChange,
51
+ onTitleAddVar,
52
+ onDescriptionAddVar,
53
+ onTagSelect,
54
+ onContextChange,
55
+ renderEditMessage,
56
+ onFieldChange,
57
+ onSuggestionsChange,
58
+ }) => (
59
+ <CapCard
60
+ title={`Card ${cardIndex + 1}`}
61
+ extra={
62
+ !isEditFlow &&
63
+ (carouselLength === 1 ? (
64
+ <CapTooltip title={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}>
65
+ <span className="button-disabled-tooltip-wrapper rcs-carousel-delete-tooltip-wrap">
66
+ <CapButton
67
+ className="rcs-carousel-card-delete"
68
+ type="flat"
69
+ onClick={onDelete}
70
+ disabled
71
+ aria-label={formatMessage(messages.rcsCarouselMinCardDeleteTooltip)}
72
+ >
73
+ <CapIcon type="delete" />
74
+ </CapButton>
75
+ </span>
76
+ </CapTooltip>
77
+ ) : (
78
+ <CapButton
79
+ className="rcs-carousel-card-delete"
80
+ type="flat"
81
+ onClick={onDelete}
82
+ aria-label={formatMessage(globalMessages.delete)}
83
+ >
84
+ <CapIcon type="delete" />
85
+ </CapButton>
86
+ ))
87
+ }
88
+ className="rcs-carousel-card"
89
+ >
90
+ {/* Media selection should be at top of card */}
91
+ {isEditFlow ? (
92
+ <div className="rcs-creative-name-readonly">
93
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
94
+ <CapHeading type="h5" className="rcs-creative-name-value">
95
+ {mediaRadioOptions.find((option) => option.value === card.mediaType)?.label || '-'}
96
+ </CapHeading>
97
+ </div>
98
+ ) : (
99
+ <CapRow className="rcs-carousel-media-selection">
100
+ <CapColumn className="rcs-carousel-media-selection-heading">
101
+ <CapHeading type="h4">{formatMessage(messages.mediaTypeLabel)}</CapHeading>
102
+ </CapColumn>
103
+ <CapColumn>
104
+ <CapRadioGroup
105
+ id={`rcs-carousel-media-radio-${cardIndex}`}
106
+ options={mediaRadioOptions}
107
+ value={card.mediaType}
108
+ onChange={({ target: { value } }) => onMediaTypeChange(value)}
109
+ disabled={!isFullMode}
110
+ className="rcs-radio"
111
+ />
112
+ </CapColumn>
113
+ </CapRow>
114
+ )}
115
+
116
+ <CapRow className={`rcs-carousel-media-upload ${isEditFlow ? 'rcs-carousel-media-upload--readonly-above' : ''}`}>
117
+ <CarouselCardMedia
118
+ cardIndex={cardIndex}
119
+ card={card}
120
+ dimKey={dimKey}
121
+ isFullMode={isFullMode}
122
+ isEditFlow={isEditFlow}
123
+ rcsData={rcsData}
124
+ uploadRcsImage={uploadRcsImage}
125
+ uploadRcsVideo={uploadRcsVideo}
126
+ formatMessage={formatMessage}
127
+ onFieldChange={onFieldChange}
128
+ />
129
+ </CapRow>
130
+
131
+ {/* Title after media */}
132
+ <CapRow className="rcs-carousel-card-row">
133
+ <CapHeader
134
+ className="rcs-template-title-label"
135
+ title={<CapHeading type="h4">Card title</CapHeading>}
136
+ suffix={
137
+ (isEditFlow || !isFullMode) ? (
138
+ <TagList
139
+ label={formatMessage(globalMessages.addLabels)}
140
+ onTagSelect={onTagSelect}
141
+ location={location}
142
+ tags={tagsForPicker}
143
+ onContextChange={onContextChange}
144
+ injectedTags={injectedTags || {}}
145
+ selectedOfferDetails={selectedOfferDetails}
146
+ />
147
+ ) : (
148
+ <CapButton
149
+ data-testid={`rcs-carousel-title-add-var-${cardIndex}`}
150
+ type="flat"
151
+ isAddBtn
152
+ onClick={onTitleAddVar}
153
+ disabled={!!cardError?.title}
154
+ >
155
+ {formatMessage(messages.addVar)}
156
+ </CapButton>
157
+ )
158
+ }
159
+ />
160
+ <CapRow className="rcs_text_area_wrapper">
161
+ {(isEditFlow || !isFullMode) ? (
162
+ renderEditMessage(card.title || '')
163
+ ) : (
164
+ <CapInput
165
+ value={card.title || ''}
166
+ placeholder={formatMessage(messages.templateTitlePlaceholder)}
167
+ onChange={onTitleChange}
168
+ disabled={isEditFlow || !isFullMode}
169
+ errorMessage={cardError?.title}
170
+ />
171
+ )}
172
+ </CapRow>
173
+ </CapRow>
174
+ {!isEditFlow && (
175
+ <CapRow className="rcs-carousel-character-count-row">
176
+ <CarouselCharacterCount
177
+ formatMessage={formatMessage}
178
+ currentLength={titleCharCount}
179
+ maxLength={titleMaxLength}
180
+ />
181
+ </CapRow>
182
+ )}
183
+
184
+ {/* Description after title */}
185
+ <CapRow className="rcs-carousel-card-row">
186
+ <CapHeader
187
+ title={<CapHeading type="h4">Card body text</CapHeading>}
188
+ suffix={
189
+ (isEditFlow || !isFullMode) ? (
190
+ <TagList
191
+ label={formatMessage(globalMessages.addLabels)}
192
+ onTagSelect={onTagSelect}
193
+ location={location}
194
+ tags={tagsForPicker}
195
+ onContextChange={onContextChange}
196
+ injectedTags={injectedTags || {}}
197
+ selectedOfferDetails={selectedOfferDetails}
198
+ />
199
+ ) : (
200
+ <CapButton
201
+ data-testid={`rcs-carousel-desc-add-var-${cardIndex}`}
202
+ type="flat"
203
+ isAddBtn
204
+ onClick={onDescriptionAddVar}
205
+ disabled={!!cardError?.description}
206
+ >
207
+ {formatMessage(messages.addVar)}
208
+ </CapButton>
209
+ )
210
+ }
211
+ />
212
+ <CapRow className="rcs_text_area_wrapper">
213
+ {(isEditFlow || !isFullMode) ? (
214
+ renderEditMessage(card.description || '')
215
+ ) : (
216
+ <TextArea
217
+ autosize={{ minRows: 3, maxRows: 5 }}
218
+ value={card.description || ''}
219
+ placeholder={formatMessage(messages.templateDescPlaceholder)}
220
+ onChange={onDescriptionChange}
221
+ disabled={isEditFlow || !isFullMode}
222
+ errorMessage={
223
+ cardError?.description && (
224
+ <CapError className="rcs-template-message-error">
225
+ {cardError.description}
226
+ </CapError>
227
+ )
228
+ }
229
+ />
230
+ )}
231
+ </CapRow>
232
+ </CapRow>
233
+ {!isEditFlow && (
234
+ <CapRow className="rcs-carousel-character-count-row">
235
+ <CarouselCharacterCount
236
+ formatMessage={formatMessage}
237
+ currentLength={descriptionCharCount}
238
+ maxLength={descriptionMaxLength}
239
+ />
240
+ </CapRow>
241
+ )}
242
+
243
+ <CapDivider className="rcs-carousel-card-divider" />
244
+ <CarouselCardButtons
245
+ cardIndex={cardIndex}
246
+ suggestions={card.suggestions || []}
247
+ isEditFlow={isEditFlow}
248
+ isFullMode={isFullMode}
249
+ hostName={hostName}
250
+ formatMessage={formatMessage}
251
+ onSuggestionsChange={onSuggestionsChange}
252
+ location={location}
253
+ tags={tagsForPicker}
254
+ injectedTags={injectedTags}
255
+ selectedOfferDetails={selectedOfferDetails}
256
+ onContextChange={onContextChange}
257
+ />
258
+ </CapCard>
259
+ );
260
+
261
+ CarouselCard.propTypes = {
262
+ cardIndex: PropTypes.number.isRequired,
263
+ card: PropTypes.object,
264
+ carouselLength: PropTypes.number,
265
+ isEditFlow: PropTypes.bool,
266
+ isFullMode: PropTypes.bool,
267
+ mediaRadioOptions: PropTypes.array,
268
+ cardError: PropTypes.object,
269
+ location: PropTypes.object,
270
+ tagsForPicker: PropTypes.array,
271
+ injectedTags: PropTypes.object,
272
+ selectedOfferDetails: PropTypes.array,
273
+ formatMessage: PropTypes.func.isRequired,
274
+ titleMaxLength: PropTypes.number,
275
+ descriptionMaxLength: PropTypes.number,
276
+ titleCharCount: PropTypes.number,
277
+ descriptionCharCount: PropTypes.number,
278
+ dimKey: PropTypes.string,
279
+ rcsData: PropTypes.object,
280
+ uploadRcsImage: PropTypes.func.isRequired,
281
+ uploadRcsVideo: PropTypes.func.isRequired,
282
+ hostName: PropTypes.string,
283
+ onDelete: PropTypes.func.isRequired,
284
+ onMediaTypeChange: PropTypes.func.isRequired,
285
+ onTitleChange: PropTypes.func.isRequired,
286
+ onDescriptionChange: PropTypes.func.isRequired,
287
+ onTitleAddVar: PropTypes.func.isRequired,
288
+ onDescriptionAddVar: PropTypes.func.isRequired,
289
+ onTagSelect: PropTypes.func.isRequired,
290
+ onContextChange: PropTypes.func,
291
+ renderEditMessage: PropTypes.func.isRequired,
292
+ onFieldChange: PropTypes.func.isRequired,
293
+ onSuggestionsChange: PropTypes.func.isRequired,
294
+ };
295
+
296
+ CarouselCard.defaultProps = {
297
+ card: {},
298
+ carouselLength: 1,
299
+ isEditFlow: false,
300
+ isFullMode: false,
301
+ mediaRadioOptions: [],
302
+ cardError: {},
303
+ location: {},
304
+ tagsForPicker: [],
305
+ injectedTags: {},
306
+ selectedOfferDetails: [],
307
+ titleMaxLength: 0,
308
+ descriptionMaxLength: 0,
309
+ titleCharCount: 0,
310
+ descriptionCharCount: 0,
311
+ dimKey: '',
312
+ rcsData: {},
313
+ hostName: '',
314
+ onContextChange: () => {},
315
+ };
316
+
317
+ export default React.memo(CarouselCard);