@capillarytech/creatives-library 8.0.271 → 8.0.273

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 (153) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +34 -0
  8. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +17 -35
  9. package/tests/integration/TemplateCreation/api-response.js +31 -1
  10. package/tests/integration/TemplateCreation/msw-handler.js +2 -0
  11. package/utils/common.js +5 -0
  12. package/utils/commonUtils.js +28 -5
  13. package/utils/imageUrlUpload.js +13 -14
  14. package/utils/tests/commonUtil.test.js +224 -0
  15. package/utils/tests/imageUrlUpload.test.js +298 -0
  16. package/utils/transformTemplateConfig.js +0 -10
  17. package/v2Components/CapDeviceContent/index.js +61 -56
  18. package/v2Components/CapTagList/index.js +6 -1
  19. package/v2Components/CapTagListWithInput/index.js +5 -1
  20. package/v2Components/CapTagListWithInput/messages.js +1 -1
  21. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  22. package/v2Components/ErrorInfoNote/constants.js +1 -0
  23. package/v2Components/ErrorInfoNote/index.js +402 -72
  24. package/v2Components/ErrorInfoNote/messages.js +32 -6
  25. package/v2Components/ErrorInfoNote/style.scss +278 -6
  26. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  27. package/v2Components/HtmlEditor/HTMLEditor.js +418 -99
  28. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +870 -0
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1882 -133
  30. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  31. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  32. package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
  33. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
  34. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
  35. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  36. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  37. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -1
  38. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +31 -6
  39. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  40. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  41. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  42. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  43. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  44. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  45. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +7 -10
  46. package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
  47. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  48. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +18 -0
  49. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +36 -31
  50. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +46 -34
  51. package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
  52. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +52 -46
  53. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +277 -0
  54. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +295 -0
  55. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  56. package/v2Components/HtmlEditor/constants.js +45 -20
  57. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  58. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
  59. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  60. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  61. package/v2Components/HtmlEditor/hooks/useValidation.js +213 -56
  62. package/v2Components/HtmlEditor/index.js +1 -1
  63. package/v2Components/HtmlEditor/messages.js +102 -94
  64. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
  65. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
  66. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  67. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  68. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +158 -124
  69. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  70. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  71. package/v2Components/HtmlEditor/utils/validationConstants.js +38 -0
  72. package/v2Components/MobilePushPreviewV2/constants.js +6 -0
  73. package/v2Components/MobilePushPreviewV2/index.js +33 -7
  74. package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
  75. package/v2Components/TemplatePreview/index.js +47 -32
  76. package/v2Components/TemplatePreview/messages.js +4 -0
  77. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  78. package/v2Containers/BeeEditor/index.js +172 -90
  79. package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
  80. package/v2Containers/BeePopupEditor/constants.js +10 -0
  81. package/v2Containers/BeePopupEditor/index.js +194 -0
  82. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  83. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  84. package/v2Containers/CreativesContainer/SlideBoxFooter.js +156 -13
  85. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  86. package/v2Containers/CreativesContainer/constants.js +1 -0
  87. package/v2Containers/CreativesContainer/index.js +251 -47
  88. package/v2Containers/CreativesContainer/messages.js +8 -0
  89. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +103 -0
  92. package/v2Containers/Email/actions.js +7 -0
  93. package/v2Containers/Email/constants.js +5 -1
  94. package/v2Containers/Email/index.js +234 -29
  95. package/v2Containers/Email/messages.js +32 -0
  96. package/v2Containers/Email/reducer.js +12 -1
  97. package/v2Containers/Email/sagas.js +61 -7
  98. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  99. package/v2Containers/Email/tests/reducer.test.js +46 -0
  100. package/v2Containers/Email/tests/sagas.test.js +320 -29
  101. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1246 -0
  102. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +212 -21
  103. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  104. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2614 -0
  105. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  106. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  107. package/v2Containers/EmailWrapper/constants.js +2 -0
  108. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +627 -79
  109. package/v2Containers/EmailWrapper/index.js +103 -23
  110. package/v2Containers/EmailWrapper/messages.js +65 -1
  111. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +955 -0
  112. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +596 -82
  113. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  114. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  115. package/v2Containers/InApp/actions.js +7 -0
  116. package/v2Containers/InApp/constants.js +20 -4
  117. package/v2Containers/InApp/index.js +802 -360
  118. package/v2Containers/InApp/index.scss +4 -3
  119. package/v2Containers/InApp/messages.js +7 -3
  120. package/v2Containers/InApp/reducer.js +21 -3
  121. package/v2Containers/InApp/sagas.js +29 -9
  122. package/v2Containers/InApp/selectors.js +25 -5
  123. package/v2Containers/InApp/tests/index.test.js +154 -50
  124. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  125. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  126. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  127. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
  128. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  129. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
  130. package/v2Containers/InAppWrapper/constants.js +16 -0
  131. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  132. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  133. package/v2Containers/InAppWrapper/index.js +148 -0
  134. package/v2Containers/InAppWrapper/messages.js +49 -0
  135. package/v2Containers/InappAdvance/index.js +1099 -0
  136. package/v2Containers/InappAdvance/index.scss +10 -0
  137. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  138. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  139. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  140. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  141. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  142. package/v2Containers/MobilePush/Create/index.js +1 -1
  143. package/v2Containers/MobilePush/Edit/index.js +10 -6
  144. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  145. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  146. package/v2Containers/TagList/index.js +62 -19
  147. package/v2Containers/Templates/_templates.scss +60 -1
  148. package/v2Containers/Templates/index.js +89 -4
  149. package/v2Containers/Templates/messages.js +4 -0
  150. package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
  151. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  152. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  153. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Tests for imageUrlUpload utils: fetchImageFromUrl and uploadImageFromUrlHelper
