@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.
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: 'https://crm-nightly-new.cc.capillarytech.com/arya/api/v1/creatives',
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
- //API Imports
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.156",
4
+ "version": "8.0.158",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
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?.value?.name,
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
+