@capillarytech/creatives-library 8.0.156 → 8.0.158
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/loading_img.gif +0 -0
- package/config/app.js +1 -1
- package/index.js +9 -1
- package/package.json +1 -1
- package/services/api.js +5 -0
- package/services/tests/api.test.js +26 -0
- package/utils/tests/transformTemplateConfig.test.js +290 -0
- package/utils/transformTemplateConfig.js +253 -0
- package/utils/whatsappMediaUtils.js +44 -0
- package/v2Containers/CreativesContainer/index.js +3 -2
- package/v2Containers/Email/tests/index.test.js +35 -0
|
Binary file
|
package/config/app.js
CHANGED
|
@@ -20,7 +20,7 @@ const config = {
|
|
|
20
20
|
accountConfig: (strs, accountId) => `${window.location.origin}/org/config/AccountAdd?q=a&channelId=2&accountId=${accountId}&edit=1`,
|
|
21
21
|
},
|
|
22
22
|
development: {
|
|
23
|
-
api_endpoint: '
|
|
23
|
+
api_endpoint: 'http://localhost:2022/arya/api/v1/creatives',
|
|
24
24
|
campaigns_api_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/iris/v2/campaigns',
|
|
25
25
|
campaigns_api_org_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/iris/v2/org/campaign',
|
|
26
26
|
auth_endpoint: 'https://crm-nightly-new.cc.capillarytech.com/arya/api/v1/auth',
|
package/index.js
CHANGED
|
@@ -103,7 +103,13 @@ import Rcs from './v2Containers/Rcs';
|
|
|
103
103
|
import rcsReducer from './v2Containers/Rcs/reducer';
|
|
104
104
|
import rcsSaga from './v2Containers/Rcs/sagas';
|
|
105
105
|
|
|
106
|
-
//
|
|
106
|
+
// Utils
|
|
107
|
+
import {
|
|
108
|
+
convertMediaTagsToUrls,
|
|
109
|
+
convertUrlsToMediaTags,
|
|
110
|
+
} from './utils/transformTemplateConfig';
|
|
111
|
+
|
|
112
|
+
//API Imports
|
|
107
113
|
import { updateMetaConfig } from './services/api';
|
|
108
114
|
|
|
109
115
|
export {default as Ebill} from './v2Containers/Ebill';
|
|
@@ -184,5 +190,7 @@ export { CapContainer,
|
|
|
184
190
|
RcsContainer,
|
|
185
191
|
ZaloContainer,
|
|
186
192
|
InAppContainer,
|
|
193
|
+
convertMediaTagsToUrls,
|
|
194
|
+
convertUrlsToMediaTags,
|
|
187
195
|
updateMetaConfig,
|
|
188
196
|
};
|
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -329,6 +329,11 @@ export const getTemplateDetails = async ({id, channel}) => {
|
|
|
329
329
|
return { ...compressedTemplatesData, response: decompressData};
|
|
330
330
|
};
|
|
331
331
|
|
|
332
|
+
export const getMediaDetails = async ({ id }) => {
|
|
333
|
+
const url = `${API_ENDPOINT}/media/${id}`;
|
|
334
|
+
return request(url, getAPICallObject('GET'));
|
|
335
|
+
};
|
|
336
|
+
|
|
332
337
|
export const getAllTemplates = async ({channel, queryParams = {}}) => {
|
|
333
338
|
const url = getUrlWithQueryParams({
|
|
334
339
|
url: `${API_ENDPOINT}/templates/v1/${channel}?`,
|
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
createTestMessageMeta,
|
|
25
25
|
updateTestMessageMeta,
|
|
26
26
|
updateMetaConfig,
|
|
27
|
+
getMediaDetails,
|
|
27
28
|
} from '../api';
|
|
28
29
|
import { mockData } from './mockData';
|
|
29
30
|
import getSchema from '../getSchema';
|
|
@@ -823,3 +824,28 @@ describe('updateMetaConfig', () => {
|
|
|
823
824
|
});
|
|
824
825
|
});
|
|
825
826
|
});
|
|
827
|
+
|
|
828
|
+
describe('getMediaDetails', () => {
|
|
829
|
+
it('should return correct response on success', async () => {
|
|
830
|
+
global.fetch.mockReturnValue(Promise.resolve({
|
|
831
|
+
status: 200,
|
|
832
|
+
json: () => Promise.resolve({
|
|
833
|
+
status: 200,
|
|
834
|
+
response: 'test media details',
|
|
835
|
+
}),
|
|
836
|
+
}));
|
|
837
|
+
const result = await getMediaDetails({ id: 'testId' });
|
|
838
|
+
expect(result).toEqual({
|
|
839
|
+
status: 200,
|
|
840
|
+
response: 'test media details',
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
it('should handle fetch failure', async () => {
|
|
845
|
+
global.fetch.mockRejectedValue({ error: 'Network error' });
|
|
846
|
+
const result = await getMediaDetails({ id: 'testId' });
|
|
847
|
+
expect(result).toEqual({
|
|
848
|
+
error: 'Network error',
|
|
849
|
+
});
|
|
850
|
+
});
|
|
851
|
+
});
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
|
|
2
|
+
import {
|
|
3
|
+
transformTemplateConfigWithMediaDetails,
|
|
4
|
+
convertMediaTagsToUrls,
|
|
5
|
+
convertUrlsToMediaTags,
|
|
6
|
+
TRANSFORM_DIRECTION,
|
|
7
|
+
MEDIA_CONTENT_TYPE,
|
|
8
|
+
} from '../transformTemplateConfig';
|
|
9
|
+
import { getMediaDetails } from '../../services/api';
|
|
10
|
+
import { WHATSAPP } from '../../v2Containers/Whatsapp/constants';
|
|
11
|
+
|
|
12
|
+
jest.mock('../../services/api', () => ({
|
|
13
|
+
getMediaDetails: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
const mockUrlToIdMapping = {
|
|
17
|
+
'http://example.com/image1.png': 'id1',
|
|
18
|
+
'http://example.com/video.mp4': 'id2',
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const mockIdToUrlMapping = {
|
|
22
|
+
id1: 'http://example.com/image1.png',
|
|
23
|
+
id2: 'http://example.com/video.mp4',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
describe('transformTemplateConfig', () => {
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
getMediaDetails.mockClear();
|
|
29
|
+
// eslint-disable-next-line no-console
|
|
30
|
+
console.warn = jest.fn();
|
|
31
|
+
// eslint-disable-next-line no-console
|
|
32
|
+
console.error = jest.fn();
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.log = jest.fn();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('transformTemplateConfigWithMediaDetails', () => {
|
|
38
|
+
it('should return original config for non-whatsapp channel', async () => {
|
|
39
|
+
const templateConfig = { cards: [{ media: { url: 'some_url' } }] };
|
|
40
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
41
|
+
templateConfig,
|
|
42
|
+
'SMS',
|
|
43
|
+
'media123',
|
|
44
|
+
);
|
|
45
|
+
expect(result).toEqual(templateConfig);
|
|
46
|
+
expect(getMediaDetails).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should return original config if no cards are present', async () => {
|
|
50
|
+
const templateConfig = { cards: [] };
|
|
51
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
52
|
+
templateConfig,
|
|
53
|
+
WHATSAPP,
|
|
54
|
+
'media123',
|
|
55
|
+
);
|
|
56
|
+
expect(result).toEqual(templateConfig);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should return original config and warn if mediaDetailsId is missing', async () => {
|
|
60
|
+
const templateConfig = { cards: [{ media: { url: 'some_url' } }] };
|
|
61
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
62
|
+
templateConfig,
|
|
63
|
+
WHATSAPP,
|
|
64
|
+
null,
|
|
65
|
+
);
|
|
66
|
+
expect(result).toEqual(templateConfig);
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
69
|
+
'mediaDetailsId is required for transformTemplateConfigWithMediaDetails',
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should handle forward transformation (URL to Tag)', async () => {
|
|
74
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
75
|
+
const templateConfig = {
|
|
76
|
+
cards: [
|
|
77
|
+
{ media: { url: 'http://example.com/image1.png' } },
|
|
78
|
+
{ media: { url: 'http://example.com/video.mp4' } },
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
83
|
+
templateConfig,
|
|
84
|
+
WHATSAPP,
|
|
85
|
+
'media123',
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(result.cards[0].media.url).toBe('{{media_content(id1)}}');
|
|
89
|
+
expect(result.cards[1].media.url).toBe('{{media_content(id2)}}');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle reverse transformation (Tag to URL)', async () => {
|
|
93
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
94
|
+
const templateConfig = {
|
|
95
|
+
cards: [
|
|
96
|
+
{ media: { url: '{{media_content(id1)}}' } },
|
|
97
|
+
{ media: { url: '{{media_content(id2)}}' } },
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
102
|
+
templateConfig,
|
|
103
|
+
WHATSAPP,
|
|
104
|
+
'media123',
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(result.cards[0].media.url).toBe(mockIdToUrlMapping.id1);
|
|
108
|
+
expect(result.cards[1].media.url).toBe(mockIdToUrlMapping.id2);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should handle smart transformation for mixed content', async () => {
|
|
112
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
113
|
+
const templateConfig = {
|
|
114
|
+
cards: [
|
|
115
|
+
{ media: { url: 'http://example.com/image1.png' } }, // URL
|
|
116
|
+
{ media: { url: '{{media_content(id2)}}' } }, // Tag
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
121
|
+
templateConfig,
|
|
122
|
+
WHATSAPP,
|
|
123
|
+
'media123',
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
expect(result.cards[0].media.url).toBe('{{media_content(id1)}}');
|
|
127
|
+
expect(result.cards[1].media.url).toBe(mockIdToUrlMapping.id2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should force forward transformation', async () => {
|
|
131
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
132
|
+
const templateConfig = {
|
|
133
|
+
cards: [{ media: { url: 'http://example.com/image1.png' } }],
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
137
|
+
templateConfig,
|
|
138
|
+
WHATSAPP,
|
|
139
|
+
'media123',
|
|
140
|
+
{ forceDirection: TRANSFORM_DIRECTION.FORWARD },
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(result.cards[0].media.url).toBe('{{media_content(id1)}}');
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('should force reverse transformation', async () => {
|
|
147
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
148
|
+
const templateConfig = {
|
|
149
|
+
cards: [{ media: { url: '{{media_content(id1)}}' } }],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
153
|
+
templateConfig,
|
|
154
|
+
WHATSAPP,
|
|
155
|
+
'media123',
|
|
156
|
+
{ forceDirection: TRANSFORM_DIRECTION.REVERSE },
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(result.cards[0].media.url).toBe(mockIdToUrlMapping.id1);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('should handle API failure gracefully', async () => {
|
|
163
|
+
const error = new Error('API Error');
|
|
164
|
+
getMediaDetails.mockRejectedValue(error);
|
|
165
|
+
const templateConfig = {
|
|
166
|
+
cards: [{ media: { url: 'http://example.com/image1.png' } }],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
170
|
+
templateConfig,
|
|
171
|
+
WHATSAPP,
|
|
172
|
+
'media123',
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
// Fallback logic does not transform
|
|
176
|
+
expect(result.cards[0].media.url).toBe('http://example.com/image1.png');
|
|
177
|
+
// eslint-disable-next-line no-console
|
|
178
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
179
|
+
'Failed to fetch media details for transformation:',
|
|
180
|
+
error,
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should warn when no media mapping is found for a URL', async () => {
|
|
185
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
186
|
+
const templateConfig = {
|
|
187
|
+
cards: [{ media: { url: 'http://example.com/unmapped.jpg' } }],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
await transformTemplateConfigWithMediaDetails(
|
|
191
|
+
templateConfig,
|
|
192
|
+
WHATSAPP,
|
|
193
|
+
'media123',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// eslint-disable-next-line no-console
|
|
197
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
198
|
+
'No media detail ID found for URL: http://example.com/unmapped.jpg',
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('should warn when no URL is found for a media ID', async () => {
|
|
203
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
204
|
+
const templateConfig = {
|
|
205
|
+
cards: [{ media: { url: '{{media_content(unmapped_id)}}' } }],
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
await transformTemplateConfigWithMediaDetails(
|
|
209
|
+
templateConfig,
|
|
210
|
+
WHATSAPP,
|
|
211
|
+
'media123',
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// eslint-disable-next-line no-console
|
|
215
|
+
expect(console.warn).toHaveBeenCalledWith(
|
|
216
|
+
'No URL found for media ID: unmapped_id',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it('should handle cards with non-string media url gracefully', async () => {
|
|
221
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
222
|
+
const templateConfig = {
|
|
223
|
+
cards: [
|
|
224
|
+
{ media: { url: 123 } },
|
|
225
|
+
],
|
|
226
|
+
};
|
|
227
|
+
const result = await transformTemplateConfigWithMediaDetails(templateConfig, WHATSAPP, 'media123');
|
|
228
|
+
expect(result.cards[0].media.url).toBe(123);
|
|
229
|
+
expect(console.warn).toHaveBeenCalledWith('No media detail ID found for URL: 123');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('should use fallback to extract media id when API fails for forced forward transform', async () => {
|
|
233
|
+
getMediaDetails.mockRejectedValue(new Error('API Error'));
|
|
234
|
+
const templateConfig = {
|
|
235
|
+
cards: [{ media: { url: '{{media_content(fallback_id)}}' } }],
|
|
236
|
+
};
|
|
237
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
238
|
+
templateConfig,
|
|
239
|
+
WHATSAPP,
|
|
240
|
+
'media123',
|
|
241
|
+
{ forceDirection: TRANSFORM_DIRECTION.FORWARD },
|
|
242
|
+
);
|
|
243
|
+
expect(result.cards[0].media.url).toBe('{{media_content(fallback_id)}}');
|
|
244
|
+
expect(console.error).toHaveBeenCalledWith(
|
|
245
|
+
'Failed to fetch media details for transformation:',
|
|
246
|
+
expect.any(Error),
|
|
247
|
+
);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should return original config if media details mapping is empty', async () => {
|
|
251
|
+
getMediaDetails.mockResolvedValue({ response: {} });
|
|
252
|
+
const templateConfig = {
|
|
253
|
+
cards: [{ media: { url: 'http://example.com/image1.png' } }],
|
|
254
|
+
};
|
|
255
|
+
const result = await transformTemplateConfigWithMediaDetails(
|
|
256
|
+
templateConfig,
|
|
257
|
+
WHATSAPP,
|
|
258
|
+
'media123',
|
|
259
|
+
);
|
|
260
|
+
expect(result).toEqual(templateConfig);
|
|
261
|
+
expect(console.warn).toHaveBeenCalledWith('No media details mapping found in API response');
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('convenience wrappers', () => {
|
|
266
|
+
it('convertUrlsToMediaTags should force forward direction', async () => {
|
|
267
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
268
|
+
const templateConfig = {
|
|
269
|
+
cards: [{ media: { url: 'http://example.com/image1.png' } }],
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const result = await convertUrlsToMediaTags(templateConfig, WHATSAPP, 'media123');
|
|
273
|
+
|
|
274
|
+
expect(getMediaDetails).toHaveBeenCalledWith({ id: 'media123' });
|
|
275
|
+
expect(result.cards[0].media.url).toBe('{{media_content(id1)}}');
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('convertMediaTagsToUrls should force reverse direction', async () => {
|
|
279
|
+
getMediaDetails.mockResolvedValue({ response: mockUrlToIdMapping });
|
|
280
|
+
const templateConfig = {
|
|
281
|
+
cards: [{ media: { url: '{{media_content(id1)}}' } }],
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const result = await convertMediaTagsToUrls(templateConfig, WHATSAPP, 'media123');
|
|
285
|
+
|
|
286
|
+
expect(getMediaDetails).toHaveBeenCalledWith({ id: 'media123' });
|
|
287
|
+
expect(result.cards[0].media.url).toBe(mockIdToUrlMapping.id1);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import cloneDeep from 'lodash/cloneDeep';
|
|
2
|
+
import { getMediaDetails } from '../services/api';
|
|
3
|
+
import { WHATSAPP } from '../v2Containers/Whatsapp/constants';
|
|
4
|
+
|
|
5
|
+
// Enum-like constants to avoid magic strings
|
|
6
|
+
export const MEDIA_CONTENT_TYPE = {
|
|
7
|
+
MIXED: 'mixed',
|
|
8
|
+
TAGS: 'tags',
|
|
9
|
+
URLS: 'urls',
|
|
10
|
+
NONE: 'none',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const TRANSFORM_DIRECTION = {
|
|
14
|
+
FORWARD: 'forward',
|
|
15
|
+
REVERSE: 'reverse',
|
|
16
|
+
SMART: 'smart',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Helper function to extract media ID from URL patterns
|
|
21
|
+
* @param {string} url - The media URL to extract ID from
|
|
22
|
+
* @returns {string|null} - Extracted media ID or null if not found
|
|
23
|
+
*/
|
|
24
|
+
function extractMediaIdFromUrl(url) {
|
|
25
|
+
if (!url || typeof url !== 'string') {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
// Already contains media_content pattern - extract the ID
|
|
31
|
+
const mediaContentMatch = url.match(/\{\{media_content\(([^)]+)\)\}\}/);
|
|
32
|
+
if (mediaContentMatch) {
|
|
33
|
+
return mediaContentMatch[1];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Helper function to detect if template config contains media_content tags or URLs
|
|
41
|
+
* @param {object} templateConfig The template configuration to analyze
|
|
42
|
+
* @returns {string} MEDIA_CONTENT_TYPE.TAGS if contains media_content tags, MEDIA_CONTENT_TYPE.URLS if contains URLs, MEDIA_CONTENT_TYPE.MIXED if both, MEDIA_CONTENT_TYPE.NONE if neither
|
|
43
|
+
*/
|
|
44
|
+
function detectMediaContentType(templateConfig) {
|
|
45
|
+
if (!templateConfig?.cards?.length) {
|
|
46
|
+
return MEDIA_CONTENT_TYPE.NONE;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
let hasUrls = false;
|
|
50
|
+
let hasTags = false;
|
|
51
|
+
|
|
52
|
+
templateConfig.cards.forEach(card => {
|
|
53
|
+
if (card?.media?.url) {
|
|
54
|
+
const url = card.media.url;
|
|
55
|
+
if (extractMediaIdFromTag(url)) {
|
|
56
|
+
hasTags = true;
|
|
57
|
+
} else if (extractMediaIdFromUrl(url)) {
|
|
58
|
+
hasUrls = true;
|
|
59
|
+
} else {
|
|
60
|
+
// Regular URL that doesn't contain extractable media ID
|
|
61
|
+
hasUrls = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
if (hasUrls && hasTags) return MEDIA_CONTENT_TYPE.MIXED;
|
|
67
|
+
if (hasTags) return MEDIA_CONTENT_TYPE.TAGS;
|
|
68
|
+
if (hasUrls) return MEDIA_CONTENT_TYPE.URLS;
|
|
69
|
+
return MEDIA_CONTENT_TYPE.NONE;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Smart bidirectional transformation function that automatically detects and applies the correct transformation.
|
|
74
|
+
* This is the recommended function to use when you have a media details ID.
|
|
75
|
+
*
|
|
76
|
+
* AUTOMATIC DETECTION:
|
|
77
|
+
* - If template contains URLs → converts to {{media_content(id)}} tags
|
|
78
|
+
* - If template contains {{media_content(id)}} tags → converts to URLs
|
|
79
|
+
* - If template contains mixed content → applies appropriate transformation per card
|
|
80
|
+
*
|
|
81
|
+
* @param {object} templateConfig The template configuration to transform.
|
|
82
|
+
* @param {string} channel The channel for which to transform the config.
|
|
83
|
+
* @param {string} mediaDetailsId The ID to use when calling getMediaDetails API.
|
|
84
|
+
* @param {object} options Additional options for the transformation.
|
|
85
|
+
* @param {string} options.forceDirection Force transformation direction: 'forward' (URLs→tags) or 'reverse' (tags→URLs).
|
|
86
|
+
* @returns {Promise<object>} Promise that resolves to the transformed template configuration.
|
|
87
|
+
*/
|
|
88
|
+
export async function transformTemplateConfigWithMediaDetails(templateConfig, channel, mediaDetailsId, options = {}) {
|
|
89
|
+
if (channel !== WHATSAPP || !templateConfig?.cards?.length) {
|
|
90
|
+
return templateConfig;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!mediaDetailsId) {
|
|
94
|
+
console.warn('mediaDetailsId is required for transformTemplateConfigWithMediaDetails');
|
|
95
|
+
return templateConfig;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const { forceDirection = null } = options;
|
|
99
|
+
|
|
100
|
+
// Detect what type of content we have
|
|
101
|
+
const contentType = detectMediaContentType(templateConfig);
|
|
102
|
+
|
|
103
|
+
if (contentType === MEDIA_CONTENT_TYPE.NONE) {
|
|
104
|
+
console.log('No media content detected, returning original config');
|
|
105
|
+
return templateConfig;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Determine transformation direction
|
|
109
|
+
let transformDirection;
|
|
110
|
+
if (forceDirection) {
|
|
111
|
+
transformDirection = forceDirection;
|
|
112
|
+
console.log(`Forced transformation direction: ${transformDirection}`);
|
|
113
|
+
} else {
|
|
114
|
+
// Auto-detect based on content
|
|
115
|
+
if (contentType === MEDIA_CONTENT_TYPE.URLS) {
|
|
116
|
+
transformDirection = TRANSFORM_DIRECTION.FORWARD; // URLs → tags
|
|
117
|
+
console.log('Detected URLs, applying forward transformation (URLs → tags)');
|
|
118
|
+
} else if (contentType === MEDIA_CONTENT_TYPE.TAGS) {
|
|
119
|
+
transformDirection = TRANSFORM_DIRECTION.REVERSE; // tags → URLs
|
|
120
|
+
console.log('Detected media_content tags, applying reverse transformation (tags → URLs)');
|
|
121
|
+
} else if (contentType === MEDIA_CONTENT_TYPE.MIXED) {
|
|
122
|
+
transformDirection = TRANSFORM_DIRECTION.SMART; // Handle mixed content intelligently
|
|
123
|
+
console.log('Detected mixed content, applying smart transformation');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const newConfig = cloneDeep(templateConfig);
|
|
128
|
+
try {
|
|
129
|
+
console.log(`Fetching media details for ${transformDirection} transformation, ID: ${mediaDetailsId}`);
|
|
130
|
+
|
|
131
|
+
// Fetch media details mapping from API
|
|
132
|
+
const response = await getMediaDetails({ id: mediaDetailsId })
|
|
133
|
+
|
|
134
|
+
// Extract the URL-to-ID mapping from the API response
|
|
135
|
+
const urlToIdMapping = response?.response || {};
|
|
136
|
+
|
|
137
|
+
if (Object.keys(urlToIdMapping).length === 0) {
|
|
138
|
+
console.warn('No media details mapping found in API response');
|
|
139
|
+
return newConfig;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Create reverse mapping: ID -> URL (for reverse transformations)
|
|
143
|
+
const idToUrlMapping = {};
|
|
144
|
+
Object.entries(urlToIdMapping).forEach(([url, mediaId]) => {
|
|
145
|
+
idToUrlMapping[mediaId] = url;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
console.log(`Processing ${Object.keys(urlToIdMapping).length} media mappings`);
|
|
149
|
+
|
|
150
|
+
// Apply transformations based on direction
|
|
151
|
+
newConfig?.cards?.forEach(card => {
|
|
152
|
+
if (card?.media && card?.media?.url) {
|
|
153
|
+
const currentUrl = card.media.url;
|
|
154
|
+
|
|
155
|
+
if (transformDirection === TRANSFORM_DIRECTION.FORWARD ||
|
|
156
|
+
(transformDirection === TRANSFORM_DIRECTION.SMART && !extractMediaIdFromTag(currentUrl))) {
|
|
157
|
+
// Forward transformation: URL → tag
|
|
158
|
+
const mediaDetailId = urlToIdMapping[currentUrl];
|
|
159
|
+
if (mediaDetailId) {
|
|
160
|
+
console.log(`Forward: ${currentUrl} → {{media_content(${mediaDetailId})}}`);
|
|
161
|
+
card.media.url = `{{media_content(${mediaDetailId})}}`;
|
|
162
|
+
} else {
|
|
163
|
+
console.warn(`No media detail ID found for URL: ${currentUrl}`);
|
|
164
|
+
}
|
|
165
|
+
} else if (transformDirection === TRANSFORM_DIRECTION.REVERSE ||
|
|
166
|
+
(transformDirection === TRANSFORM_DIRECTION.SMART && extractMediaIdFromTag(currentUrl))) {
|
|
167
|
+
// Reverse transformation: tag → URL
|
|
168
|
+
const mediaId = extractMediaIdFromTag(currentUrl);
|
|
169
|
+
if (mediaId) {
|
|
170
|
+
const actualUrl = idToUrlMapping[mediaId];
|
|
171
|
+
if (actualUrl) {
|
|
172
|
+
console.log(`Reverse: {{media_content(${mediaId})}} → ${actualUrl}`);
|
|
173
|
+
card.media.url = actualUrl;
|
|
174
|
+
} else {
|
|
175
|
+
console.warn(`No URL found for media ID: ${mediaId}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return newConfig;
|
|
183
|
+
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Failed to fetch media details for transformation:', error);
|
|
186
|
+
|
|
187
|
+
// Fallback: try to extract/convert what we can without API
|
|
188
|
+
console.log('Falling back to URL extraction method');
|
|
189
|
+
newConfig.cards.forEach(card => {
|
|
190
|
+
if (card.media && card.media.url) {
|
|
191
|
+
if (transformDirection === TRANSFORM_DIRECTION.FORWARD) {
|
|
192
|
+
const extractedId = extractMediaIdFromUrl(card.media.url);
|
|
193
|
+
if (extractedId) {
|
|
194
|
+
card.media.url = `{{media_content(${extractedId})}}`;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// For reverse transformation, we can't do much without the API mapping
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// cleanup the newConfig
|
|
202
|
+
delete newConfig.accessToken;
|
|
203
|
+
|
|
204
|
+
return newConfig;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Helper function to extract media ID from media_content tags
|
|
210
|
+
* @param {string} url - The URL containing media_content tag
|
|
211
|
+
* @returns {string|null} - Extracted media ID or null if not found
|
|
212
|
+
*/
|
|
213
|
+
function extractMediaIdFromTag(url) {
|
|
214
|
+
if (!url || typeof url !== 'string') {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Extract media ID from {{media_content(id)}} pattern
|
|
219
|
+
const tagMatch = url.match(/\{\{media_content\(([^)]+)\)\}\}/);
|
|
220
|
+
return tagMatch ? tagMatch[1] : null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Convenience function for reverse transformation: fetches media details and converts tags to URLs in one call.
|
|
225
|
+
* This function forces reverse transformation direction. For automatic detection, use transformTemplateConfigWithMediaDetails.
|
|
226
|
+
* @param {object} templateConfig The template configuration containing media_content tags.
|
|
227
|
+
* @param {string} channel The channel for which to transform the config.
|
|
228
|
+
* @param {string} mediaDetailsId The ID to use when calling getMediaDetails API.
|
|
229
|
+
* @param {object} options Additional options for the transformation.
|
|
230
|
+
* @returns {Promise<object>} Promise that resolves to the template config with actual URLs.
|
|
231
|
+
*/
|
|
232
|
+
export async function convertMediaTagsToUrls(templateConfig, channel, mediaDetailsId, options = {}) {
|
|
233
|
+
return transformTemplateConfigWithMediaDetails(templateConfig, channel, mediaDetailsId, {
|
|
234
|
+
...options,
|
|
235
|
+
forceDirection: TRANSFORM_DIRECTION.REVERSE
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Convenience function for forward transformation: fetches media details and converts URLs to tags in one call.
|
|
241
|
+
* This function forces forward transformation direction. For automatic detection, use transformTemplateConfigWithMediaDetails.
|
|
242
|
+
* @param {object} templateConfig The template configuration containing URLs.
|
|
243
|
+
* @param {string} channel The channel for which to transform the config.
|
|
244
|
+
* @param {string} mediaDetailsId The ID to use when calling getMediaDetails API.
|
|
245
|
+
* @param {object} options Additional options for the transformation.
|
|
246
|
+
* @returns {Promise<object>} Promise that resolves to the template config with media_content tags.
|
|
247
|
+
*/
|
|
248
|
+
export async function convertUrlsToMediaTags(templateConfig, channel, mediaDetailsId, options = {}) {
|
|
249
|
+
return transformTemplateConfigWithMediaDetails(templateConfig, channel, mediaDetailsId, {
|
|
250
|
+
...options,
|
|
251
|
+
forceDirection: TRANSFORM_DIRECTION.FORWARD
|
|
252
|
+
});
|
|
253
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for handling WhatsApp media URL transformations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Extracts creative ID from media URL pattern
|
|
7
|
+
* @param {string} mediaUrl - URL with pattern {{media_content(<creativeId>_<mediaDetailId>)}}
|
|
8
|
+
* @returns {string|null} - Extracted creative ID or null if not found
|
|
9
|
+
*/
|
|
10
|
+
export const extractCreativeIdFromMediaUrl = (mediaUrl) => {
|
|
11
|
+
if (!mediaUrl || typeof mediaUrl !== 'string') {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Extract creativeId from pattern: {{media_content(<creativeId>_<mediaDetailId>)}}
|
|
16
|
+
const match = mediaUrl.match(/\{\{media_content\(([^_]+)_[^)]+\)\}\}/);
|
|
17
|
+
return match ? match[1] : null;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates URL mapping from API response format to template pattern format
|
|
22
|
+
* @param {Object} mediaDetails - API response with format {url: "mediaDetailId_creativeId"}
|
|
23
|
+
* @returns {Object} - Mapping object {creativeId_mediaDetailId: url}
|
|
24
|
+
*/
|
|
25
|
+
export const createMediaUrlMapping = (mediaDetails) => {
|
|
26
|
+
const mediaUrlMap = {};
|
|
27
|
+
|
|
28
|
+
if (!mediaDetails || typeof mediaDetails !== 'object') {
|
|
29
|
+
return mediaUrlMap;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
Object.keys(mediaDetails).forEach(url => {
|
|
33
|
+
const value = mediaDetails[url];
|
|
34
|
+
// API response format: "mediaDetailId_creativeId"
|
|
35
|
+
// We need to reverse it to match our pattern: "creativeId_mediaDetailId"
|
|
36
|
+
if (typeof value === 'string' && value.includes('_')) {
|
|
37
|
+
const [mediaDetailId, creativeId] = value.split('_');
|
|
38
|
+
const templatePattern = `${mediaDetailId}_${creativeId}`;
|
|
39
|
+
mediaUrlMap[templatePattern] = url;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return mediaUrlMap;
|
|
44
|
+
};
|
|
@@ -81,7 +81,7 @@ const SlideBoxWrapper = styled.div`
|
|
|
81
81
|
export class Creatives extends React.Component {
|
|
82
82
|
constructor(props) {
|
|
83
83
|
super(props);
|
|
84
|
-
|
|
84
|
+
|
|
85
85
|
const initialSlidBoxContent = this.getSlideBoxContent({ mode: props.creativesMode, templateData: props.templateData, isFullMode: props.isFullMode });
|
|
86
86
|
|
|
87
87
|
this.state = {
|
|
@@ -954,6 +954,7 @@ export class Creatives extends React.Component {
|
|
|
954
954
|
default:
|
|
955
955
|
break;
|
|
956
956
|
}
|
|
957
|
+
|
|
957
958
|
templateData = {
|
|
958
959
|
channel,
|
|
959
960
|
accountId,
|
|
@@ -961,7 +962,7 @@ export class Creatives extends React.Component {
|
|
|
961
962
|
accountName: accountDetails?.name || '',
|
|
962
963
|
messageBody: languages[0].content,
|
|
963
964
|
templateConfigs: {
|
|
964
|
-
id: template?.
|
|
965
|
+
id: template?._id,
|
|
965
966
|
name: template?.value?.name,
|
|
966
967
|
template: templateEditor,
|
|
967
968
|
varMapped,
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { injectIntl } from 'react-intl';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import {
|
|
5
|
+
render,
|
|
6
|
+
} from '../../../utils/test-utils';
|
|
7
|
+
import { EMAILPreviewMockData } from '../mockdata/mockdata';
|
|
8
|
+
import { Email } from "../index";
|
|
9
|
+
|
|
10
|
+
const initializeComponent = () => {
|
|
11
|
+
const Component = injectIntl(Email);
|
|
12
|
+
const resetUploadData = jest.fn();
|
|
13
|
+
const clearStoreValues = jest.fn();
|
|
14
|
+
const clearCRUDResponse = jest.fn();
|
|
15
|
+
const fetchSchemaForEntity = jest.fn();
|
|
16
|
+
|
|
17
|
+
return render( <Component
|
|
18
|
+
{...EMAILPreviewMockData}
|
|
19
|
+
templatesActions={{resetUploadData}}
|
|
20
|
+
actions={{
|
|
21
|
+
clearStoreValues,
|
|
22
|
+
clearCRUDResponse,
|
|
23
|
+
}}
|
|
24
|
+
globalActions={{
|
|
25
|
+
fetchSchemaForEntity,
|
|
26
|
+
}}
|
|
27
|
+
/>);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
describe('renders a message', () => {
|
|
31
|
+
it("Test if Email component renders", () => {
|
|
32
|
+
initializeComponent();
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|