3
+ */
4
+
5
+ import { fetchImageFromUrl, uploadImageFromUrlHelper } from '../imageUrlUpload';
6
+
7
+ describe('imageUrlUpload', () => {
8
+ const formatMessage = jest.fn((msg) => msg?.id ?? msg?.defaultMessage ?? 'formatted');
9
+ const messages = {
10
+ imageTypeInvalid: { id: 'imageTypeInvalid' },
11
+ imageSizeInvalid: { id: 'imageSizeInvalid' },
12
+ imageLoadError: { id: 'imageLoadError' },
13
+ };
14
+
15
+ beforeEach(() => {
16
+ jest.clearAllMocks();
17
+ formatMessage.mockImplementation((msg) => (msg && typeof msg === 'object' && msg.id) ? msg.id : 'formatted');
18
+ });
19
+
20
+ describe('fetchImageFromUrl', () => {
21
+ const validUrl = 'https://example.com/image.jpg';
22
+
23
+ it('throws when url is missing', async () => {
24
+ await expect(fetchImageFromUrl()).rejects.toThrow('URL is required');
25
+ await expect(fetchImageFromUrl(null)).rejects.toThrow('URL is required');
26
+ });
27
+
28
+ it('throws when url is empty string', async () => {
29
+ await expect(fetchImageFromUrl('')).rejects.toThrow('URL is required');
30
+ });
31
+
32
+ it('throws when url is only whitespace', async () => {
33
+ await expect(fetchImageFromUrl(' ')).rejects.toThrow('URL is required');
34
+ });
35
+
36
+ it('trims url before fetching', async () => {
37
+ const mockResponse = { ok: true, headers: new Headers() };
38
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
39
+
40
+ await fetchImageFromUrl(' https://example.com/img.png ');
41
+ expect(global.fetch).toHaveBeenCalledWith(
42
+ 'https://example.com/img.png',
43
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
44
+ );
45
+ });
46
+
47
+ it('returns response when fetch is ok', async () => {
48
+ const mockResponse = { ok: true, status: 200, headers: new Headers() };
49
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
50
+
51
+ const result = await fetchImageFromUrl(validUrl);
52
+ expect(result).toBe(mockResponse);
53
+ expect(global.fetch).toHaveBeenCalledWith(
54
+ validUrl,
55
+ expect.objectContaining({ method: 'GET', redirect: 'follow', mode: 'cors' })
56
+ );
57
+ });
58
+
59
+ it('throws when response is not ok', async () => {
60
+ const mockResponse = { ok: false, status: 404, statusText: 'Not Found' };
61
+ global.fetch = jest.fn().mockResolvedValue(mockResponse);
62
+
63
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow(
64
+ 'Failed to fetch image: 404 Not Found'
65
+ );
66
+ });
67
+
68
+ it('throws on network or CORS error', async () => {
69
+ global.fetch = jest.fn().mockRejectedValue(new Error('Network error'));
70
+
71
+ await expect(fetchImageFromUrl(validUrl)).rejects.toThrow('Network error');
72
+ });
73
+ });
74
+
75
+ describe('uploadImageFromUrlHelper', () => {
76
+ const uploadAssetFn = jest.fn();
77
+ const fileNamePrefix = 'test-image';
78
+ const maxSize = 5000000;
79
+
80
+ it('returns error when content type is not allowed', async () => {
81
+ const response = {
82
+ ok: true,
83
+ headers: new Headers({ 'Content-Type': 'text/html' }),
84
+ blob: jest.fn().mockResolvedValue(new Blob()),
85
+ };
86
+ global.fetch = jest.fn().mockResolvedValue(response);
87
+
88
+ const result = await uploadImageFromUrlHelper(
89
+ 'https://example.com/page',
90
+ formatMessage,
91
+ messages,
92
+ uploadAssetFn,
93
+ fileNamePrefix,
94
+ maxSize,
95
+ ['image/jpeg', 'image/png']
96
+ );
97
+
98
+ expect(result).toEqual({ success: false, error: 'imageTypeInvalid' });
99
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageTypeInvalid);
100
+ expect(uploadAssetFn).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it('normalizes content type by stripping charset and trimming', async () => {
104
+ const response = {
105
+ ok: true,
106
+ headers: new Headers({ 'Content-Type': ' image/PNG; charset=utf-8 ' }),
107
+ blob: jest.fn().mockResolvedValue(new Blob(['x'], { type: 'image/png' })),
108
+ };
109
+ global.fetch = jest.fn().mockResolvedValue(response);
110
+
111
+ const OriginalImage = global.Image;
112
+ global.Image = class MockImage {
113
+ constructor() {
114
+ this.width = 10;
115
+ this.height = 10;
116
+ setTimeout(() => { if (this.onload) this.onload(); }, 0);
117
+ }
118
+
119
+ get src() { return this._src; }
120
+
121
+ set src(v) { this._src = v; }
122
+ };
123
+
124
+ const result = await uploadImageFromUrlHelper(
125
+ 'https://example.com/img.png',
126
+ formatMessage,
127
+ messages,
128
+ uploadAssetFn,
129
+ fileNamePrefix,
130
+ maxSize,
131
+ ['image/png']
132
+ );
133
+
134
+ global.Image = OriginalImage;
135
+
136
+ expect(result.success).toBe(true);
137
+ expect(uploadAssetFn).toHaveBeenCalledWith(
138
+ expect.any(File),
139
+ 'image',
140
+ expect.objectContaining({ width: 10, height: 10, error: false })
141
+ );
142
+ const file = uploadAssetFn.mock.calls[0][0];
143
+ expect(file.name).toMatch(/\.png$/);
144
+ });
145
+
146
+ it('returns error when blob size exceeds maxSize', async () => {
147
+ const largeBlob = new Blob([new ArrayBuffer(maxSize + 1)]);
148
+ const response = {
149
+ ok: true,
150
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
151
+ blob: jest.fn().mockResolvedValue(largeBlob),
152
+ };
153
+ global.fetch = jest.fn().mockResolvedValue(response);
154
+
155
+ const result = await uploadImageFromUrlHelper(
156
+ 'https://example.com/large.jpg',
157
+ formatMessage,
158
+ messages,
159
+ uploadAssetFn,
160
+ fileNamePrefix,
161
+ maxSize
162
+ );
163
+
164
+ expect(result).toEqual({ success: false, error: 'imageSizeInvalid' });
165
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageSizeInvalid);
166
+ expect(uploadAssetFn).not.toHaveBeenCalled();
167
+ });
168
+
169
+ it('returns error when image fails to load (invalid image data)', async () => {
170
+ const blob = new Blob(['not-an-image'], { type: 'image/jpeg' });
171
+ const response = {
172
+ ok: true,
173
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
174
+ blob: jest.fn().mockResolvedValue(blob),
175
+ };
176
+ global.fetch = jest.fn().mockResolvedValue(response);
177
+
178
+ const OriginalImage = global.Image;
179
+ global.Image = class MockImage {
180
+ constructor() {
181
+ setTimeout(() => {
182
+ if (this.onerror) this.onerror(new Event('error'));
183
+ }, 0);
184
+ }
185
+
186
+ get src() { return this._src; }
187
+
188
+ set src(v) { this._src = v; }
189
+ };
190
+
191
+ const result = await uploadImageFromUrlHelper(
192
+ 'https://example.com/bad.jpg',
193
+ formatMessage,
194
+ messages,
195
+ uploadAssetFn,
196
+ fileNamePrefix,
197
+ maxSize
198
+ );
199
+
200
+ global.Image = OriginalImage;
201
+
202
+ expect(result.success).toBe(false);
203
+ expect(result.error).toBe('imageLoadError');
204
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
205
+ expect(uploadAssetFn).not.toHaveBeenCalled();
206
+ });
207
+
208
+ it('returns error on fetch failure and uses imageLoadError message', async () => {
209
+ global.fetch = jest.fn().mockRejectedValue(new Error('CORS'));
210
+
211
+ const result = await uploadImageFromUrlHelper(
212
+ 'https://example.com/image.jpg',
213
+ formatMessage,
214
+ messages,
215
+ uploadAssetFn,
216
+ fileNamePrefix,
217
+ maxSize
218
+ );
219
+
220
+ expect(result).toEqual({ success: false, error: 'imageLoadError' });
221
+ expect(formatMessage).toHaveBeenCalledWith(messages.imageLoadError);
222
+ expect(uploadAssetFn).not.toHaveBeenCalled();
223
+ });
224
+
225
+ it('trims url before processing', async () => {
226
+ const response = {
227
+ ok: true,
228
+ headers: new Headers({ 'Content-Type': 'image/jpeg' }),
229
+ blob: jest.fn().mockResolvedValue(new Blob()),
230
+ };
231
+ global.fetch = jest.fn().mockResolvedValue(response);
232
+
233
+ await uploadImageFromUrlHelper(
234
+ ' https://example.com/img.jpg ',
235
+ formatMessage,
236
+ messages,
237
+ uploadAssetFn,
238
+ fileNamePrefix,
239
+ maxSize
240
+ );
241
+
242
+ expect(global.fetch).toHaveBeenCalledWith(
243
+ 'https://example.com/img.jpg',
244
+ expect.any(Object)
245
+ );
246
+ });
247
+
248
+ it('calls uploadAssetFn with file, type and fileParams on success', async () => {
249
+ const blob = new Blob([new ArrayBuffer(100)], { type: 'image/png' });
250
+ const response = {
251
+ ok: true,
252
+ headers: new Headers({ 'Content-Type': 'image/png' }),
253
+ blob: jest.fn().mockResolvedValue(blob),
254
+ };
255
+ global.fetch = jest.fn().mockResolvedValue(response);
256
+
257
+ const OriginalImage = global.Image;
258
+ global.Image = class MockImage {
259
+ constructor() {
260
+ this.width = 100;
261
+ this.height = 80;
262
+ setTimeout(() => {
263
+ if (this.onload) this.onload();
264
+ }, 0);
265
+ }
266
+
267
+ get src() { return this._src; }
268
+
269
+ set src(v) { this._src = v; }
270
+ };
271
+
272
+ const result = await uploadImageFromUrlHelper(
273
+ 'https://example.com/valid.png',
274
+ formatMessage,
275
+ messages,
276
+ uploadAssetFn,
277
+ fileNamePrefix,
278
+ maxSize
279
+ );
280
+
281
+ global.Image = OriginalImage;
282
+
283
+ expect(result).toEqual({ success: true, error: '' });
284
+ expect(uploadAssetFn).toHaveBeenCalledTimes(1);
285
+ const [file, type, fileParams] = uploadAssetFn.mock.calls[0];
286
+ expect(file).toBeInstanceOf(File);
287
+ expect(file.name).toMatch(new RegExp(`^${fileNamePrefix}\\.png$`));
288
+ expect(type).toBe('image');
289
+ expect(fileParams).toEqual(
290
+ expect.objectContaining({
291
+ width: 100,
292
+ height: 80,
293
+ error: false,
294
+ })
295
+ );
296
+ });
297
+ });
298
+ });
@@ -101,7 +101,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
101
101
  const contentType = detectMediaContentType(templateConfig);
