@capillarytech/creatives-library 8.0.330-alpha.0 → 8.0.330-alpha.2
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/package.json +1 -1
- package/utils/tests/tagValidations.test.js +20 -0
- package/v2Components/CapActionButton/constants.js +7 -0
- package/v2Components/CapActionButton/index.js +167 -109
- package/v2Components/CapActionButton/index.scss +157 -6
- package/v2Components/CapActionButton/messages.js +19 -3
- package/v2Components/CapActionButton/tests/index.test.js +41 -17
- package/v2Components/CapTagList/index.js +28 -23
- package/v2Components/CapTagList/style.scss +29 -0
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
- package/v2Components/CapTagListWithInput/index.js +4 -0
- package/v2Components/CapWhatsappCTA/index.js +2 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
- package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
- package/v2Components/CommonTestAndPreview/messages.js +8 -0
- package/v2Components/CommonTestAndPreview/reducer.js +3 -1
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
- package/v2Components/FormBuilder/index.js +1 -0
- package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
- package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
- package/v2Components/TemplatePreview/constants.js +2 -0
- package/v2Components/TemplatePreview/index.js +143 -28
- package/v2Components/TemplatePreview/tests/index.test.js +142 -0
- package/v2Components/mockdata.js +1 -0
- package/v2Containers/BeeEditor/index.js +19 -1
- package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
- package/v2Containers/Email/index.js +78 -39
- package/v2Containers/Email/reducer.js +2 -2
- package/v2Containers/Email/sagas.js +3 -1
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
- package/v2Containers/Email/tests/sagas.test.js +230 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
- package/v2Containers/EmailWrapper/index.js +4 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +2 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -0
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/constants.js +79 -5
- package/v2Containers/Rcs/index.js +1374 -73
- package/v2Containers/Rcs/index.js.rej +1336 -0
- package/v2Containers/Rcs/index.scss +191 -0
- package/v2Containers/Rcs/index.scss.rej +74 -0
- package/v2Containers/Rcs/messages.js +26 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69173 -118166
- package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
- package/v2Containers/Rcs/tests/index.test.js +132 -94
- package/v2Containers/Rcs/tests/utils.test.js +220 -38
- package/v2Containers/Rcs/utils.js +77 -1
- package/v2Containers/Sms/Edit/index.js +2 -0
- package/v2Containers/SmsWrapper/index.js +2 -0
- package/v2Containers/TagList/index.js +73 -20
- package/v2Containers/TagList/messages.js +4 -0
- package/v2Containers/TagList/tests/TagList.test.js +124 -20
- package/v2Containers/TagList/tests/mockdata.js +17 -0
- package/v2Containers/Templates/_templates.scss +99 -0
- package/v2Containers/Templates/index.js +29 -14
- package/v2Containers/Viber/index.js +3 -0
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
- package/v2Containers/WebPush/Create/index.js +10 -2
- package/v2Containers/Whatsapp/index.js +5 -0
- package/v2Containers/Zalo/index.js +2 -0
|
@@ -41,38 +41,26 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
|
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
const renderWithSuggestion = (buttonType, text) => {
|
|
45
45
|
const template = JSON.parse(JSON.stringify(templateBase));
|
|
46
46
|
template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
|
|
47
|
-
{ type:
|
|
47
|
+
{ type: buttonType, text },
|
|
48
48
|
];
|
|
49
49
|
render(getRCSContent(template));
|
|
50
|
-
|
|
51
|
-
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
52
|
-
expect(labels.length).toBe(1);
|
|
53
|
-
expect(labels[0].textContent).toContain('Reply');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('renders CTA suggestion with launch icon', () => {
|
|
57
|
-
const template = JSON.parse(JSON.stringify(templateBase));
|
|
58
|
-
template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
|
|
59
|
-
{ type: RCS_BUTTON_TYPES.CTA, text: 'Visit' },
|
|
60
|
-
];
|
|
61
|
-
render(getRCSContent(template));
|
|
62
|
-
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
63
|
-
expect(labels.length).toBe(1);
|
|
64
|
-
expect(labels[0].textContent).toContain('Visit');
|
|
65
|
-
});
|
|
50
|
+
};
|
|
66
51
|
|
|
67
|
-
it(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
52
|
+
it.each([
|
|
53
|
+
[RCS_BUTTON_TYPES.QUICK_REPLY, 'Reply', true],
|
|
54
|
+
[RCS_BUTTON_TYPES.CTA, 'Visit', false],
|
|
55
|
+
[RCS_BUTTON_TYPES.PHONE_NUMBER, 'Call', false],
|
|
56
|
+
])('renders %s suggestion with expected label', (buttonType, text, expectDivider) => {
|
|
57
|
+
renderWithSuggestion(buttonType, text);
|
|
58
|
+
if (expectDivider) {
|
|
59
|
+
expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
|
|
60
|
+
}
|
|
73
61
|
const labels = document.querySelectorAll('.rcs-cta-preview');
|
|
74
62
|
expect(labels.length).toBe(1);
|
|
75
|
-
expect(labels[0].textContent).toContain(
|
|
63
|
+
expect(labels[0].textContent).toContain(text);
|
|
76
64
|
});
|
|
77
65
|
|
|
78
66
|
it('renders only divider for unknown suggestion type', () => {
|
|
@@ -97,16 +85,17 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
|
|
|
97
85
|
|
|
98
86
|
describe('RCS utils', () => {
|
|
99
87
|
describe('getRCSContent', () => {
|
|
88
|
+
/** Single place for snapshot serialization — keeps test bodies focused on template setup. */
|
|
89
|
+
const expectGetRCSContentSnapshot = (template) => {
|
|
90
|
+
expect(renderer.create(getRCSContent(template)).toJSON()).toMatchSnapshot();
|
|
91
|
+
};
|
|
92
|
+
|
|
100
93
|
it('renders RCS content with image and CTA suggestion', () => {
|
|
101
|
-
|
|
102
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
103
|
-
expect(tree).toMatchSnapshot();
|
|
94
|
+
expectGetRCSContentSnapshot(mockData.editData1.templateDetails);
|
|
104
95
|
});
|
|
105
96
|
|
|
106
97
|
it('renders RCS content with no media', () => {
|
|
107
|
-
|
|
108
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
109
|
-
expect(tree).toMatchSnapshot();
|
|
98
|
+
expectGetRCSContentSnapshot(mockData.editData3.templateDetails);
|
|
110
99
|
});
|
|
111
100
|
|
|
112
101
|
it('renders RCS content with multiple suggestion types', () => {
|
|
@@ -116,19 +105,212 @@ describe('RCS utils', () => {
|
|
|
116
105
|
{ type: RCS_BUTTON_TYPES.CTA, text: 'URL Action' },
|
|
117
106
|
{ type: RCS_BUTTON_TYPES.PHONE_NUMBER, text: 'Call Now' },
|
|
118
107
|
];
|
|
119
|
-
|
|
120
|
-
expect(tree).toMatchSnapshot();
|
|
108
|
+
expectGetRCSContentSnapshot(template);
|
|
121
109
|
});
|
|
122
110
|
|
|
123
111
|
it('renders RCS content with missing cardContent', () => {
|
|
124
|
-
|
|
125
|
-
const tree = renderer.create(getRCSContent(template)).toJSON();
|
|
126
|
-
expect(tree).toMatchSnapshot();
|
|
112
|
+
expectGetRCSContentSnapshot({ versions: { base: { content: { RCS: { rcsContent: {} } } } } });
|
|
127
113
|
});
|
|
128
114
|
|
|
129
115
|
it('renders RCS content with empty template', () => {
|
|
130
|
-
|
|
131
|
-
|
|
116
|
+
expectGetRCSContentSnapshot({});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('renders RCS carousel listing preview when cardType is carousel', () => {
|
|
120
|
+
const template = JSON.parse(JSON.stringify(mockData.editData1.templateDetails));
|
|
121
|
+
const rcsContent = template.versions.base.content.RCS.rcsContent;
|
|
122
|
+
rcsContent.cardType = 'CAROUSEL';
|
|
123
|
+
rcsContent.cardContent = [
|
|
124
|
+
{
|
|
125
|
+
title: 'Card1',
|
|
126
|
+
description: 'Desc1',
|
|
127
|
+
media: { mediaUrl: 'https://example.com/1.png' },
|
|
128
|
+
suggestions: [],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
title: 'Card2',
|
|
132
|
+
description: 'Desc2',
|
|
133
|
+
media: { mediaUrl: 'https://example.com/2.png' },
|
|
134
|
+
suggestions: [],
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
title: 'Card3',
|
|
138
|
+
description: 'Desc3',
|
|
139
|
+
media: { mediaUrl: 'https://example.com/3.png' },
|
|
140
|
+
suggestions: [],
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
title: 'Card4',
|
|
144
|
+
description: 'Desc4',
|
|
145
|
+
media: { mediaUrl: 'https://example.com/4.png' },
|
|
146
|
+
suggestions: [],
|
|
147
|
+
},
|
|
148
|
+
];
|
|
149
|
+
|
|
150
|
+
render(getRCSContent(template));
|
|
151
|
+
// "Peek" shows max 3
|
|
152
|
+
expect(document.querySelectorAll('.whatsapp-carousel-container').length).toBe(3);
|
|
153
|
+
expect(document.querySelectorAll('.scroll-container').length).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const carouselTemplate = (cardContent) => ({
|
|
157
|
+
versions: {
|
|
158
|
+
base: {
|
|
159
|
+
content: {
|
|
160
|
+
[RCS]: {
|
|
161
|
+
rcsContent: {
|
|
162
|
+
cardType: 'carousel',
|
|
163
|
+
cardContent,
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const carouselSecondCardFiller = {
|
|
172
|
+
title: 'B',
|
|
173
|
+
description: 'D',
|
|
174
|
+
media: { mediaUrl: 'https://example.com/b.png' },
|
|
175
|
+
suggestions: [],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
it('carousel: video card with thumbnailUrl uses thumbnail (not placeholder)', () => {
|
|
179
|
+
const template = carouselTemplate([
|
|
180
|
+
{
|
|
181
|
+
title: 'Vid',
|
|
182
|
+
description: 'With thumb',
|
|
183
|
+
media: {
|
|
184
|
+
mediaType: 'VIDEO',
|
|
185
|
+
mediaUrl: 'https://example.com/video.mp4',
|
|
186
|
+
thumbnailUrl: 'https://example.com/poster.jpg',
|
|
187
|
+
},
|
|
188
|
+
suggestions: [],
|
|
189
|
+
},
|
|
190
|
+
carouselSecondCardFiller,
|
|
191
|
+
]);
|
|
192
|
+
render(getRCSContent(template));
|
|
193
|
+
expect(document.querySelector('.rcs-video-preview-placeholder')).toBeFalsy();
|
|
194
|
+
const firstImg = document.querySelector('.whatsapp-carousel-card .whatsapp-image');
|
|
195
|
+
expect(firstImg).toBeTruthy();
|
|
196
|
+
expect(firstImg.getAttribute('src')).toBe('https://example.com/poster.jpg');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('carousel: shows video preview placeholder when VIDEO has mediaUrl but no thumbnail (no img src to mp4)', () => {
|
|
200
|
+
const template = carouselTemplate([
|
|
201
|
+
{
|
|
202
|
+
title: 'Clip',
|
|
203
|
+
description: 'No thumb',
|
|
204
|
+
media: {
|
|
205
|
+
mediaType: 'VIDEO',
|
|
206
|
+
mediaUrl: 'https://example.com/video.mp4',
|
|
207
|
+
},
|
|
208
|
+
suggestions: [],
|
|
209
|
+
},
|
|
210
|
+
carouselSecondCardFiller,
|
|
211
|
+
]);
|
|
212
|
+
render(getRCSContent(template));
|
|
213
|
+
expect(document.querySelector('.rcs-video-preview-placeholder')).toBeTruthy();
|
|
214
|
+
expect(document.querySelector('.rcs-video-preview-label')?.textContent).toContain('Video preview');
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('carousel: uses whatsapp-message-without-media when card has no image/video URL', () => {
|
|
218
|
+
const template = carouselTemplate([
|
|
219
|
+
{
|
|
220
|
+
title: 'Text only',
|
|
221
|
+
description: 'No media',
|
|
222
|
+
media: {},
|
|
223
|
+
suggestions: [],
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
title: 'Second',
|
|
227
|
+
description: '',
|
|
228
|
+
suggestions: [],
|
|
229
|
+
},
|
|
230
|
+
]);
|
|
231
|
+
render(getRCSContent(template));
|
|
232
|
+
const withoutMedia = document.querySelectorAll('.whatsapp-message-without-media');
|
|
233
|
+
expect(withoutMedia.length).toBeGreaterThan(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('carousel: body includes description with newline when description is set', () => {
|
|
237
|
+
const template = carouselTemplate([
|
|
238
|
+
{
|
|
239
|
+
title: 'Head',
|
|
240
|
+
description: 'Tail line',
|
|
241
|
+
media: { mediaUrl: 'https://example.com/x.png' },
|
|
242
|
+
suggestions: [],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
title: 'Y',
|
|
246
|
+
description: 'Z',
|
|
247
|
+
media: { mediaUrl: 'https://example.com/y.png' },
|
|
248
|
+
suggestions: [],
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
render(getRCSContent(template));
|
|
252
|
+
const bodies = document.querySelectorAll('.whatsapp-carousel-body');
|
|
253
|
+
expect(bodies[0].textContent).toContain('Head');
|
|
254
|
+
expect(bodies[0].textContent).toContain('Tail line');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('carousel: body handles missing title and empty description', () => {
|
|
258
|
+
const template = carouselTemplate([
|
|
259
|
+
{
|
|
260
|
+
description: '',
|
|
261
|
+
media: { mediaUrl: 'https://example.com/x.png' },
|
|
262
|
+
suggestions: [],
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
title: 'Only title',
|
|
266
|
+
media: { mediaUrl: 'https://example.com/y.png' },
|
|
267
|
+
suggestions: [],
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
render(getRCSContent(template));
|
|
271
|
+
const bodies = document.querySelectorAll('.whatsapp-carousel-body');
|
|
272
|
+
expect(bodies[0].textContent.trim()).toBe('');
|
|
273
|
+
expect(bodies[1].textContent).toContain('Only title');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('carousel: renders per-card suggestions via renderRcsSuggestionsPreview', () => {
|
|
277
|
+
const template = carouselTemplate([
|
|
278
|
+
{
|
|
279
|
+
title: 'C1',
|
|
280
|
+
description: 'D1',
|
|
281
|
+
media: { mediaUrl: 'https://example.com/1.png' },
|
|
282
|
+
suggestions: [{ type: RCS_BUTTON_TYPES.CTA, text: 'Go' }],
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
title: 'C2',
|
|
286
|
+
description: 'D2',
|
|
287
|
+
media: { mediaUrl: 'https://example.com/2.png' },
|
|
288
|
+
suggestions: [],
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
render(getRCSContent(template));
|
|
292
|
+
expect(document.querySelectorAll('.rcs-cta-preview').length).toBeGreaterThanOrEqual(1);
|
|
293
|
+
expect(Array.from(document.querySelectorAll('.rcs-cta-preview')).some((el) => el.textContent.includes('Go'))).toBe(true);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('carousel: does not render suggestion block when suggestions missing or not an array', () => {
|
|
297
|
+
const template = carouselTemplate([
|
|
298
|
+
{
|
|
299
|
+
title: 'A',
|
|
300
|
+
description: 'B',
|
|
301
|
+
media: { mediaUrl: 'https://example.com/a.png' },
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
title: 'C',
|
|
305
|
+
description: 'D',
|
|
306
|
+
media: { mediaUrl: 'https://example.com/c.png' },
|
|
307
|
+
suggestions: null,
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
render(getRCSContent(template));
|
|
311
|
+
const firstCard = document.querySelectorAll('.whatsapp-carousel-card')[0];
|
|
312
|
+
const dividersInFirst = firstCard.querySelectorAll('.whatsapp-divider');
|
|
313
|
+
expect(dividersInFirst.length).toBe(0);
|
|
132
314
|
});
|
|
133
315
|
});
|
|
134
316
|
|
|
@@ -50,6 +50,13 @@ export const getTemplateStatusType = (templateStatus) => {
|
|
|
50
50
|
}
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
+
/** Localized label for a carousel video thumbnail size (width × height in px). */
|
|
54
|
+
export const formatRcsCarouselVideoThumbnailLabel = (formatMessage, dimensionEntry) => {
|
|
55
|
+
if (!dimensionEntry) return '';
|
|
56
|
+
const { width, height } = dimensionEntry;
|
|
57
|
+
return formatMessage(messages.rcsCarouselVideoThumbnailLabel, { width, height });
|
|
58
|
+
};
|
|
59
|
+
|
|
53
60
|
/**
|
|
54
61
|
* Global RegExp matching `{{numericVarName}}` in RCS template strings.
|
|
55
62
|
* `numericVarName` is escaped for regex metacharacters.
|
|
@@ -162,9 +169,13 @@ export function coalesceCardVarMappedToTemplate(
|
|
|
162
169
|
templateTitle,
|
|
163
170
|
templateDesc,
|
|
164
171
|
rcsVarRegex,
|
|
172
|
+
rcsVarRegexParam,
|
|
165
173
|
) {
|
|
166
174
|
const getVarNameFromToken = (token = '') => token.replace(RCS_STRIP_MUSTACHE_DELIMITERS_REGEX, '');
|
|
167
175
|
const templateVarTokens = [
|
|
176
|
+
...(templateTitle?.match(rcsVarRegexParam) ?? []),
|
|
177
|
+
rcsVarRegexParam,
|
|
178
|
+
...(templateDesc?.match(rcsVarRegexParam) ?? []),
|
|
168
179
|
...(templateTitle?.match(rcsVarRegex) ?? []),
|
|
169
180
|
...(templateDesc?.match(rcsVarRegex) ?? []),
|
|
170
181
|
];
|
|
@@ -247,6 +258,7 @@ export function resolveCardVarMappedSlotValue(
|
|
|
247
258
|
}
|
|
248
259
|
let numericSlotValue = '';
|
|
249
260
|
if (Object.prototype.hasOwnProperty.call(varMap, slotKey)) {
|
|
261
|
+
/** Text-only RCS card: editor shows a single "Text message" field (description); title row is hidden. */
|
|
250
262
|
numericSlotValue = String(varMap[slotKey] ?? '');
|
|
251
263
|
} else if (Object.prototype.hasOwnProperty.call(varMap, globalSlotIndexZeroBased + 1)) {
|
|
252
264
|
numericSlotValue = String(varMap[globalSlotIndexZeroBased + 1] ?? '');
|
|
@@ -463,6 +475,7 @@ export const getRCSContent = (template) => {
|
|
|
463
475
|
content: {
|
|
464
476
|
[RCS]: {
|
|
465
477
|
rcsContent: {
|
|
478
|
+
cardType = '',
|
|
466
479
|
cardContent = [{}],
|
|
467
480
|
cardSettings = {},
|
|
468
481
|
} = {},
|
|
@@ -478,8 +491,72 @@ export const getRCSContent = (template) => {
|
|
|
478
491
|
mediaType,
|
|
479
492
|
suggestions = [],
|
|
480
493
|
} = cardContent[0];
|
|
494
|
+
const isCarousel =
|
|
495
|
+
(cardType || '').toString().toLowerCase() === 'carousel' ||
|
|
496
|
+
(cardContent || []).length > 1;
|
|
481
497
|
const isTextOnlyCard = isRcsTextOnlyCardMediaType(mediaType);
|
|
482
498
|
const mediaPreview = media?.thumbnailUrl ? media.thumbnailUrl : media.mediaUrl;
|
|
499
|
+
|
|
500
|
+
const renderCarouselListingPreview = () => {
|
|
501
|
+
const cards = Array.isArray(cardContent) ? cardContent : [];
|
|
502
|
+
if (!cards.length) return null;
|
|
503
|
+
const cardsToShow = cards.slice(0, 3); // enough to show a "peek" of next card
|
|
504
|
+
return (
|
|
505
|
+
<div className="scroll-container">
|
|
506
|
+
{cardsToShow.map((c, idx) => {
|
|
507
|
+
const m = c?.media || {};
|
|
508
|
+
const isVideo = (m?.mediaType || '').toString().toUpperCase() === RCS_MEDIA_TYPES.VIDEO;
|
|
509
|
+
const thumbUrl = m?.thumbnailUrl;
|
|
510
|
+
const mediaUrl = m?.mediaUrl;
|
|
511
|
+
// Avoid rendering an <img src="...mp4"> when a video doesn't have a thumbnail.
|
|
512
|
+
const url = thumbUrl ? thumbUrl : (isVideo ? '' : mediaUrl);
|
|
513
|
+
return (
|
|
514
|
+
<div
|
|
515
|
+
key={`rcs-carousel-listing-${idx}-${url || ''}`}
|
|
516
|
+
className="whatsapp-carousel-container"
|
|
517
|
+
role="group"
|
|
518
|
+
>
|
|
519
|
+
<div className="whatsapp-carousel-card">
|
|
520
|
+
{url && (
|
|
521
|
+
<CapImage
|
|
522
|
+
src={url}
|
|
523
|
+
className="whatsapp-image"
|
|
524
|
+
/>
|
|
525
|
+
)}
|
|
526
|
+
{!url && isVideo && (
|
|
527
|
+
<div
|
|
528
|
+
className="whatsapp-image video-preview rcs-video-preview-placeholder"
|
|
529
|
+
>
|
|
530
|
+
<CapLabel type="label9" className="rcs-video-preview-label">
|
|
531
|
+
Video preview
|
|
532
|
+
</CapLabel>
|
|
533
|
+
</div>
|
|
534
|
+
)}
|
|
535
|
+
<span
|
|
536
|
+
className={`${url ? 'whatsapp-message-with-media' : 'whatsapp-message-without-media'}`}
|
|
537
|
+
>
|
|
538
|
+
<CapLabel type="label9" className="whatsapp-carousel-body">
|
|
539
|
+
{c?.title || ''}
|
|
540
|
+
{c?.description ? `\n${c?.description}` : ''}
|
|
541
|
+
</CapLabel>
|
|
542
|
+
</span>
|
|
543
|
+
{Array.isArray(c?.suggestions) && c.suggestions.length > 0 && (
|
|
544
|
+
<>
|
|
545
|
+
{renderRcsSuggestionsPreview(c.suggestions)}
|
|
546
|
+
</>
|
|
547
|
+
)}
|
|
548
|
+
</div>
|
|
549
|
+
</div>
|
|
550
|
+
);
|
|
551
|
+
})}
|
|
552
|
+
</div>
|
|
553
|
+
);
|
|
554
|
+
};
|
|
555
|
+
|
|
556
|
+
if (isCarousel) {
|
|
557
|
+
return renderCarouselListingPreview();
|
|
558
|
+
}
|
|
559
|
+
|
|
483
560
|
return (
|
|
484
561
|
<div className="cap-rcs-creatives">
|
|
485
562
|
{mediaPreview && (
|
|
@@ -504,4 +581,3 @@ export const getRCSContent = (template) => {
|
|
|
504
581
|
</div>
|
|
505
582
|
);
|
|
506
583
|
};
|
|
507
|
-
|
|
@@ -1091,6 +1091,7 @@ export class Edit extends React.Component { // eslint-disable-line react/prefer-
|
|
|
1091
1091
|
onPreviewContentClicked={this.props.onPreviewContentClicked}
|
|
1092
1092
|
onTestContentClicked={this.props.onTestContentClicked}
|
|
1093
1093
|
eventContextTags={this.props?.eventContextTags}
|
|
1094
|
+
waitEventContextTags={this.props?.waitEventContextTags}
|
|
1094
1095
|
messageDetails={this.props?.messageDetails}
|
|
1095
1096
|
/>
|
|
1096
1097
|
</CapColumn>
|
|
@@ -1131,6 +1132,7 @@ Edit.propTypes = {
|
|
|
1131
1132
|
injectedTags: PropTypes.object,
|
|
1132
1133
|
selectedOfferDetails: PropTypes.array,
|
|
1133
1134
|
eventContextTags: PropTypes.array,
|
|
1135
|
+
waitEventContextTags: PropTypes.object,
|
|
1134
1136
|
messageDetails: PropTypes.object,
|
|
1135
1137
|
showTestAndPreviewSlidebox: PropTypes.bool,
|
|
1136
1138
|
handleTestAndPreview: PropTypes.func,
|
|
@@ -32,6 +32,7 @@ const SmsWrapper = (props) => {
|
|
|
32
32
|
smsRegister,
|
|
33
33
|
onShowTemplates,
|
|
34
34
|
eventContextTags,
|
|
35
|
+
waitEventContextTags,
|
|
35
36
|
showLiquidErrorInFooter,
|
|
36
37
|
getLiquidTags,
|
|
37
38
|
showTestAndPreviewSlidebox,
|
|
@@ -73,6 +74,7 @@ const SmsWrapper = (props) => {
|
|
|
73
74
|
onPreviewContentClicked,
|
|
74
75
|
onTestContentClicked,
|
|
75
76
|
eventContextTags,
|
|
77
|
+
waitEventContextTags,
|
|
76
78
|
showLiquidErrorInFooter,
|
|
77
79
|
getLiquidTags,
|
|
78
80
|
showTestAndPreviewSlidebox,
|
|
@@ -22,7 +22,7 @@ import messages, { scope } from './messages';
|
|
|
22
22
|
// import styled from styled-components;
|
|
23
23
|
import CapTagList from '../../v2Components/CapTagList';
|
|
24
24
|
import './_tagList.scss';
|
|
25
|
-
import { selectCurrentOrgDetails, makeSelectFetchingSchemaError } from '../Cap/selectors';
|
|
25
|
+
import { selectCurrentOrgDetails, makeSelectFetchingSchemaError, makeSelectFetchingSchema } from '../Cap/selectors';
|
|
26
26
|
import {
|
|
27
27
|
handleInjectedData, hasGiftVoucherFeature, hasPromoFeature, hasBadgesFeature, transformBadgeTags,
|
|
28
28
|
} from '../../utils/common';
|
|
@@ -35,12 +35,14 @@ const {TreeNode} = Tree;
|
|
|
35
35
|
export class TagList extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
|
36
36
|
constructor(props) {
|
|
37
37
|
super(props);
|
|
38
|
+
const { tags, injectedTags } = props;
|
|
39
|
+
const hasInitialData = (tags && tags.length > 0) || !_.isEmpty(injectedTags);
|
|
38
40
|
this.state = {
|
|
39
41
|
loading: false,
|
|
40
42
|
tags: [],
|
|
41
43
|
tagsError: false,
|
|
42
44
|
currentContext: null, // Track current context to detect changes
|
|
43
|
-
hasTriggeredInitialApiCall:
|
|
45
|
+
hasTriggeredInitialApiCall: hasInitialData, // Seed from initial props to avoid duplicate fetch on popover open
|
|
44
46
|
};
|
|
45
47
|
this.renderTags = this.renderTags.bind(this);
|
|
46
48
|
this.populateTags = this.populateTags.bind(this);
|
|
@@ -52,14 +54,8 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
52
54
|
|
|
53
55
|
componentDidMount() {
|
|
54
56
|
this.generateTags(this.props);
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
const hasNoTags = (!tags || tags.length === 0) && _.isEmpty(injectedTags);
|
|
58
|
-
if (hasNoTags && onContextChange) {
|
|
59
|
-
// Trigger API call with default 'Outbound' context to match CapTagList default
|
|
60
|
-
// This ensures tags are loaded when component mounts
|
|
61
|
-
this.getTagsforContext('Outbound');
|
|
62
|
-
}
|
|
57
|
+
// Initial schema fetch is the parent's responsibility (useTagManagement hook handles it)
|
|
58
|
+
// This avoids duplicate requests when both parent and child try to fetch on mount
|
|
63
59
|
}
|
|
64
60
|
|
|
65
61
|
componentWillReceiveProps(nextProps) {
|
|
@@ -85,9 +81,10 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
85
81
|
if (!_.isEqual(nextTags, currentTags)) {
|
|
86
82
|
this.setState({loading: false});
|
|
87
83
|
this.clearLoadingTimeout();
|
|
88
|
-
//
|
|
84
|
+
// Tags received (from prefetch or button-click API call) — mark as triggered
|
|
85
|
+
// so handlePopoverVisibilityChange won't fire a duplicate call on popover open
|
|
89
86
|
if (nextTags && nextTags.length > 0) {
|
|
90
|
-
this.setState({ hasTriggeredInitialApiCall:
|
|
87
|
+
this.setState({ hasTriggeredInitialApiCall: true });
|
|
91
88
|
}
|
|
92
89
|
}
|
|
93
90
|
if (fetchingSchemaError) {
|
|
@@ -97,10 +94,28 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
97
94
|
}
|
|
98
95
|
|
|
99
96
|
componentDidUpdate(prevProps) {
|
|
100
|
-
const {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
97
|
+
const {
|
|
98
|
+
tags,
|
|
99
|
+
injectedTags,
|
|
100
|
+
selectedOfferDetails,
|
|
101
|
+
eventContextTags,
|
|
102
|
+
waitEventContextTags,
|
|
103
|
+
} = this.props;
|
|
104
|
+
const {
|
|
105
|
+
tags: prevTags,
|
|
106
|
+
injectedTags: prevInjectedTags,
|
|
107
|
+
selectedOfferDetails: prevSelectedOfferDetails,
|
|
108
|
+
eventContextTags: prevEventContextTags,
|
|
109
|
+
waitEventContextTags: prevWaitEventContextTags,
|
|
110
|
+
} = prevProps;
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
tags !== prevTags
|
|
114
|
+
|| injectedTags !== prevInjectedTags
|
|
115
|
+
|| selectedOfferDetails !== prevSelectedOfferDetails
|
|
116
|
+
|| !_.isEqual(eventContextTags, prevEventContextTags)
|
|
117
|
+
|| !_.isEqual(waitEventContextTags, prevWaitEventContextTags)
|
|
118
|
+
) {
|
|
104
119
|
this.generateTags(this.props);
|
|
105
120
|
}
|
|
106
121
|
}
|
|
@@ -155,9 +170,7 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
155
170
|
if ((hasNoTags || hasNoStateTags || hasNotTriggeredApiCall)) {
|
|
156
171
|
// Mark that we've triggered the API call
|
|
157
172
|
this.setState({ hasTriggeredInitialApiCall: true });
|
|
158
|
-
|
|
159
|
-
// This will call onContextChange which triggers handleOnTagsContextChange in InApp
|
|
160
|
-
this.getTagsforContext('Outbound');
|
|
173
|
+
this.getTagsforContext('Outbound'); // Default to Outbound context
|
|
161
174
|
}
|
|
162
175
|
}
|
|
163
176
|
};
|
|
@@ -167,7 +180,7 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
167
180
|
let injectedTags = {};
|
|
168
181
|
const eventContextTagsObj = {};
|
|
169
182
|
|
|
170
|
-
const {selectedOfferDetails, eventContextTags } = props;
|
|
183
|
+
const {selectedOfferDetails, eventContextTags, waitEventContextTags } = props;
|
|
171
184
|
if (props.injectedTags && !_.isEmpty(props.injectedTags)) {
|
|
172
185
|
const formattedInjectedTags = handleInjectedData(
|
|
173
186
|
props.injectedTags,
|
|
@@ -219,6 +232,43 @@ export class TagList extends React.Component { // eslint-disable-line react/pref
|
|
|
219
232
|
};
|
|
220
233
|
});
|
|
221
234
|
}
|
|
235
|
+
// Wait event context tags should be displayed in the Add Labels when node is next to Event based wait node.
|
|
236
|
+
if (waitEventContextTags && Object.keys(waitEventContextTags)?.length) {
|
|
237
|
+
|
|
238
|
+
Object.keys(waitEventContextTags).forEach((blockId) => {
|
|
239
|
+
const WAIT_EVENT_HEADER_MSG_LABEL = `${waitEventContextTags[blockId].eventName} (${waitEventContextTags[blockId].blockName})`;
|
|
240
|
+
eventContextTagsObj[blockId] = {
|
|
241
|
+
"name": WAIT_EVENT_HEADER_MSG_LABEL,
|
|
242
|
+
"desc": WAIT_EVENT_HEADER_MSG_LABEL,
|
|
243
|
+
"resolved": true,
|
|
244
|
+
'tag-header': true,
|
|
245
|
+
"subtags": {},
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
waitEventContextTags?.[blockId]?.tags?.forEach((tag) => {
|
|
249
|
+
const {
|
|
250
|
+
tagName, label, profileId, profileName, blockName, eventName
|
|
251
|
+
} = tag || {};
|
|
252
|
+
if (!profileId || !tagName || !label || !profileName) return;
|
|
253
|
+
// Initializing the tags profile if it doesn't exist
|
|
254
|
+
if (!eventContextTagsObj?.[blockId]?.subtags?.[profileId]) {
|
|
255
|
+
eventContextTagsObj[blockId].subtags[profileId] = {
|
|
256
|
+
"name": profileName,
|
|
257
|
+
"desc": profileName,
|
|
258
|
+
"resolved": true,
|
|
259
|
+
'tag-header': true,
|
|
260
|
+
"subtags": {},
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
// Adding the current tag to the profile group
|
|
264
|
+
eventContextTagsObj[blockId].subtags[profileId].subtags[tagName] = {
|
|
265
|
+
name: label,
|
|
266
|
+
desc: label,
|
|
267
|
+
resolved: true,
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
222
272
|
this.setState({tags: _.merge( {}, tags, injectedTags, eventContextTagsObj )});
|
|
223
273
|
}
|
|
224
274
|
|
|
@@ -440,6 +490,7 @@ TagList.defaultProps = {
|
|
|
440
490
|
isNewVersionFlow: false,
|
|
441
491
|
userLocale: 'en',
|
|
442
492
|
eventContextTags: [],
|
|
493
|
+
waitEventContextTags: {},
|
|
443
494
|
};
|
|
444
495
|
|
|
445
496
|
TagList.propTypes = {
|
|
@@ -460,6 +511,7 @@ TagList.propTypes = {
|
|
|
460
511
|
disabled: PropTypes.bool,
|
|
461
512
|
fetchingSchemaError: PropTypes.bool,
|
|
462
513
|
eventContextTags: PropTypes.array,
|
|
514
|
+
waitEventContextTags: PropTypes.object,
|
|
463
515
|
popoverPlacement: PropTypes.string,
|
|
464
516
|
// message to show when Add Label button is disabled (e.g. personalization restriction)
|
|
465
517
|
disableTooltipMsg: PropTypes.string,
|
|
@@ -477,6 +529,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
477
529
|
TagList: makeSelectTagList(),
|
|
478
530
|
currentOrgDetails: selectCurrentOrgDetails(),
|
|
479
531
|
fetchingSchemaError: makeSelectFetchingSchemaError(),
|
|
532
|
+
fetchingSchema: makeSelectFetchingSchema(),
|
|
480
533
|
});
|
|
481
534
|
|
|
482
535
|
function mapDispatchToProps(dispatch) {
|
|
@@ -19,4 +19,8 @@ export default defineMessages({
|
|
|
19
19
|
id: `${scope}.personalizationNotSupportedAnonymous`,
|
|
20
20
|
defaultMessage: 'Personalization tags are not supported for anonymous customers',
|
|
21
21
|
},
|
|
22
|
+
waitEvent: {
|
|
23
|
+
id: `${scope}.waitEvent`,
|
|
24
|
+
defaultMessage: 'Wait Event',
|
|
25
|
+
},
|
|
22
26
|
});
|