@capillarytech/creatives-library 9.0.29 → 9.0.30

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 (147) 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 +261 -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 +322 -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/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -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);
@@ -0,0 +1,113 @@
1
+ import React from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import cloneDeep from 'lodash/cloneDeep';
4
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
5
+ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
6
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
7
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
8
+ import CapActionButton from '../../../v2Components/CapActionButton';
9
+ import { MAX_BUTTONS, RCS_BUTTON_TYPES } from '../constants';
10
+ import messages from '../messages';
11
+
12
+ /** Reuse rich-card buttons UI per carousel card. */
13
+ const CarouselCardButtons = ({
14
+ cardIndex,
15
+ suggestions,
16
+ isEditFlow,
17
+ isFullMode,
18
+ hostName,
19
+ formatMessage,
20
+ onSuggestionsChange,
21
+ location,
22
+ tags,
23
+ injectedTags,
24
+ selectedOfferDetails,
25
+ onContextChange,
26
+ }) => {
27
+ const suggestionsForCard = suggestions || [];
28
+ return (
29
+ <>
30
+ <CapHeader
31
+ className="rcs-button-cta"
32
+ title={(
33
+ <CapRow type="flex">
34
+ <CapHeading type="h4">
35
+ {formatMessage(messages.btnLabel)}
36
+ </CapHeading>
37
+ </CapRow>
38
+ )}
39
+ description={!isEditFlow && (
40
+ <CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>
41
+ )}
42
+ />
43
+ <CapActionButton
44
+ buttonType={RCS_BUTTON_TYPES.NONE}
45
+ updateButtonChange={(data, btnIndex) => {
46
+ // Match existing behavior: allow CapActionButton to manage save gating.
47
+ const updated = cloneDeep(suggestionsForCard);
48
+ if (btnIndex === MAX_BUTTONS) {
49
+ onSuggestionsChange(data);
50
+ return;
51
+ }
52
+ updated[btnIndex] = data;
53
+ onSuggestionsChange(updated);
54
+ }}
55
+ deleteButtonHandler={(btnIndex) => {
56
+ if (cardIndex === 0 && btnIndex === 0) {
57
+ return;
58
+ }
59
+ const savedCount = (suggestionsForCard || []).filter((suggestion) => suggestion && suggestion.isSaved).length;
60
+ const target = (suggestionsForCard || []).find((suggestion) => suggestion && suggestion.index === btnIndex);
61
+ if (cardIndex === 0 && target?.isSaved && savedCount <= 1) {
62
+ return;
63
+ }
64
+ const updated = cloneDeep(suggestionsForCard)
65
+ .filter((i) => i.index !== btnIndex)
66
+ .map((i, idx) => ({ ...i, index: idx }));
67
+ onSuggestionsChange(updated);
68
+ }}
69
+ suggestions={suggestionsForCard}
70
+ isEditFlow={isEditFlow}
71
+ isFullMode={isFullMode}
72
+ maxButtons={MAX_BUTTONS}
73
+ host={hostName}
74
+ minSavedSuggestions={cardIndex === 0 ? 1 : 0}
75
+ hideDeleteSuggestionIndexes={cardIndex === 0 ? [0] : []}
76
+ location={location}
77
+ tags={tags}
78
+ injectedTags={injectedTags}
79
+ selectedOfferDetails={selectedOfferDetails}
80
+ onContextChange={onContextChange}
81
+ />
82
+ </>
83
+ );
84
+ };
85
+
86
+ CarouselCardButtons.propTypes = {
87
+ cardIndex: PropTypes.number.isRequired,
88
+ suggestions: PropTypes.array,
89
+ isEditFlow: PropTypes.bool,
90
+ isFullMode: PropTypes.bool,
91
+ hostName: PropTypes.string,
92
+ formatMessage: PropTypes.func.isRequired,
93
+ onSuggestionsChange: PropTypes.func.isRequired,
94
+ location: PropTypes.object,
95
+ tags: PropTypes.array,
96
+ injectedTags: PropTypes.object,
97
+ selectedOfferDetails: PropTypes.array,
98
+ onContextChange: PropTypes.func,
99
+ };
100
+
101
+ CarouselCardButtons.defaultProps = {
102
+ suggestions: [],
103
+ isEditFlow: false,
104
+ isFullMode: false,
105
+ hostName: '',
106
+ location: {},
107
+ tags: [],
108
+ injectedTags: {},
109
+ selectedOfferDetails: [],
110
+ onContextChange: () => {},
111
+ };
112
+
113
+ export default React.memo(CarouselCardButtons);