102
102
 
103
103
  if (contentType === MEDIA_CONTENT_TYPE.NONE) {
104
- console.log('No media content detected, returning original config');
105
104
  return templateConfig;
106
105
  }
107
106
 
@@ -109,24 +108,19 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
109
108
  let transformDirection;
110
109
  if (forceDirection) {
111
110
  transformDirection = forceDirection;
112
- console.log(`Forced transformation direction: ${transformDirection}`);
113
111
  } else {
114
112
  // Auto-detect based on content
115
113
  if (contentType === MEDIA_CONTENT_TYPE.URLS) {
116
114
  transformDirection = TRANSFORM_DIRECTION.FORWARD; // URLs → tags
117
- console.log('Detected URLs, applying forward transformation (URLs → tags)');
118
115
  } else if (contentType === MEDIA_CONTENT_TYPE.TAGS) {
119
116
  transformDirection = TRANSFORM_DIRECTION.REVERSE; // tags → URLs
120
- console.log('Detected media_content tags, applying reverse transformation (tags → URLs)');
121
117
  } else if (contentType === MEDIA_CONTENT_TYPE.MIXED) {
122
118
  transformDirection = TRANSFORM_DIRECTION.SMART; // Handle mixed content intelligently
123
- console.log('Detected mixed content, applying smart transformation');
124
119
  }
125
120
  }
