@capillarytech/creatives-library 8.0.128 → 8.0.129

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 (68) hide show
  1. package/containers/App/constants.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +1 -1
  4. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +8 -3
  5. package/tests/integration/TemplateCreation/api-response.js +5 -0
  6. package/tests/integration/TemplateCreation/msw-handler.js +42 -63
  7. package/utils/common.js +7 -0
  8. package/utils/commonUtils.js +2 -6
  9. package/utils/createPayload.js +405 -0
  10. package/utils/tests/createPayload.test.js +785 -0
  11. package/v2Components/CapImageUpload/index.js +59 -46
  12. package/v2Components/CapInAppCTA/index.js +1 -0
  13. package/v2Components/CapMpushCTA/constants.js +25 -0
  14. package/v2Components/CapMpushCTA/index.js +402 -0
  15. package/v2Components/CapMpushCTA/index.scss +95 -0
  16. package/v2Components/CapMpushCTA/messages.js +101 -0
  17. package/v2Components/CapTagList/index.js +177 -120
  18. package/v2Components/CapVideoUpload/constants.js +3 -0
  19. package/v2Components/CapVideoUpload/index.js +167 -110
  20. package/v2Components/CapVideoUpload/messages.js +16 -0
  21. package/v2Components/Carousel/index.js +15 -13
  22. package/v2Components/ErrorInfoNote/style.scss +1 -0
  23. package/v2Components/MobilePushPreviewV2/index.js +37 -5
  24. package/v2Components/TemplatePreview/_templatePreview.scss +114 -72
  25. package/v2Components/TemplatePreview/assets/images/Android _ With date and time.svg +29 -0
  26. package/v2Components/TemplatePreview/assets/images/android.svg +9 -0
  27. package/v2Components/TemplatePreview/assets/images/iOS _ With date and time.svg +26 -0
  28. package/v2Components/TemplatePreview/assets/images/ios.svg +9 -0
  29. package/v2Components/TemplatePreview/index.js +178 -50
  30. package/v2Components/TemplatePreview/messages.js +4 -0
  31. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -62
  32. package/v2Containers/CreativesContainer/index.js +191 -136
  33. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +0 -22
  34. package/v2Containers/InApp/constants.js +1 -0
  35. package/v2Containers/InApp/index.js +13 -13
  36. package/v2Containers/MobilePush/Create/index.js +1 -0
  37. package/v2Containers/MobilePush/commonMethods.js +7 -14
  38. package/v2Containers/MobilePushNew/actions.js +116 -0
  39. package/v2Containers/MobilePushNew/components/CtaButtons.js +181 -0
  40. package/v2Containers/MobilePushNew/components/MediaUploaders.js +834 -0
  41. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +345 -0
  42. package/v2Containers/MobilePushNew/components/index.js +5 -0
  43. package/v2Containers/MobilePushNew/components/tests/CtaButtons.test.js +798 -0
  44. package/v2Containers/MobilePushNew/components/tests/MediaUploaders.test.js +2114 -0
  45. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +343 -0
  46. package/v2Containers/MobilePushNew/constants.js +115 -0
  47. package/v2Containers/MobilePushNew/hooks/tests/usePlatformSync.test.js +1299 -0
  48. package/v2Containers/MobilePushNew/hooks/tests/useUpload.test.js +1223 -0
  49. package/v2Containers/MobilePushNew/hooks/usePlatformSync.js +246 -0
  50. package/v2Containers/MobilePushNew/hooks/useUpload.js +726 -0
  51. package/v2Containers/MobilePushNew/index.js +3412 -0
  52. package/v2Containers/MobilePushNew/index.scss +308 -0
  53. package/v2Containers/MobilePushNew/messages.js +242 -0
  54. package/v2Containers/MobilePushNew/reducer.js +160 -0
  55. package/v2Containers/MobilePushNew/sagas.js +198 -0
  56. package/v2Containers/MobilePushNew/selectors.js +55 -0
  57. package/v2Containers/MobilePushNew/tests/reducer.test.js +741 -0
  58. package/v2Containers/MobilePushNew/tests/sagas.test.js +863 -0
  59. package/v2Containers/MobilePushNew/tests/selectors.test.js +425 -0
  60. package/v2Containers/MobilePushNew/tests/utils.test.js +322 -0
  61. package/v2Containers/MobilePushNew/utils.js +33 -0
  62. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +5 -5
  63. package/v2Containers/TagList/index.js +56 -10
  64. package/v2Containers/Templates/_templates.scss +101 -1
  65. package/v2Containers/Templates/index.js +147 -35
  66. package/v2Containers/Templates/messages.js +8 -0
  67. package/v2Containers/Templates/sagas.js +2 -0
  68. package/v2Containers/Whatsapp/constants.js +1 -0
