@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.
Files changed (75) hide show
  1. package/package.json +1 -1
  2. package/utils/tests/tagValidations.test.js +20 -0
  3. package/v2Components/CapActionButton/constants.js +7 -0
  4. package/v2Components/CapActionButton/index.js +167 -109
  5. package/v2Components/CapActionButton/index.scss +157 -6
  6. package/v2Components/CapActionButton/messages.js +19 -3
  7. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  8. package/v2Components/CapTagList/index.js +28 -23
  9. package/v2Components/CapTagList/style.scss +29 -0
  10. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  11. package/v2Components/CapTagListWithInput/index.js +4 -0
  12. package/v2Components/CapWhatsappCTA/index.js +2 -0
  13. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  16. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
  17. package/v2Components/CommonTestAndPreview/messages.js +8 -0
  18. package/v2Components/CommonTestAndPreview/reducer.js +3 -1
  19. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  20. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  22. package/v2Components/FormBuilder/index.js +1 -0
  23. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  24. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  25. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  26. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  27. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  28. package/v2Components/TemplatePreview/constants.js +2 -0
  29. package/v2Components/TemplatePreview/index.js +143 -28
  30. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  31. package/v2Components/mockdata.js +1 -0
  32. package/v2Containers/BeeEditor/index.js +19 -1
  33. package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
  34. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
  35. package/v2Containers/Email/index.js +78 -39
  36. package/v2Containers/Email/reducer.js +2 -2
  37. package/v2Containers/Email/sagas.js +3 -1
  38. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
  39. package/v2Containers/Email/tests/sagas.test.js +230 -0
  40. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
  41. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  42. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  43. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  44. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
  45. package/v2Containers/EmailWrapper/index.js +4 -0
  46. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  47. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  48. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
  49. package/v2Containers/MobilePush/Create/index.js +2 -0
  50. package/v2Containers/MobilePush/Edit/index.js +2 -0
  51. package/v2Containers/MobilepushWrapper/index.js +3 -1
  52. package/v2Containers/Rcs/constants.js +79 -5
  53. package/v2Containers/Rcs/index.js +1374 -73
  54. package/v2Containers/Rcs/index.js.rej +1336 -0
  55. package/v2Containers/Rcs/index.scss +191 -0
  56. package/v2Containers/Rcs/index.scss.rej +74 -0
  57. package/v2Containers/Rcs/messages.js +26 -1
  58. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69173 -118166
  59. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  60. package/v2Containers/Rcs/tests/index.test.js +132 -94
  61. package/v2Containers/Rcs/tests/utils.test.js +220 -38
  62. package/v2Containers/Rcs/utils.js +77 -1
  63. package/v2Containers/Sms/Edit/index.js +2 -0
  64. package/v2Containers/SmsWrapper/index.js +2 -0
  65. package/v2Containers/TagList/index.js +73 -20
  66. package/v2Containers/TagList/messages.js +4 -0
  67. package/v2Containers/TagList/tests/TagList.test.js +124 -20
  68. package/v2Containers/TagList/tests/mockdata.js +17 -0
  69. package/v2Containers/Templates/_templates.scss +99 -0
  70. package/v2Containers/Templates/index.js +29 -14
  71. package/v2Containers/Viber/index.js +3 -0
  72. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  73. package/v2Containers/WebPush/Create/index.js +10 -2
  74. package/v2Containers/Whatsapp/index.js +5 -0
  75. package/v2Containers/Zalo/index.js +2 -0
@@ -41,38 +41,26 @@ describe('RCS utils - renderRcsSuggestionsPreview', () => {
41
41
  },
42
42
  };
43
43
 
44
- it('renders divider and QUICK_REPLY suggestion with small-link icon', () => {
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: RCS_BUTTON_TYPES.QUICK_REPLY, text: 'Reply' },
47
+ { type: buttonType, text },
48
48
  ];
49
49
  render(getRCSContent(template));
50
- expect(document.querySelectorAll('.whatsapp-divider').length).toBeGreaterThan(0);
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('renders PHONE_NUMBER suggestion with call icon', () => {
68
- const template = JSON.parse(JSON.stringify(templateBase));
69
- template.versions.base.content[RCS].rcsContent.cardContent[0].suggestions = [
70
- { type: RCS_BUTTON_TYPES.PHONE_NUMBER, text: 'Call' },
71
- ];
72
- render(getRCSContent(template));
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('Call');
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
- const template = mockData.editData1.templateDetails;
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
- const template = mockData.editData3.templateDetails;
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
- const tree = renderer.create(getRCSContent(template)).toJSON();
120
- expect(tree).toMatchSnapshot();
108
+ expectGetRCSContentSnapshot(template);
121
109
  });
122
110
 
123
111
  it('renders RCS content with missing cardContent', () => {
124
- const template = { versions: { base: { content: { RCS: { rcsContent: {} } } } } };
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
- const tree = renderer.create(getRCSContent({})).toJSON();
131
- expect(tree).toMatchSnapshot();
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: false, // Track if we've triggered API call when popover opens
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
- // Trigger initial API call if tags are empty (similar to Email/SMS behavior)
56
- const { tags, injectedTags, onContextChange } = this.props;
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
- // Reset the flag when tags are received, so we can trigger API call again if needed
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: false });
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 { tags, injectedTags, selectedOfferDetails } = this.props;
101
- const { tags: prevTags, injectedTags: prevInjectedTags, selectedOfferDetails: prevSelectedOfferDetails } = prevProps;
102
-
103
- if (tags !== prevTags || injectedTags !== prevInjectedTags || selectedOfferDetails !== prevSelectedOfferDetails) {
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
- // Trigger API call with default 'Outbound' context to match CapTagList default
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
  });