126
121
 
127
122
  const newConfig = cloneDeep(templateConfig);
128
123
  try {
129
- console.log(`Fetching media details for ${transformDirection} transformation, ID: ${mediaDetailsId}`);
130
124
 
131
125
  // Fetch media details mapping from API
132
126
  const response = await getMediaDetails({ id: mediaDetailsId })
@@ -145,7 +139,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
145
139
  idToUrlMapping[mediaId] = url;
146
140
  });
147
141
 
148
- console.log(`Processing ${Object.keys(urlToIdMapping).length} media mappings`);
149
142
 
150
143
  // Apply transformations based on direction
151
144
  newConfig?.cards?.forEach(card => {
@@ -157,7 +150,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
157
150
  // Forward transformation: URL → tag
158
151
  const mediaDetailId = urlToIdMapping[currentUrl];
159
152
  if (mediaDetailId) {
160
- console.log(`Forward: ${currentUrl} → {{media_content(${mediaDetailId})}}`);
161
153
  card.media.url = `{{media_content(${mediaDetailId})}}`;
162
154
  } else {
163
155
  console.warn(`No media detail ID found for URL: ${currentUrl}`);
@@ -169,7 +161,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
169
161
  if (mediaId) {
170
162
  const actualUrl = idToUrlMapping[mediaId];
171
163
  if (actualUrl) {
172
- console.log(`Reverse: {{media_content(${mediaId})}} → ${actualUrl}`);
173
164
  card.media.url = actualUrl;
174
165
  } else {
175
166
  console.warn(`No URL found for media ID: ${mediaId}`);
@@ -185,7 +176,6 @@ export async function transformTemplateConfigWithMediaDetails(templateConfig, ch
185
176
  console.error('Failed to fetch media details for transformation:', error);
186
177
 
187
178
  // Fallback: try to extract/convert what we can without API
188
- console.log('Falling back to URL extraction method');
189
179
  newConfig.cards.forEach(card => {
190
180
  if (card.media && card.media.url) {
191
181
  if (transformDirection === TRANSFORM_DIRECTION.FORWARD) {
@@ -69,7 +69,7 @@ const CapDeviceContent = (props) => {
69
69
  templateDescErrorHandler,
70
70
  templateTitleError,
71
71
  setTemplateTitleError,
72
- isAiContentBotDisabled
72
+ isAiContentBotDisabled,
73
73
  } = props || {};
74
74
  const { TextArea } = CapInput;
75
75
  const { formatMessage } = intl;
@@ -167,21 +167,22 @@ const CapDeviceContent = (props) => {
167
167
  return (
168
168
  <>
169
169
  <CapRow className="creatives-device-content">
170
- <CapLink
170
+ <CapLink
171
171
  title={isAndroid
172
- ? formatMessage(messages.copyContentFromIOS)
173
- : formatMessage(messages.copyCotentFromAndroid)}
172
+ ? formatMessage(messages.copyContentFromIOS)
173
+ : formatMessage(messages.copyCotentFromAndroid)}
174
174
  className="inapp-copy-content"
175
175
  onClick={onCopyTitleAndContent}
176
- />
176
+ />
177
177
  <CapRow className="creatives-inapp-title">
178
178
  <CapColumn
179
- className="inapp-content-main"
179
+ className="inapp-content-main"
180
180
  >
181
181
  <CapHeading type="h5" className="inapp-title">
182
182
  {formatMessage(messages.title)}
183
183
  </CapHeading>
184
- {getTagList(0)} {/* here 0 signifies the tags for template title */}
184
+ {getTagList(0)}
185
+ {/* here 0 signifies the tags for template title */}
185
186
  </CapColumn>
186
187
  <CapInput
187
188
  id="inapp-title-name-input"
@@ -213,7 +214,7 @@ const CapDeviceContent = (props) => {
213
214
  </CapRow>
214
215
  <CapRow className={`creatives-inapp-message ${!isMediaTypeImage && "message-bottom-margin"}`}>
215
216
  <CapColumn
216
- className="inapp-message-header"
217
+ className="inapp-message-header"
217
218
  >
218
219
  <CapHeading type="h5" className="inapp-message-header-style">
219
220
  {formatMessage(messages.message)}
@@ -222,37 +223,37 @@ const CapDeviceContent = (props) => {
222
223
  {/* here 1 signifies the tags for template message */}
223
224
  </CapColumn>
224
225
  <div className="inapp-create-template-message-input-wrapper">
225
- <TextArea
226
- id="inapp-create-template-message-input"
227
- className="inapp-create-template-message-input"
228
- placeholder={formatMessage(messages.textAreaInputPlaceholder)}
229
- onChange={onTemplateMessageChange}
230
- value={templateMessage || ""}
231
- autosize={{ minRows: 5, maxRows: 5 }}
232
- errorMessage={
233
- templateMessageError && (
234
- <CapError className="inapp-template-message-error">
235
- {templateMessageError}
236
- </CapError>
237
- )
238
- }
239
- />
240
- {!isAiContentBotDisabled && (
241
- <CapAskAira.ContentGenerationBot
242
- text={templateMessage || ""}
243
- setText={(text) => {
244
- onTemplateMessageChange({ target: { value: text } });
245
- }}
246
- iconPlacement="float-br"
247
- iconSize="1.6rem"
248
- rootStyle={{
249
- bottom: "0.2rem",
250
- right: "0.2rem",
251
- left: "auto",
252
- position: "absolute",
253
- }}
226
+ <TextArea
227
+ id="inapp-create-template-message-input"
228
+ className="inapp-create-template-message-input"
229
+ placeholder={formatMessage(messages.textAreaInputPlaceholder)}
230
+ onChange={onTemplateMessageChange}
231
+ value={templateMessage || ""}
232
+ autosize={{ minRows: 5, maxRows: 5 }}
233
+ errorMessage={
234
+ templateMessageError && (
235
+ <CapError className="inapp-template-message-error">
236
+ {templateMessageError}
237
+ </CapError>
238
+ )
239
+ }
254
240
  />
255
- )}
241
+ {!isAiContentBotDisabled && (
242
+ <CapAskAira.ContentGenerationBot
243
+ text={templateMessage || ""}
244
+ setText={(text) => {
245
+ onTemplateMessageChange({ target: { value: text } });
246
+ }}
247
+ iconPlacement="float-br"
248
+ iconSize="1.6rem"
249
+ rootStyle={{
250
+ bottom: "0.2rem",
251
+ right: "0.2rem",
252
+ left: "auto",
253
+ position: "absolute",
254
+ }}
255
+ />
256
+ )}
256
257
  </div>
257
258
  {isMediaTypeImage && (
258
259
  <>
@@ -279,14 +280,16 @@ const CapDeviceContent = (props) => {
279
280
  </CapRow>
280
281
  </CapRow>
281
282
  <CapRow className="inapp-action-link">
282
- <CapCheckbox onChange={onActionLinkCheckBoxChange} checked={addActionLink}/>
283
+ <CapCheckbox onChange={onActionLinkCheckBoxChange} checked={addActionLink} />
283
284
  <CapRow className="inapp-render-heading">
284
285
  <CapHeader
285
- title={<CapRow type="flex">
286
- <CapHeading type="h4">
287
- {formatMessage(messages.addActionLink)}
288
- </CapHeading>
289
- </CapRow>}
286
+ title={(
287
+ <CapRow type="flex">
288
+ <CapHeading type="h4">
289
+ {formatMessage(messages.addActionLink)}
290
+ </CapHeading>
291
+ </CapRow>
292
+ )}
290
293
  description={<CapLabel type="label3">{formatMessage(messages.addActionLinkDesc)}</CapLabel>}
291
294
  />
292
295
  {addActionLink && (
@@ -310,19 +313,21 @@ const CapDeviceContent = (props) => {
310
313
  <CapRow className="inapp-cta-button">
311
314
  <CapHeader
312
315
  className="inapp-render-heading-cta-button"
313
- title={<CapRow type="flex">
314
- <CapHeading type="h4">
315
- {formatMessage(messages.btnLabel)}
316
- </CapHeading>
317
- <CapHeading
318
- type="h6"
319
- className="inapp-optional-label"
320
- >
321
- {formatMessage(messages.optional)}
322
- </CapHeading>
323
- </CapRow>}
316
+ title={(
317
+ <CapRow type="flex">
318
+ <CapHeading type="h4">
319
+ {formatMessage(messages.btnLabel)}
320
+ </CapHeading>
321
+ <CapHeading
322
+ type="h6"
323
+ className="inapp-optional-label"
324
+ >
325
+ {formatMessage(messages.optional)}
326
+ </CapHeading>
327
+ </CapRow>
328
+ )}
324
329
  description={<CapLabel type="label3">{formatMessage(messages.btnDesc)}</CapLabel>}
325
- />
330
+ />
326
331
  <CapRadioGroup
327
332
  options={BUTTON_RADIO_OPTIONS}
328
333
  value={buttonType}
@@ -227,6 +227,10 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
227
227
 
228
228
  togglePopoverVisibility = (visible) => {
229
229
  this.setState({visible});
230
+ // Call onVisibleChange callback if provided (for triggering API calls when popover opens)
231
+ if (this.props.onVisibleChange) {
232
+ this.props.onVisibleChange(visible);
233
+ }
230
234
  };
231
235
 
232
236
  renderDynamicTagFlow = () => {
@@ -468,7 +472,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
468
472
  onVisibleChange={this.togglePopoverVisibility}
469
473
  content={contentSection}
470
474
  trigger="click"
471
- placement={channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop"}
475
+ placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
472
476
  >
473
477
  <CapTooltip
474
478
  title={
@@ -535,6 +539,7 @@ CapTagList.propTypes = {
535
539
  channel: PropTypes.string,
536
540
  disabled: PropTypes.bool,
537
541
  fetchingSchemaError: PropTypes.bool,
542
+ popoverPlacement: PropTypes.string,
538
543
  };
539
544
 
540
545
  CapTagList.defaultValue = {
@@ -48,13 +48,14 @@ export const CapTagListWithInput = (props) => {
48
48
  showTagList = true,
49
49
  showInput = true,
50
50
  inputProps = {},
51
+ popoverPlacement,
51
52
  } = props;
52
53
 
53
54
  const { formatMessage } = intl;
54
55
 
55
56
  return (
56
57
  <CapColumn style={containerStyle}>
57
- <CapRow style={{display: 'flex', flexDirection: 'row'}}>
58
+ <CapRow align="middle" type="flex">
58
59
  {showHeading && headingText && (
59
60
  <CapHeading type={headingType} style={headingStyle}>
60
61
  {headingText}
@@ -76,6 +77,7 @@ export const CapTagListWithInput = (props) => {
76
77
  selectedOfferDetails={selectedOfferDetails}
77
78
  eventContextTags={eventContextTags}
78
79
  style={tagListStyle}
80
+ popoverPlacement={popoverPlacement}
79
81
  />
80
82
  )}
81
83
  </CapRow>
@@ -136,6 +138,7 @@ CapTagListWithInput.propTypes = {
136
138
  showHeading: PropTypes.bool,
137
139
  showTagList: PropTypes.bool,
138
140
  showInput: PropTypes.bool,
141
+ popoverPlacement: PropTypes.string,
139
142
  };
140
143
 
141
144
  CapTagListWithInput.defaultProps = {
@@ -164,6 +167,7 @@ CapTagListWithInput.defaultProps = {
164
167
  showTagList: true,
165
168
  showInput: true,
166
169
  inputProps: {},
170
+ popoverPlacement: undefined,
167
171
  };
168
172
 
169
173
  export default injectIntl(CapTagListWithInput);
@@ -5,6 +5,6 @@ const prefix = 'creatives.componentsV2.CapTagListWithInput';
5
5
  export default defineMessages({
6
6
  addLabels: {
7
7
  id: `${prefix}.addLabels`,
8
- defaultMessage: 'Add labels',
8
+ defaultMessage: 'Add label',
9
9
  },
10
10
  });
@@ -4,6 +4,11 @@ import '@testing-library/jest-dom';
4
4
  import { render, screen, fireEvent } from '../../../utils/test-utils';
5
5
  import { CapWhatsappCTA } from '../index';
6
6
 
7
+ // Mock the missing reducer
8
+ jest.mock('@capillarytech/cap-ui-library/CapCollapsibleLeftNavigation/reducer', () => {
9
+ return (state = {}) => state;
10
+ }, { virtual: true });
11
+
7
12
  const updateHandler = jest.fn();
8
13
  const deleteHandler = jest.fn();
9
14
  const initializeComponent = (
@@ -0,0 +1 @@
1
+ export const LIQUID_DOC_URL = 'https://docs.capillarytech.com/docs/add-dynamic-content-using-liquid';