@@ -0,0 +1,834 @@
1
+ import React, {
2
+ useCallback,
3
+ useState,
4
+ useEffect,
5
+ useRef,
6
+ } from "react";
7
+ import PropTypes from "prop-types";
8
+ import { FormattedMessage } from "react-intl";
9
+ import CapButton from "@capillarytech/cap-ui-library/CapButton";
10
+ import CapCard from "@capillarytech/cap-ui-library/CapCard";
11
+ import CapColumn from "@capillarytech/cap-ui-library/CapColumn";
12
+ import CapDivider from "@capillarytech/cap-ui-library/CapDivider";
13
+ import CapHeading from "@capillarytech/cap-ui-library/CapHeading";
14
+ import CapIcon from "@capillarytech/cap-ui-library/CapIcon";
15
+ import CapRadioGroup from "@capillarytech/cap-ui-library/CapRadioGroup";
16
+ import CapRow from "@capillarytech/cap-ui-library/CapRow";
17
+ import CapTab from "@capillarytech/cap-ui-library/CapTab";
18
+ import CapCheckbox from "@capillarytech/cap-ui-library/CapCheckbox";
19
+ import CapLabel from "@capillarytech/cap-ui-library/CapLabel";
20
+ import CapSelect from "@capillarytech/cap-ui-library/CapSelect";
21
+ import CapInput from "@capillarytech/cap-ui-library/CapInput";
22
+ import CapError from "@capillarytech/cap-ui-library/CapError";
23
+ import CapImageUpload from "../../../v2Components/CapImageUpload";
24
+ import CapVideoUpload from "../../../v2Components/CapVideoUpload";
25
+ import {
26
+ ALLOWED_IMAGE_EXTENSIONS_REGEX,
27
+ MPUSH_IMG_SIZE,
28
+ MPUSH_IMG_MAX_WIDTH,
29
+ MPUSH_IMG_MAX_HEIGHT,
30
+ ALLOWED_EXTENSIONS_VIDEO_REGEX,
31
+ MPUSH_VIDEO_SIZE,
32
+ ALLOWED_GIF_REGEX,
33
+ MPUSH_GIF_SIZE,
34
+ MOBILE_PUSH_CHANNEL,
35
+ ANDROID,
36
+ GIF,
37
+ IMAGE,
38
+ VIDEO,
39
+ LINK_TYPE_OPTIONS,
40
+ DEEP_LINK,
41
+ EXTERNAL_LINK,
42
+ } from "../constants";
43
+ import messages from "../messages";
44
+ import { validateExternalLink, validateDeepLink } from "../utils";
45
+
46
+ // Initial carousel data structure
47
+ const CAROUSEL_INITIAL_DATA = {
48
+ mediaType: 'image',
49
+ imageUrl: '',
50
+ videoSrc: '',
51
+ buttons: [{
52
+ actionOnClick: false,
53
+ linkType: DEEP_LINK,
54
+ deepLinkValue: '',
55
+ deepLinkKeys: [],
56
+ externalLinkValue: '',
57
+ }],
58
+ };
59
+
60
+ // Maximum number of carousel cards allowed
61
+ const MAX_CAROUSEL_ALLOWED = 10;
62
+
63
+ const MediaUploaders = ({
64
+ mediaType,
65
+ activeTab,
66
+ imageSrc,
67
+ uploadMpushAsset,
68
+ isFullMode,
69
+ setUpdateMpushImageSrc,
70
+ updateOnMpushImageReUpload,
71
+ imageData,
72
+ videoAssetList,
73
+ gifAssetList,
74
+ setUpdateMpushVideoSrc,
75
+ videoDataForVideo,
76
+ videoDataForGif,
77
+ videoSrc, // Add videoSrc prop for content state fallback
78
+ formatMessage,
79
+ linkProps,
80
+ clearImageDataByMediaType,
81
+ carouselData = [], // Carousel data from content state
82
+ onCarouselDataChange, // Callback to update carousel data in parent
83
+ mobilePushActions, // Mobile push actions for clearing assets
84
+ carouselActiveTabIndex,
85
+ setCarouselActiveTabIndex,
86
+ carouselLinkErrors = {}, // Carousel link errors from parent
87
+ updateCarouselLinkError, // Function to update carousel link errors in parent
88
+ }) => {
89
+ // Carousel state management - separate for Android and iOS
90
+ const [carouselMediaType, setCarouselMediaType] = useState('image');
91
+
92
+ // Track re-upload mode to prevent automatic restoration during re-upload
93
+ const [isReUploading, setIsReUploading] = useState(false);
94
+
95
+ // Track previous media type to handle transitions
96
+ const previousMediaTypeRef = useRef(mediaType);
97
+
98
+ // Extract link props
99
+ const { deepLink } = linkProps || {};
100
+
101
+ // Get current carousel data based on active tab
102
+ const getCurrentCarouselData = useCallback(() => carouselData || [], [carouselData]);
103
+
104
+ // Set current carousel data based on active tab
105
+ const setCurrentCarouselData = useCallback((newData) => {
106
+ if (onCarouselDataChange) {
107
+ onCarouselDataChange(activeTab, newData);
108
+ }
109
+ }, [activeTab, onCarouselDataChange]);
110
+
111
+ // Clear previous media data when switching media types
112
+ useEffect(() => {
113
+ // Clear carousel data when switching to non-carousel media types
114
+ if (mediaType !== 'CAROUSEL') {
115
+ setCurrentCarouselData([]);
116
+ setCarouselActiveTabIndex(0);
117
+ }
118
+
119
+ // Clear image data when switching away from IMAGE media type
120
+ if (mediaType !== 'IMAGE') {
121
+ clearImageDataByMediaType('IMAGE');
122
+ }
123
+
124
+ // Clear image data when switching FROM CAROUSEL TO IMAGE
125
+ // This prevents carousel images from appearing in IMAGE media type
126
+ if (previousMediaTypeRef.current === 'CAROUSEL' && mediaType === 'IMAGE') {
127
+ clearImageDataByMediaType('IMAGE');
128
+ }
129
+
130
+ // Reset re-upload mode when switching media types
131
+ setIsReUploading(false);
132
+
133
+ // Update the previous media type ref
134
+ previousMediaTypeRef.current = mediaType;
135
+ }, [mediaType, clearImageDataByMediaType, setCurrentCarouselData]);
136
+
137
+ // Handle platform tab changes for carousel data
138
+ useEffect(() => {
139
+ // Reset active tab index when switching platforms
140
+ setCarouselActiveTabIndex(0);
141
+ }, [activeTab]);
142
+
143
+ // Initialize carousel data if empty
144
+ useEffect(() => {
145
+ if (mediaType === 'CAROUSEL' && (!carouselData || carouselData.length === 0)) {
146
+ setCurrentCarouselData([CAROUSEL_INITIAL_DATA]);
147
+ }
148
+ }, [mediaType, carouselData, activeTab, setCurrentCarouselData]);
149
+
150
+ // Listen for upload success and update carousel data
151
+ useEffect(() => {
152
+ // Skip automatic updates during re-upload mode
153
+ if (isReUploading) {
154
+ return;
155
+ }
156
+
157
+ if (mediaType === 'CAROUSEL' && imageData) {
158
+ const platformIndex = activeTab === ANDROID ? 0 : 1;
159
+ const uploadedAssetData = imageData[`uploadedAssetData${platformIndex}`];
160
+
161
+ // Only update if there's new upload data and it's different from current carousel data
162
+ if (uploadedAssetData && uploadedAssetData.metaInfo && uploadedAssetData.metaInfo.secure_file_path) {
163
+ const currentData = getCurrentCarouselData();
164
+ if (currentData.length > carouselActiveTabIndex) {
165
+ const currentCard = currentData[carouselActiveTabIndex];
166
+ const newImageUrl = uploadedAssetData.metaInfo.secure_file_path;
167
+
168
+ // Only update if the image URL is different from the current one and not empty
169
+ if (currentCard.imageUrl !== newImageUrl && newImageUrl !== '') {
170
+ const updatedData = [...currentData];
171
+ updatedData[carouselActiveTabIndex] = {
172
+ ...updatedData[carouselActiveTabIndex],
173
+ imageUrl: newImageUrl,
174
+ };
175
+ setCurrentCarouselData(updatedData);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }, [imageData, mediaType, activeTab, carouselActiveTabIndex, getCurrentCarouselData, setCurrentCarouselData, isReUploading]);
181
+
182
+ const renderImageComponent = useCallback(() => {
183
+ let imageSource = '';
184
+
185
+ // Use platform-specific image source
186
+ const platformKey = activeTab === ANDROID ? "androidImageSrc" : "iosImageSrc";
187
+ imageSource = imageSrc[platformKey] || '';
188
+
189
+ return (
190
+ <CapImageUpload
191
+ style={{ paddingTop: "20px" }}
192
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
193
+ imgSize={MPUSH_IMG_SIZE}
194
+ imgWidth={MPUSH_IMG_MAX_WIDTH}
195
+ imgHeight={MPUSH_IMG_MAX_HEIGHT}
196
+ uploadAsset={uploadMpushAsset}
197
+ isFullMode={isFullMode}
198
+ imageSrc={imageSource}
199
+ updateImageSrc={(src) => setUpdateMpushImageSrc(src, activeTab === ANDROID ? 0 : 1, IMAGE)}
200
+ updateOnReUpload={updateOnMpushImageReUpload}
201
+ index={activeTab === ANDROID ? 0 : 1}
202
+ className="cap-custom-image-upload"
203
+ key="mpush-uploaded-image"
204
+ imageData={imageData}
205
+ channel={MOBILE_PUSH_CHANNEL}
206
+ showReUploadButton
207
+ errorMessage={formatMessage(messages.imageErrorMessage)}
208
+ />
209
+ );
210
+ }, [
211
+ activeTab,
212
+ imageSrc,
213
+ uploadMpushAsset,
214
+ isFullMode,
215
+ setUpdateMpushImageSrc,
216
+ updateOnMpushImageReUpload,
217
+ imageData,
218
+ formatMessage,
219
+ ]);
220
+
221
+ const renderVideoComponent = useCallback(() => {
222
+ // Apply same platform isolation logic as images
223
+ let platformVideoData = {};
224
+ const currentIndex = activeTab === ANDROID ? 0 : 1;
225
+
226
+ // Use platform-specific video data
227
+ if (videoDataForVideo && videoDataForVideo[`uploadedAssetData${currentIndex}`] && Object.keys(videoDataForVideo[`uploadedAssetData${currentIndex}`]).length > 0) {
228
+ // ONLY include the current platform's uploadedAssetData to prevent cross-platform bleeding
229
+ platformVideoData = {
230
+ [`uploadedAssetData${currentIndex}`]: videoDataForVideo[`uploadedAssetData${currentIndex}`],
231
+ };
232
+ } else {
233
+ // No platform-specific video data in Redux - check if video exists in content state (like images do)
234
+ // This handles initialization from edit data where video is in content but not yet in Redux
235
+ platformVideoData = {};
236
+ }
237
+
238
+ // Add videoSrc content state as additional source for the component
239
+ let videoSource = '';
240
+ videoSource = activeTab === ANDROID ? videoSrc?.androidVideoSrc || '' : videoSrc?.iosVideoSrc || '';
241
+
242
+ return (
243
+ <CapVideoUpload
244
+ key="mpush-video-upload"
245
+ index={currentIndex}
246
+ allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
247
+ videoSize={MPUSH_VIDEO_SIZE}
248
+ isFullMode={isFullMode}
249
+ uploadAsset={uploadMpushAsset}
250
+ uploadedAssetList={videoAssetList}
251
+ onVideoUploadUpdateAssestList={setUpdateMpushVideoSrc}
252
+ videoData={platformVideoData}
253
+ videoSrc={videoSource} // Pass video content state as additional source
254
+ className="cap-custom-video-upload"
255
+ formClassName="mpush-video-upload"
256
+ channel={MOBILE_PUSH_CHANNEL}
257
+ errorMessage={formatMessage(messages.videoErrorMessage)}
258
+ showVideoNameAndDuration={false}
259
+ showReUploadButton
260
+ mediaType="video"
261
+ />
262
+ );
263
+ }, [
264
+ activeTab,
265
+ isFullMode,
266
+ uploadMpushAsset,
267
+ videoAssetList,
268
+ setUpdateMpushVideoSrc,
269
+ videoDataForVideo,
270
+ videoSrc,
271
+ formatMessage,
272
+ ]);
273
+
274
+ const renderGifComponent = useCallback(() => {
275
+ // Apply same platform isolation logic as images
276
+ let platformGifData = {};
277
+ const currentIndex = activeTab === ANDROID ? 0 : 1;
278
+
279
+ // Use platform-specific GIF data
280
+ if (videoDataForGif && videoDataForGif[`uploadedAssetData${currentIndex}`] && Object.keys(videoDataForGif[`uploadedAssetData${currentIndex}`]).length > 0) {
281
+ // ONLY include the current platform's uploadedAssetData to prevent cross-platform bleeding
282
+ platformGifData = {
283
+ [`uploadedAssetData${currentIndex}`]: videoDataForGif[`uploadedAssetData${currentIndex}`],
284
+ };
285
+ } else {
286
+ // No platform-specific GIF data in Redux - check if GIF exists in content state (like images do)
287
+ // This handles initialization from edit data where GIF is in content but not yet in Redux
288
+ platformGifData = {};
289
+ }
290
+ // Add videoSrc content state as additional source for the component
291
+ const gifSource = activeTab === ANDROID ? videoSrc?.androidVideoSrc || '' : videoSrc?.iosVideoSrc || '';
292
+
293
+ return (
294
+ <CapVideoUpload
295
+ key="mpush-gif-upload"
296
+ index={currentIndex}
297
+ allowedExtensionsRegex={ALLOWED_GIF_REGEX}
298
+ videoSize={MPUSH_GIF_SIZE}
299
+ isFullMode={isFullMode}
300
+ uploadAsset={uploadMpushAsset}
301
+ uploadedAssetList={gifAssetList}
302
+ onVideoUploadUpdateAssestList={setUpdateMpushVideoSrc}
303
+ videoData={platformGifData}
304
+ videoSrc={gifSource} // Pass GIF content state as additional source
305
+ className="cap-custom-gif-upload"
306
+ formClassName="mpush-gif-upload"
307
+ channel={MOBILE_PUSH_CHANNEL}
308
+ errorMessage={formatMessage(messages.gifErrorMessage)}
309
+ showVideoNameAndDuration={false}
310
+ showReUploadButton
311
+ mediaType={GIF.toLowerCase()}
312
+ />
313
+ );
314
+ }, [
315
+ activeTab,
316
+ uploadMpushAsset,
317
+ gifAssetList,
318
+ setUpdateMpushVideoSrc,
319
+ videoDataForGif,
320
+ videoSrc,
321
+ ]);
322
+
323
+ const renderCarouselComponent = useCallback(() => {
324
+ const handleCarouselMediaOptions = ({ target: { value = '' } = {} }) => {
325
+ setCarouselMediaType(value);
326
+ const currentData = getCurrentCarouselData();
327
+ setCurrentCarouselData(currentData.map((card) => ({
328
+ ...card,
329
+ mediaType: value,
330
+ imageUrl: value === 'image' ? card.imageUrl : '',
331
+ videoSrc: value === 'video' ? card.videoSrc : '',
332
+ buttons: card.buttons || [{
333
+ actionOnClick: false,
334
+ linkType: DEEP_LINK,
335
+ deepLinkValue: '',
336
+ externalLinkValue: '',
337
+ }],
338
+ })));
339
+ };
340
+
341
+ const onTabChange = (index) => {
342
+ setCarouselActiveTabIndex(index);
343
+ };
344
+
345
+ const addCarouselCard = () => {
346
+ const currentData = getCurrentCarouselData();
347
+ if (currentData.length < MAX_CAROUSEL_ALLOWED) {
348
+ setCurrentCarouselData([
349
+ ...currentData,
350
+ { ...CAROUSEL_INITIAL_DATA, mediaType: carouselMediaType },
351
+ ]);
352
+ }
353
+ };
354
+
355
+ const deleteCarouselCard = (index) => {
356
+ const currentData = getCurrentCarouselData();
357
+ if (currentData.length > 1) {
358
+ setCurrentCarouselData(currentData.filter((_, i) => i !== index));
359
+ if (carouselActiveTabIndex >= currentData.length - 1) {
360
+ setCarouselActiveTabIndex(Math.max(0, currentData.length - 2));
361
+ }
362
+ }
363
+ };
364
+
365
+ const updateCarouselCard = (index, fields) => {
366
+ const currentData = getCurrentCarouselData();
367
+ setCurrentCarouselData(currentData.map((card, i) => (
368
+ i === index ? { ...card, ...fields } : card
369
+ )));
370
+ // Don't clear assets for carousel cards since they store data in component state, not Redux
371
+ // Only clear assets for non-carousel media types
372
+ };
373
+
374
+ const handleCarouselImageChange = (image) => {
375
+ updateCarouselCard(parseInt(carouselActiveTabIndex, 10), { imageUrl: image });
376
+ };
377
+
378
+ // Carousel-specific re-upload handler that clears both Redux and component state
379
+ const handleCarouselImageReUpload = () => {
380
+ // Set re-upload mode to prevent automatic restoration
381
+ setIsReUploading(true);
382
+
383
+ // Clear Redux state (same as updateOnMpushImageReUpload)
384
+ updateOnMpushImageReUpload();
385
+
386
+ // Clear Redux uploadedAssetData to prevent automatic restoration
387
+ const platformIndex = activeTab === ANDROID ? 0 : 1;
388
+ mobilePushActions.clearAsset(platformIndex);
389
+
390
+ // Clear carousel component state for the current card
391
+ const currentData = getCurrentCarouselData();
392
+ if (currentData.length > carouselActiveTabIndex) {
393
+ const updatedData = [...currentData];
394
+ updatedData[carouselActiveTabIndex] = {
395
+ ...updatedData[carouselActiveTabIndex],
396
+ imageUrl: '',
397
+ };
398
+ setCurrentCarouselData(updatedData);
399
+ }
400
+
401
+ // Reset re-upload mode after a longer delay to allow for upload completion
402
+ setTimeout(() => {
403
+ setIsReUploading(false);
404
+ }, 3000); // Increased from 1000ms to 3000ms
405
+ };
406
+
407
+ // Action link handlers for carousel cards
408
+ const handleCarouselActionOnClickChange = (cardIndex, checked) => {
409
+ const currentData = getCurrentCarouselData();
410
+ const card = currentData[cardIndex];
411
+ const updatedButtons = [...card.buttons];
412
+ updatedButtons[0] = { ...updatedButtons[0], actionOnClick: checked };
413
+ updateCarouselCard(cardIndex, { buttons: updatedButtons });
414
+ };
415
+
416
+ const handleCarouselLinkTypeChange = (cardIndex, value) => {
417
+ const currentData = getCurrentCarouselData();
418
+ const card = currentData[cardIndex];
419
+ const updatedButtons = [...card.buttons];
420
+ updatedButtons[0] = { ...updatedButtons[0], linkType: value };
421
+ updateCarouselCard(cardIndex, { buttons: updatedButtons });
422
+ // Clear errors when link type changes
423
+ updateCarouselLinkError(cardIndex, 'deepLink', null);
424
+ updateCarouselLinkError(cardIndex, 'externalLink', null);
425
+ };
426
+
427
+ const handleCarouselDeepLinkChange = (cardIndex, value) => {
428
+ const currentData = getCurrentCarouselData();
429
+ const card = currentData[cardIndex];
430
+ const updatedButtons = [...card.buttons];
431
+ updatedButtons[0] = { ...updatedButtons[0], deepLinkValue: value };
432
+ updateCarouselCard(cardIndex, { buttons: updatedButtons });
433
+ // Validate deep link
434
+ const error = validateDeepLink(value, formatMessage, messages);
435
+ updateCarouselLinkError(cardIndex, 'deepLink', error);
436
+ };
437
+
438
+ const handleCarouselExternalLinkChange = (cardIndex, value) => {
439
+ const currentData = getCurrentCarouselData();
440
+ const card = currentData[cardIndex];
441
+ const updatedButtons = [...card.buttons];
442
+ updatedButtons[0] = { ...updatedButtons[0], externalLinkValue: value };
443
+ updateCarouselCard(cardIndex, { buttons: updatedButtons });
444
+ // Validate external link
445
+ const error = validateExternalLink(value, formatMessage, messages);
446
+ updateCarouselLinkError(cardIndex, 'externalLink', error);
447
+ };
448
+
449
+ const handleCarouselDeepLinkKeysChange = (cardIndex, value) => {
450
+ const currentData = getCurrentCarouselData();
451
+ const card = currentData[cardIndex];
452
+ const updatedButtons = [...card.buttons];
453
+ // Parse comma-separated values into array
454
+ const keysArray = value.split(',').map((key) => key.trim()).filter((key) => key.length > 0);
455
+ updatedButtons[0] = { ...updatedButtons[0], deepLinkKeys: keysArray };
456
+ updateCarouselCard(cardIndex, { buttons: updatedButtons });
457
+ // Validate deep link keys
458
+ const error = keysArray.length === 0 ? formatMessage(messages.deepLinkKeysRequired) : null;
459
+ updateCarouselLinkError(cardIndex, 'deepLinkKeys', error);
460
+ };
461
+
462
+ const renderCarouselImageComponent = (cardIndex) => {
463
+ const currentData = getCurrentCarouselData();
464
+ const card = currentData[cardIndex];
465
+
466
+ // Each carousel card maintains its own imageUrl
467
+ const imageSource = card.imageUrl || '';
468
+
469
+ return (
470
+ <CapImageUpload
471
+ style={{ paddingTop: "20px" }}
472
+ allowedExtensionsRegex={ALLOWED_IMAGE_EXTENSIONS_REGEX}
473
+ imgSize={MPUSH_IMG_SIZE}
474
+ imgWidth={MPUSH_IMG_MAX_WIDTH}
475
+ imgHeight={MPUSH_IMG_MAX_HEIGHT}
476
+ uploadAsset={uploadMpushAsset}
477
+ isFullMode={isFullMode}
478
+ imageSrc={imageSource}
479
+ updateImageSrc={(src) => {
480
+ // Update only this specific carousel card's image
481
+ handleCarouselImageChange(src);
482
+ }}
483
+ updateOnReUpload={handleCarouselImageReUpload}
484
+ index={activeTab === ANDROID ? 0 : 1}
485
+ className="cap-custom-image-upload"
486
+ key={`carousel-image-${activeTab}-${cardIndex}`}
487
+ imageData={{}} // Empty object for carousel images since they're stored in component state
488
+ channel={MOBILE_PUSH_CHANNEL}
489
+ showReUploadButton
490
+ errorMessage={formatMessage(messages.imageErrorMessage)}
491
+ disableAutoRestore // Disable automatic restoration for carousel images
492
+ />
493
+ );
494
+ };
495
+
496
+ const renderCarouselVideoComponent = (cardIndex) => (
497
+ <CapVideoUpload
498
+ index={activeTab === ANDROID ? 0 : 1}
499
+ allowedExtensionsRegex={ALLOWED_EXTENSIONS_VIDEO_REGEX}
500
+ videoSize={MPUSH_VIDEO_SIZE}
501
+ isFullMode={isFullMode}
502
+ uploadAsset={uploadMpushAsset}
503
+ uploadedAssetList={videoAssetList}
504
+ onVideoUploadUpdateAssestList={(src) => updateCarouselCard(cardIndex, { videoSrc: src })}
505
+ videoData={videoDataForVideo}
506
+ className="cap-custom-video-upload"
507
+ formClassName="mpush-video-upload"
508
+ channel={MOBILE_PUSH_CHANNEL}
509
+ errorMessage={formatMessage(messages.videoErrorMessage)}
510
+ showVideoNameAndDuration={false}
511
+ showReUploadButton
512
+ mediaType="video"
513
+ />
514
+ );
515
+
516
+ const renderCarouselActionLinks = (cardIndex) => {
517
+ const currentData = getCurrentCarouselData();
518
+ const card = currentData[cardIndex];
519
+
520
+ // Ensure buttons array exists and has at least one element
521
+ if (!card.buttons || !Array.isArray(card.buttons) || card.buttons.length === 0) {
522
+ return null;
523
+ }
524
+
525
+ const button = card.buttons[0]; // We're handling only one button for now
526
+
527
+ return (
528
+ <>
529
+ <CapDivider />
530
+ <CapRow className="creatives-mpush-actions">
531
+ <CapRow className="mpush-actions-main">
532
+ <CapHeading type="h4" className="mpush-actions">
533
+ <FormattedMessage {...messages.buttonsAndLinks} />
534
+ </CapHeading>
535
+ <CapLabel className="optional-text">
536
+ <FormattedMessage {...messages.optionalText} />
537
+ </CapLabel>
538
+ </CapRow>
539
+ <CapCheckbox
540
+ checked={button.actionOnClick}
541
+ onChange={(e) => handleCarouselActionOnClickChange(cardIndex, e.target.checked)}
542
+ className="action-on-click-checkbox"
543
+ >
544
+ <FormattedMessage {...messages.actionOnClickBody} />
545
+ </CapCheckbox>
546
+ <CapRow>
547
+ <CapLabel className="action-description">
548
+ <FormattedMessage {...messages.actionDescription} />
549
+ </CapLabel>
550
+ </CapRow>
551
+ {button.actionOnClick && (
552
+ <CapRow style={{ display: "flex", justifyContent: "space-around" }}>
553
+ <CapColumn span={6}>
554
+ <CapHeading type="h4" className="buttons-heading">
555
+ <FormattedMessage {...messages.linkType} />
556
+ </CapHeading>
557
+ <CapSelect.CapCustomSelect
558
+ options={LINK_TYPE_OPTIONS}
559
+ value={button.linkType}
560
+ onChange={(value) => handleCarouselLinkTypeChange(cardIndex, value)}
561
+ key={`mobile-push-link-type-${activeTab}-${cardIndex}`}
562
+ selectPlaceholder={formatMessage(messages.selectDeepLink)}
563
+ />
564
+ </CapColumn>
565
+ {button.linkType === DEEP_LINK && (
566
+ <CapColumn span={14}>
567
+ <CapHeading type="h4" className="buttons-heading">
568
+ {formatMessage(messages.deepLink)}
569
+ </CapHeading>
570
+ <CapSelect.CapCustomSelect
571
+ options={deepLink || []}
572
+ value={button.deepLinkValue}
573
+ onChange={(value) => handleCarouselDeepLinkChange(cardIndex, value)}
574
+ key={`mobile-push-deep-link-type-${activeTab}-${cardIndex}`}
575
+ placeholder={formatMessage(messages.selectDeepLink)}
576
+ style={{ marginTop: "10px" }}
577
+ />
578
+ {carouselLinkErrors[`${cardIndex}-deepLink`] && (
579
+ <CapError className="mobile-push-carousel-deep-link-error">
580
+ {carouselLinkErrors[`${cardIndex}-deepLink`]}
581
+ </CapError>
582
+ )}
583
+ </CapColumn>
584
+ )}
585
+ {button.linkType === EXTERNAL_LINK && (
586
+ <CapColumn span={14}>
587
+ <CapHeading type="h4" className="buttons-heading">
588
+ {formatMessage(messages.externalLink)}
589
+ </CapHeading>
590
+ <CapInput
591
+ id={`mobile-push-external-link-input-${activeTab}-${cardIndex}`}
592
+ onChange={(e) => handleCarouselExternalLinkChange(cardIndex, e.target.value)}
593
+ placeholder={formatMessage(messages.enterExternalLink)}
594
+ value={button.externalLinkValue}
595
+ size="default"
596
+ isRequired
597
+ />
598
+ {carouselLinkErrors[`${cardIndex}-externalLink`] && (
599
+ <CapError className="mobile-push-carousel-external-link-error">
600
+ {carouselLinkErrors[`${cardIndex}-externalLink`]}
601
+ </CapError>
602
+ )}
603
+ </CapColumn>
604
+ )}
605
+ </CapRow>
606
+ )}
607
+ {button.linkType === DEEP_LINK && button.deepLinkValue && (() => {
608
+ const selectedDeepLink = deepLink?.find((link) => link?.value === button.deepLinkValue);
609
+ const deepLinkKeysFromSelection = selectedDeepLink?.keys;
610
+ let deepLinkKeysFromSelectionArray = [];
611
+ if (Array.isArray(deepLinkKeysFromSelection)) {
612
+ deepLinkKeysFromSelectionArray = deepLinkKeysFromSelection;
613
+ } else if (deepLinkKeysFromSelection) {
614
+ deepLinkKeysFromSelectionArray = [deepLinkKeysFromSelection];
615
+ }
616
+ return deepLinkKeysFromSelectionArray.length > 0;
617
+ })() && (
618
+ <CapRow style={{ marginTop: "10px", left: "6%", width: '115%'}}>
619
+ <CapColumn span={7}>
620
+ <CapHeading type="h4" className="deep-link-keys-heading">
621
+ {formatMessage(messages.deepLinkKeys)}
622
+ </CapHeading>
623
+ <CapLabel type="label2" className="deep-link-keys-value">
624
+ {(() => {
625
+ const selectedDeepLink = deepLink?.find((link) => link?.value === button.deepLinkValue);
626
+ const deepLinkKeysFromSelection = selectedDeepLink?.keys;
627
+ let deepLinkKeysArray = [];
628
+ if (Array.isArray(button.deepLinkKeys)) {
629
+ deepLinkKeysArray = button.deepLinkKeys;
630
+ } else if (button.deepLinkKeys) {
631
+ deepLinkKeysArray = [button.deepLinkKeys];
632
+ }
633
+ let deepLinkKeysFromSelectionArray = [];
634
+ if (Array.isArray(deepLinkKeysFromSelection)) {
635
+ deepLinkKeysFromSelectionArray = deepLinkKeysFromSelection;
636
+ } else if (deepLinkKeysFromSelection) {
637
+ deepLinkKeysFromSelectionArray = [deepLinkKeysFromSelection];
638
+ }
639
+
640
+ if (deepLinkKeysFromSelectionArray.length > 0) {
641
+ return deepLinkKeysFromSelectionArray.join(', ');
642
+ }
643
+ if (deepLinkKeysArray.length > 0) {
644
+ return deepLinkKeysArray.join(', ');
645
+ }
646
+ return "No value set";
647
+ })()}
648
+ </CapLabel>
649
+ <CapInput
650
+ id={`mobile-push-carousel-deep-link-keys-input-${activeTab}-${cardIndex}`}
651
+ onChange={(e) => handleCarouselDeepLinkKeysChange(cardIndex, e.target.value)}
652
+ placeholder={formatMessage(messages.deepLinkKeysPlaceholder, {
653
+ key: (() => {
654
+ const selectedDeepLink = deepLink?.find((link) => link?.value === button.deepLinkValue);
655
+ const deepLinkKeysFromSelection = selectedDeepLink?.keys;
656
+ let deepLinkKeysFromSelectionArray = [];
657
+ if (Array.isArray(deepLinkKeysFromSelection)) {
658
+ deepLinkKeysFromSelectionArray = deepLinkKeysFromSelection;
659
+ } else if (deepLinkKeysFromSelection) {
660
+ deepLinkKeysFromSelectionArray = [deepLinkKeysFromSelection];
661
+ }
662
+ return deepLinkKeysFromSelectionArray.join(', ') || 'deep link keys';
663
+ })(),
664
+ })}
665
+ value={Array.isArray(button.deepLinkKeys) ? button.deepLinkKeys.join(', ') : (button.deepLinkKeys || "")}
666
+ size="default"
667
+ style={{ marginTop: "10px" }}
668
+ error={carouselLinkErrors[`${cardIndex}-deepLinkKeys`]}
669
+ />
670
+ </CapColumn>
671
+ </CapRow>
672
+ )}
673
+ </CapRow>
674
+ </>
675
+ );
676
+ };
677
+
678
+ const getTabPanes = () => {
679
+ const currentData = getCurrentCarouselData();
680
+ return currentData.map((data, index) => ({
681
+ key: index,
682
+ tab: index + 1,
683
+ content: (
684
+ <CapCard
685
+ title={`${formatMessage(messages.card)} ${index + 1}`}
686
+ extra={(
687
+ <CapButton
688
+ type="flat"
689
+ onClick={() => deleteCarouselCard(index)}
690
+ disabled={currentData.length === 1}
691
+ >
692
+ <CapIcon type="delete" />
693
+ </CapButton>
694
+ )}
695
+ className="mobile-push-carousel-card"
696
+ >
697
+ <CapRow>
698
+ {carouselMediaType === 'image' ? (
699
+ <CapRow>
700
+ <CapHeading type="h4">
701
+ {formatMessage(messages.mediaImage)}
702
+ </CapHeading>
703
+ {renderCarouselImageComponent(index)}
704
+ </CapRow>
705
+ ) : (
706
+ <CapRow>
707
+ <CapHeading type="h4">
708
+ {formatMessage(messages.mediaVideo)}
709
+ </CapHeading>
710
+ {renderCarouselVideoComponent(index)}
711
+ </CapRow>
712
+ )}
713
+ </CapRow>
714
+ {renderCarouselActionLinks(index)}
715
+ </CapCard>
716
+ ),
717
+ }));
718
+ };
719
+
720
+ const operations = (
721
+ <>
722
+ <CapDivider type="vertical" />
723
+ <CapButton
724
+ onClick={addCarouselCard}
725
+ type="flat"
726
+ className="add-carousel-content-button"
727
+ disabled={getCurrentCarouselData().length >= MAX_CAROUSEL_ALLOWED}
728
+ >
729
+ <CapIcon type="plus" />
730
+ </CapButton>
731
+ </>
732
+ );
733
+
734
+ return (
735
+ <div>
736
+ <CapRow>
737
+ <CapRow className="carousel-media-selection">
738
+ <CapColumn className="carousel-media-selection-heading">
739
+ <CapHeading type="h4">
740
+ {formatMessage(messages.carouselMediaType)}
741
+ </CapHeading>
742
+ </CapColumn>
743
+ <CapColumn>
744
+ <div className="carousel-radio-wrapper">
745
+ <CapRadioGroup
746
+ id="carousel-media-radio"
747
+ options={[
748
+ {
749
+ value: 'image',
750
+ label: formatMessage(messages.mediaImage),
751
+ },
752
+ {
753
+ value: 'video',
754
+ label: formatMessage(messages.mediaVideo),
755
+ disabled: true,
756
+ },
757
+ ]}
758
+ value={carouselMediaType}
759
+ onChange={handleCarouselMediaOptions}
760
+ className="mobile-push-media-radio"
761
+ />
762
+ </div>
763
+ </CapColumn>
764
+ </CapRow>
765
+ <CapRow className="mobile-push-carousel-tab">
766
+ <CapTab
767
+ defaultActiveKey="0"
768
+ activeKey={carouselActiveTabIndex.toString()}
769
+ tabBarExtraContent={operations}
770
+ onChange={onTabChange}
771
+ panes={getTabPanes()}
772
+ />
773
+ </CapRow>
774
+ </CapRow>
775
+ </div>
776
+ );
777
+ }, [
778
+ carouselMediaType,
779
+ activeTab,
780
+ getCurrentCarouselData,
781
+ setCurrentCarouselData,
782
+ carouselActiveTabIndex,
783
+ uploadMpushAsset,
784
+ isFullMode,
785
+ setUpdateMpushImageSrc,
786
+ updateOnMpushImageReUpload,
787
+ imageData,
788
+ videoAssetList,
789
+ setUpdateMpushVideoSrc,
790
+ videoDataForVideo,
791
+ formatMessage,
792
+ clearImageDataByMediaType,
793
+ mobilePushActions,
794
+ carouselLinkErrors,
795
+ updateCarouselLinkError,
796
+ carouselActiveTabIndex,
797
+ setCarouselActiveTabIndex,
798
+ ]);
799
+
800
+ if (mediaType === IMAGE) return renderImageComponent();
801
+ if (mediaType === VIDEO) return renderVideoComponent();
802
+ if (mediaType === GIF) return renderGifComponent();
803
+ if (mediaType === "CAROUSEL") return renderCarouselComponent();
804
+ return null;
805
+ };
806
+
807
+ MediaUploaders.propTypes = {
808
+ mediaType: PropTypes.string.isRequired,
809
+ activeTab: PropTypes.string.isRequired,
810
+ imageSrc: PropTypes.object.isRequired,
811
+ uploadMpushAsset: PropTypes.func.isRequired,
812
+ isFullMode: PropTypes.bool.isRequired,
813
+ setUpdateMpushImageSrc: PropTypes.func.isRequired,
814
+ updateOnMpushImageReUpload: PropTypes.func.isRequired,
815
+ imageData: PropTypes.object.isRequired,
816
+ videoAssetList: PropTypes.object.isRequired,
817
+ gifAssetList: PropTypes.object.isRequired,
818
+ setUpdateMpushVideoSrc: PropTypes.func.isRequired,
819
+ videoDataForVideo: PropTypes.object.isRequired,
820
+ videoDataForGif: PropTypes.object.isRequired,
821
+ videoSrc: PropTypes.object, // Add videoSrc prop for content state fallback
822
+ formatMessage: PropTypes.func.isRequired,
823
+ linkProps: PropTypes.object,
824
+ clearImageDataByMediaType: PropTypes.func.isRequired,
825
+ carouselData: PropTypes.array,
826
+ onCarouselDataChange: PropTypes.func,
827
+ mobilePushActions: PropTypes.object.isRequired,
828
+ carouselActiveTabIndex: PropTypes.number.isRequired,
829
+ setCarouselActiveTabIndex: PropTypes.func.isRequired,
830
+ carouselLinkErrors: PropTypes.object,
831
+ updateCarouselLinkError: PropTypes.func,
832
+ };
833
+
834
+ export default MediaUploaders;