@capillarytech/creatives-library 8.0.316-alpha.4 → 8.0.317-alpha.0

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 (91) hide show
  1. package/constants/unified.js +1 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/tests/tagValidations.test.js +34 -0
  7. package/v2Components/CapTagList/index.js +15 -22
  8. package/v2Components/CapTagList/style.scss +48 -0
  9. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  10. package/v2Components/CapTagListWithInput/index.js +4 -0
  11. package/v2Components/CapWhatsappCTA/index.js +2 -0
  12. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +180 -0
  13. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +96 -0
  14. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +99 -0
  15. package/v2Components/CommonTestAndPreview/tests/index.test.js +113 -3
  16. package/v2Components/FormBuilder/index.js +7 -0
  17. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  18. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  19. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  20. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  21. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +95 -0
  22. package/v2Containers/BeeEditor/index.js +3 -0
  23. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  24. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  25. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  26. package/v2Containers/CommunicationFlow/constants.js +200 -0
  27. package/v2Containers/CommunicationFlow/index.js +102 -0
  28. package/v2Containers/CommunicationFlow/messages.js +346 -0
  29. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  30. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  31. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  32. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  33. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  34. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  35. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  36. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  37. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  38. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  39. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  40. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  41. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  42. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  43. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  44. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  45. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  46. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  47. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  48. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  49. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  50. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  51. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  52. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  53. package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
  54. package/v2Containers/CreativesContainer/constants.js +3 -0
  55. package/v2Containers/CreativesContainer/index.js +3 -0
  56. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  57. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  58. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  59. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  60. package/v2Containers/Email/index.js +1 -0
  61. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +7 -1
  62. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  63. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  64. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  65. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -0
  66. package/v2Containers/EmailWrapper/index.js +4 -0
  67. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  68. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  69. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +19 -0
  70. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +3 -0
  71. package/v2Containers/InAppWrapper/index.js +3 -0
  72. package/v2Containers/MobilePush/Create/index.js +2 -0
  73. package/v2Containers/MobilePush/Edit/index.js +2 -0
  74. package/v2Containers/MobilepushWrapper/index.js +3 -1
  75. package/v2Containers/Rcs/index.js +1 -0
  76. package/v2Containers/Sms/Create/index.js +2 -0
  77. package/v2Containers/Sms/Edit/index.js +2 -0
  78. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  79. package/v2Containers/SmsTrai/Edit/index.js +2 -0
  80. package/v2Containers/SmsWrapper/index.js +2 -0
  81. package/v2Containers/TagList/index.js +41 -2
  82. package/v2Containers/TagList/messages.js +4 -0
  83. package/v2Containers/TagList/tests/TagList.test.js +122 -20
  84. package/v2Containers/TagList/tests/mockdata.js +17 -0
  85. package/v2Containers/Templates/tests/sagas.test.js +83 -0
  86. package/v2Containers/Viber/index.js +5 -0
  87. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  88. package/v2Containers/WebPush/Create/index.js +9 -1
  89. package/v2Containers/Whatsapp/index.js +5 -0
  90. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +20 -0
  91. package/v2Containers/Zalo/index.js +2 -0
@@ -60,6 +60,7 @@ export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
60
60
  export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
61
61
  export const ENABLE_NEW_LEFT_NAVIGATION = 'ENABLE_NEW_LEFT_NAVIGATION';
62
62
  export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
63
+ export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
63
64
  export const EMBEDDED = 'embedded';
64
65
  // --- Tag/Validation Constants ---
65
66
  export const CARD_RELATED_TAGS = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.316-alpha.4",
4
+ "version": "8.0.317-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -334,6 +334,12 @@ export const getTemplateDetails = async ({id, channel}) => {
334
334
  return { ...compressedTemplatesData, response: decompressData};
335
335
  };
336
336
 
337
+ export const getDomainProperties = (channels, orgUnitId) => {
338
+ const queryString = channels?.map((channel) => `channel=${channel?.toUpperCase()}`)?.join('&');
339
+ const url = `${CAMPAIGNS_API_ORG_ENDPOINT}/meta/domainProperties?${queryString}`;
340
+ return request(url, getAPICallObject('GET', null, false, true, orgUnitId));
341
+ };
342
+
337
343
  export const getMediaDetails = async ({ id }) => {
338
344
  const url = `${API_ENDPOINT}/media/${id}`;
339
345
  return request(url, getAPICallObject('GET'));
@@ -1,5 +1,6 @@
1
1
  import {
2
2
  getSenderDetails,
3
+ getDomainProperties,
3
4
  uploadFile,
4
5
  getCdnTransformationConfig,
5
6
  createWhatsappTemplate,
@@ -52,6 +53,12 @@ describe('getSenderDetails -- Test with valid responses', () => {
52
53
  expect(getSenderDetails('WHATSAPP', -1)).toEqual(Promise.resolve()));
53
54
  });
54
55
 
56
+ describe('getDomainProperties -- Test with valid responses', () => {
57
+ it('Should return correct response', () => {
58
+ expect(getDomainProperties(['SMS', 'EMAIL'], null)).toEqual(Promise.resolve());
59
+ });
60
+ });
61
+
55
62
  describe('getCdnTransformationConfigs -- Test with valid responses', () => {
56
63
  it('Should return correct response', () =>
57
64
  expect(getCdnTransformationConfig()).toEqual(Promise.resolve()));
package/utils/common.js CHANGED
@@ -25,7 +25,8 @@ import {
25
25
  ENABLE_WEBPUSH,
26
26
  SUPPORT_CK_EDITOR,
27
27
  ENABLE_NEW_MPUSH,
28
- ENABLE_NEW_EDITOR_FLOW_INAPP
28
+ ENABLE_NEW_EDITOR_FLOW_INAPP,
29
+ SUPPORT_ENGAGEMENT_MODULE,
29
30
  } from '../constants/unified';
30
31
  import { apiMessageFormatHandler } from './commonUtils';
31
32
 
@@ -96,6 +97,10 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
96
97
  null,
97
98
  SUPPORT_CK_EDITOR,
98
99
  );
100
+ export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
101
+ null,
102
+ SUPPORT_ENGAGEMENT_MODULE,
103
+ );
99
104
 
100
105
  export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
101
106
  null,
@@ -242,6 +242,7 @@ describe("validateTags", () => {
242
242
  tagsParam,
243
243
  location,
244
244
  tagModule,
245
+ waitEventContextTags: {},
245
246
  });
246
247
 
247
248
  expect(result.valid).toEqual(true);
@@ -273,6 +274,7 @@ describe("validateTags", () => {
273
274
  tagsParam: tagsParamLocal,
274
275
  location,
275
276
  tagModule,
277
+ waitEventContextTags: {},
276
278
  });
277
279
 
278
280
  expect(result.valid).toEqual(true);
@@ -310,6 +312,7 @@ describe("validateTags", () => {
310
312
  tagsParam: tagsParamLocal,
311
313
  location,
312
314
  tagModule,
315
+ waitEventContextTags: {},
313
316
  });
314
317
 
315
318
  expect(result.valid).toEqual(false);
@@ -335,6 +338,7 @@ describe("validateTags", () => {
335
338
  tagsParam: tagsParamUnsubscribe,
336
339
  location,
337
340
  tagModule,
341
+ waitEventContextTags: {},
338
342
  });
339
343
  expect(resultMissing.missingTags).toContain("unsubscribe");
340
344
  expect(resultMissing.valid).toBe(false);
@@ -345,6 +349,7 @@ describe("validateTags", () => {
345
349
  tagsParam: tagsParamUnsubscribe,
346
350
  location,
347
351
  tagModule,
352
+ waitEventContextTags: {},
348
353
  });
349
354
  expect(resultSkipped.missingTags).not.toContain("unsubscribe");
350
355
  expect(resultSkipped.valid).toBe(true);
@@ -360,6 +365,35 @@ describe("validateTags", () => {
360
365
  expect(resultWhitespace.valid).toBe(true);
361
366
  expect(resultWhitespace.unsupportedTags ?? []).toEqual([]);
362
367
  });
368
+
369
+ it('should treat tags from waitEventContextTags as supported', () => {
370
+ const content = 'Hello {{waitEvent.orderId}}';
371
+ const tagsParam = [];
372
+ const injectedTagsParams = [];
373
+ const location = { query: { module: 'DEFAULT' } };
374
+ const tagModule = null;
375
+ const waitEventContextTags = {
376
+ block1: {
377
+ eventName: 'Order Placed',
378
+ blockName: 'Wait Block',
379
+ tags: [{ tagName: 'waitEvent.orderId', label: 'Order ID' }],
380
+ },
381
+ };
382
+
383
+ const result = validateTags({
384
+ content,
385
+ tagsParam,
386
+ injectedTagsParams,
387
+ location,
388
+ tagModule,
389
+ eventContextTags: [],
390
+ waitEventContextTags,
391
+ });
392
+
393
+ expect(result.valid).toEqual(true);
394
+ expect(result.missingTags).toEqual([]);
395
+ expect(result.isBraceError).toEqual(false);
396
+ });
363
397
  });
364
398
 
365
399
  describe('validateTags wrapper (v2 consumers)', () => {
@@ -211,6 +211,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
211
211
  } else if (info && info.selectedNodes && info.selectedNodes.length > 0 && !info.selectedNodes[0].props.isLeaf) {
212
212
  this.handleOnExpand(selectedKeys[0]);
213
213
  }
214
+ this.setState({expandedKeys: []})
214
215
  }
215
216
  };
216
217
 
@@ -233,6 +234,13 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
233
234
  }
234
235
  };
235
236
 
237
+ /** Single-line ellipsis within popover width; full label on hover via CapTooltip. */
238
+ wrapTreeTitle = (displayNode, text) => (
239
+ <CapTooltip title={displayNode}>
240
+ <CapLabel.CapLabelInline type="label15" className="cap-tag-list-tree-title-wrap">{text || displayNode}</CapLabel.CapLabelInline>
241
+ </CapTooltip>
242
+ );
243
+
236
244
  renderDynamicTagFlow = () => {
237
245
  this.setState({showModal: true, visible: false});
238
246
  };
@@ -280,7 +288,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
280
288
  if (temp?.length) {
281
289
  const tagValue = (
282
290
  <CapTreeNode
283
- title={disabled ? <CapTooltip title={loyaltyAttrDisableText}>{val?.name}</CapTooltip> : val?.name}
291
+ title={disabled ? this.wrapTreeTitle(loyaltyAttrDisableText, val?.name) : this.wrapTreeTitle(val?.name)}
284
292
  tag={val}
285
293
  key={val?.incentiveSeriesId ? `${key}(${val?.incentiveSeriesId})` : `${key}`}
286
294
  disabled={disabled}
@@ -303,17 +311,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
303
311
  <CapTreeNode
304
312
  title={
305
313
  childDisabled ? (
306
- <CapTooltip
307
- title={
308
- key === CUSTOMER_BARCODE_TAG
309
- ? customerBarcodeDisableText
310
- : loyaltyAttrDisableText
311
- }
312
- >
313
- {val?.desc || val?.name}
314
- </CapTooltip>
314
+ this.wrapTreeTitle(key === CUSTOMER_BARCODE_TAG ? customerBarcodeDisableText : loyaltyAttrDisableText, val?.desc || val?.name)
315
315
  ) : (
316
- val?.desc || val?.name
316
+ this.wrapTreeTitle(val?.desc || val?.name)
317
317
  )
318
318
  }
319
319
  tag={val}
@@ -339,17 +339,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
339
339
  <CapTreeNode
340
340
  title={
341
341
  childDisabled ? (
342
- <CapTooltip
343
- title={
344
- key === CUSTOMER_BARCODE_TAG
345
- ? customerBarcodeDisableText
346
- : loyaltyAttrDisableText
347
- }
348
- >
349
- {val?.desc || val?.name}
350
- </CapTooltip>
342
+ this.wrapTreeTitle(key === CUSTOMER_BARCODE_TAG ? customerBarcodeDisableText : loyaltyAttrDisableText, val?.desc || val?.name)
351
343
  ) : (
352
- val?.desc || val?.name
344
+ this.wrapTreeTitle(val?.desc || val?.name)
353
345
  )
354
346
  }
355
347
  tag={val}
@@ -407,7 +399,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
407
399
  },
408
400
  ];
409
401
  const contentSection = (
410
- <CapRow>
402
+ <CapRow className="cap-tag-list-popover-inner">
411
403
  <CapSpin tip={formatMessage(messages.gettingTags)} spinning={shouldShowLoading}>
412
404
  <Search
413
405
  style={{ marginBottom: 8, width: '250px'}}
@@ -475,6 +467,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
475
467
  visible={fetchingSchemaError ? false : visible}
476
468
  onVisibleChange={this.togglePopoverVisibility}
477
469
  content={contentSection}
470
+ overlayClassName="cap-tag-list-popover-overlay"
478
471
  trigger="click"
479
472
  placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
480
473
  overlayStyle={overlayStyle}
@@ -1,5 +1,53 @@
1
1
  @import "~@capillarytech/cap-ui-library/styles/_variables";
2
2
 
3
+ // Tag list popover: keep overlay width aligned with search (250px); tree rows ellipsis + tooltip for full text
4
+ .cap-tag-list-popover-overlay.ant-popover {
5
+ .ant-popover-inner-content {
6
+ max-width: 20rem;
7
+ box-sizing: border-box;
8
+ overflow: hidden;
9
+ }
10
+ }
11
+
12
+ .cap-tag-list-popover-inner {
13
+ max-width: 20rem;
14
+ min-width: 0;
15
+ box-sizing: border-box;
16
+
17
+ .ant-tree.cap-tree-v2.ant-tree-icon-hide {
18
+ width: 100%;
19
+ max-width: 100%;
20
+
21
+ ul {
22
+ max-width: 100%;
23
+ }
24
+
25
+ li {
26
+ overflow: hidden;
27
+ max-width: 100%;
28
+ }
29
+
30
+ li .ant-tree-node-content-wrapper {
31
+ width: calc(100% - 3.5rem); // leave room for switcher (~24px)
32
+ max-width: calc(100% - 3.5rem);
33
+ overflow: hidden;
34
+ vertical-align: top;
35
+ box-sizing: border-box;
36
+ }
37
+
38
+ .cap-tag-list-tree-title-wrap {
39
+ display: inline-block;
40
+ width: 100%;
41
+ overflow: hidden;
42
+ text-overflow: ellipsis;
43
+ white-space: nowrap;
44
+ max-width: 100%;
45
+ vertical-align: top;
46
+ margin-top: 0.5rem;
47
+ }
48
+ }
49
+ }
50
+
3
51
  @media (max-height: 25rem) {
4
52
  .ant-tree.cap-tree-v2.ant-tree-icon-hide {
5
53
  height: 8.5714rem;
@@ -0,0 +1,63 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import { IntlProvider } from 'react-intl';
5
+ import CapTagListWithInput from '../index';
6
+
7
+ const capturedTagListProps = { current: null };
8
+
9
+ jest.mock('../../../v2Containers/TagList', () => {
10
+ const React = require('react');
11
+ const Mock = (props) => {
12
+ capturedTagListProps.current = props;
13
+ return <div data-testid="mock-tag-list">TagList</div>;
14
+ };
15
+ return Mock;
16
+ });
17
+
18
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({ children }) => <div>{children}</div>);
19
+ jest.mock('@capillarytech/cap-ui-library/CapColumn', () => ({ children }) => <div>{children}</div>);
20
+ jest.mock('@capillarytech/cap-ui-library/CapHeading', () => () => null);
21
+ jest.mock('@capillarytech/cap-ui-library/CapInput', () => () => <input data-testid="cap-input" />);
22
+
23
+ const waitMap = {
24
+ b1: { eventName: 'Order Placed', blockName: 'Wait', tags: [] },
25
+ };
26
+
27
+ describe('CapTagListWithInput', () => {
28
+ beforeEach(() => {
29
+ capturedTagListProps.current = null;
30
+ });
31
+
32
+ it('forwards waitEventContextTags to TagList', () => {
33
+ render(
34
+ <IntlProvider locale="en" messages={{}}>
35
+ <CapTagListWithInput
36
+ inputId="test-url"
37
+ inputOnChange={jest.fn()}
38
+ waitEventContextTags={waitMap}
39
+ onTagSelect={jest.fn()}
40
+ onContextChange={jest.fn()}
41
+ />
42
+ </IntlProvider>
43
+ );
44
+
45
+ expect(screen.getByTestId('mock-tag-list')).toBeInTheDocument();
46
+ expect(capturedTagListProps.current.waitEventContextTags).toEqual(waitMap);
47
+ });
48
+
49
+ it('uses default empty object for waitEventContextTags when omitted', () => {
50
+ render(
51
+ <IntlProvider locale="en" messages={{}}>
52
+ <CapTagListWithInput
53
+ inputId="test-url"
54
+ inputOnChange={jest.fn()}
55
+ onTagSelect={jest.fn()}
56
+ onContextChange={jest.fn()}
57
+ />
58
+ </IntlProvider>
59
+ );
60
+
61
+ expect(capturedTagListProps.current.waitEventContextTags).toEqual({});
62
+ });
63
+ });
@@ -27,6 +27,7 @@ export const CapTagListWithInput = (props) => {
27
27
  userLocale = 'en',
28
28
  eventContextTags = [],
29
29
  restrictPersonalization = false,
30
+ waitEventContextTags = {},
30
31
  // CapInput props
31
32
  inputId,
32
33
  inputValue = '',
@@ -77,6 +78,7 @@ export const CapTagListWithInput = (props) => {
77
78
  userLocale={userLocale}
78
79
  selectedOfferDetails={selectedOfferDetails}
79
80
  eventContextTags={eventContextTags}
81
+ waitEventContextTags={waitEventContextTags}
80
82
  style={tagListStyle}
81
83
  popoverPlacement={popoverPlacement}
82
84
  restrictPersonalization={restrictPersonalization}
@@ -116,6 +118,7 @@ CapTagListWithInput.propTypes = {
116
118
  userLocale: PropTypes.string,
117
119
  eventContextTags: PropTypes.array,
118
120
  restrictPersonalization: PropTypes.bool,
121
+ waitEventContextTags: PropTypes.object,
119
122
 
120
123
  // CapInput props
121
124
  inputId: PropTypes.string.isRequired,
@@ -154,6 +157,7 @@ CapTagListWithInput.defaultProps = {
154
157
  userLocale: 'en',
155
158
  eventContextTags: [],
156
159
  restrictPersonalization: false,
160
+ waitEventContextTags: {},
157
161
  inputValue: '',
158
162
  inputSize: 'default',
159
163
  inputRequired: false,
@@ -52,6 +52,7 @@ export const CapWhatsappCTA = (props) => {
52
52
  injectedTags = {},
53
53
  selectedOfferDetails = [],
54
54
  eventContextTags = [],
55
+ waitEventContextTags = {},
55
56
  } = props;
56
57
  const { formatMessage } = intl;
57
58
  const invalidVarRegex = /{{(.*?)}}/g;
@@ -283,6 +284,7 @@ export const CapWhatsappCTA = (props) => {
283
284
  injectedTags={injectedTags}
284
285
  selectedOfferDetails={selectedOfferDetails}
285
286
  eventContextTags={eventContextTags}
287
+ waitEventContextTags={waitEventContextTags}
286
288
  />
287
289
  </CapColumn>
288
290
  )}
@@ -169,4 +169,184 @@ describe('CustomValuesEditor', () => {
169
169
  );
170
170
  expect(screen.getAllByTestId('tag-input').length).toBeGreaterThan(0);
171
171
  });
172
+
173
+ it('renders section title as FormattedMessage when title is an intl message object (not a string)', () => {
174
+ render(
175
+ <IntlProvider locale="en" messages={{ 'section.title.id': 'Intl Section Title' }}>
176
+ <CustomValuesEditor
177
+ {...baseProps}
178
+ sections={[
179
+ {
180
+ key: 'intl-sec',
181
+ title: { id: 'section.title.id', defaultMessage: 'Intl Section Title' },
182
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
183
+ optionalTags: [],
184
+ },
185
+ ]}
186
+ />
187
+ </IntlProvider>,
188
+ );
189
+ expect(screen.getByText('Intl Section Title')).toBeInTheDocument();
190
+ });
191
+
192
+ it('renders section title as plain text when title is a string', () => {
193
+ render(
194
+ <IntlProvider locale="en" messages={{}}>
195
+ <CustomValuesEditor
196
+ {...baseProps}
197
+ sections={[
198
+ {
199
+ key: 'str-sec',
200
+ title: 'Plain String Title',
201
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
202
+ optionalTags: [],
203
+ },
204
+ ]}
205
+ />
206
+ </IntlProvider>,
207
+ );
208
+ expect(screen.getByText('Plain String Title')).toBeInTheDocument();
209
+ });
210
+
211
+ it('renders nothing for section title when title is null (does not crash)', () => {
212
+ render(
213
+ <IntlProvider locale="en" messages={{}}>
214
+ <CustomValuesEditor
215
+ {...baseProps}
216
+ sections={[
217
+ {
218
+ key: 'no-title-sec',
219
+ title: null,
220
+ requiredTags: [{ fullPath: 'tag.a', name: 'a' }],
221
+ optionalTags: [],
222
+ },
223
+ ]}
224
+ />
225
+ </IntlProvider>,
226
+ );
227
+ const inputs = screen.getAllByTestId('tag-input');
228
+ expect(inputs.length).toBeGreaterThan(0);
229
+ });
230
+
231
+ it('uses tag.name as column label when fullPath is absent', () => {
232
+ render(
233
+ <IntlProvider locale="en" messages={{}}>
234
+ <CustomValuesEditor
235
+ {...baseProps}
236
+ sections={[
237
+ {
238
+ key: 'name-only',
239
+ title: 'Section',
240
+ requiredTags: [{ name: 'myTag' }],
241
+ optionalTags: [],
242
+ },
243
+ ]}
244
+ />
245
+ </IntlProvider>,
246
+ );
247
+ expect(screen.getByText('myTag')).toBeInTheDocument();
248
+ });
249
+
250
+ it('uses empty string as column label when both fullPath and name are absent', () => {
251
+ render(
252
+ <IntlProvider locale="en" messages={{}}>
253
+ <CustomValuesEditor
254
+ {...baseProps}
255
+ sections={[
256
+ {
257
+ key: 'no-label',
258
+ title: 'Section',
259
+ requiredTags: [{}],
260
+ optionalTags: [],
261
+ },
262
+ ]}
263
+ />
264
+ </IntlProvider>,
265
+ );
266
+ const inputs = screen.getAllByTestId('tag-input');
267
+ expect(inputs.length).toBeGreaterThan(0);
268
+ });
269
+
270
+ it('renders optional-only section (no requiredTags) correctly', () => {
271
+ render(
272
+ <IntlProvider locale="en" messages={{}}>
273
+ <CustomValuesEditor
274
+ {...baseProps}
275
+ sections={[
276
+ {
277
+ key: 'optional-only',
278
+ title: 'Optional Section',
279
+ requiredTags: [],
280
+ optionalTags: [{ fullPath: 'tag.opt', name: 'opt' }],
281
+ },
282
+ ]}
283
+ />
284
+ </IntlProvider>,
285
+ );
286
+ expect(screen.getByText('Optional Section')).toBeInTheDocument();
287
+ const inputs = screen.getAllByTestId('tag-input');
288
+ expect(inputs.length).toBeGreaterThan(0);
289
+ });
290
+
291
+ it('renders required-only section (no optionalTags) correctly', () => {
292
+ render(
293
+ <IntlProvider locale="en" messages={{}}>
294
+ <CustomValuesEditor
295
+ {...baseProps}
296
+ sections={[
297
+ {
298
+ key: 'required-only',
299
+ title: 'Required Section',
300
+ requiredTags: [{ fullPath: 'tag.req', name: 'req' }],
301
+ optionalTags: [],
302
+ },
303
+ ]}
304
+ />
305
+ </IntlProvider>,
306
+ );
307
+ expect(screen.getByText('Required Section')).toBeInTheDocument();
308
+ const inputs = screen.getAllByTestId('tag-input');
309
+ expect(inputs.length).toBeGreaterThan(0);
310
+ });
311
+
312
+ it('renders line numbers in JSON mode matching JSON line count', () => {
313
+ const multiValueCustomValues = { a: 'val1', b: 'val2', c: 'val3' };
314
+ render(
315
+ <IntlProvider locale="en" messages={{}}>
316
+ <CustomValuesEditor
317
+ {...baseProps}
318
+ showJSON
319
+ customValues={multiValueCustomValues}
320
+ />
321
+ </IntlProvider>,
322
+ );
323
+ const lineNumbers = document.querySelectorAll('.line-number');
324
+ const expectedLineCount = JSON.stringify(multiValueCustomValues, null, 2).split('\n').length;
325
+ expect(lineNumbers.length).toBe(expectedLineCount);
326
+ });
327
+
328
+ it('passes fullPath as key for required tag row when fullPath is present', () => {
329
+ const handleChange = jest.fn();
330
+ render(
331
+ <IntlProvider locale="en" messages={{}}>
332
+ <CustomValuesEditor
333
+ {...baseProps}
334
+ sections={[
335
+ {
336
+ key: 'sec',
337
+ title: 'S',
338
+ requiredTags: [{ fullPath: 'my.full.path', name: 'name' }],
339
+ optionalTags: [],
340
+ },
341
+ ]}
342
+ handleCustomValueChange={handleChange}
343
+ customValues={{ 'my.full.path': 'existing' }}
344
+ />
345
+ </IntlProvider>,
346
+ );
347
+ const input = screen.getByTestId('tag-input');
348
+ expect(input.value).toBe('existing');
349
+ fireEvent.change(input, { target: { value: 'updated' } });
350
+ expect(handleChange).toHaveBeenCalledWith('my.full.path', 'updated');
351
+ });
172
352
  });
@@ -477,4 +477,100 @@ describe('parseSenderDetailsResponse', () => {
477
477
  expect(parseSenderDetailsResponse('SMS', response)).toEqual({ domains: [] });
478
478
  });
479
479
  });
480
+
481
+ describe('unwrapEntity edge cases', () => {
482
+ it('should return empty domains when entity key is explicitly null', () => {
483
+ // unwrapEntity: response.entity === null → returns null → parseSenderDetailsResponse returns { domains: [] }
484
+ expect(parseSenderDetailsResponse('SMS', { entity: null })).toEqual({ domains: [] });
485
+ });
486
+ });
487
+
488
+ describe('null or empty channel', () => {
489
+ it('should return empty domains when channel is null', () => {
490
+ const response = { entity: { SMS: [{ id: 1, domainProperties: { domainName: 'D', id: 10, contactInfo: [] } }] } };
491
+ expect(parseSenderDetailsResponse(null, response)).toEqual({ domains: [] });
492
+ });
493
+
494
+ it('should return empty domains when channel is empty string', () => {
495
+ const response = { entity: { SMS: [{ id: 1, domainProperties: { domainName: 'D', id: 10, contactInfo: [] } }] } };
496
+ expect(parseSenderDetailsResponse('', response)).toEqual({ domains: [] });
497
+ });
498
+ });
499
+
500
+ describe('single non-array domain object', () => {
501
+ it('should wrap single domain with domainProperties in an array and return one domain', () => {
502
+ const response = {
503
+ entity: {
504
+ SMS: {
505
+ id: 5,
506
+ domainProperties: {
507
+ domainName: 'SingleDomain',
508
+ id: 99,
509
+ contactInfo: [{ type: 'gsm_sender_id', valid: true, value: 'GSMSINGLE' }],
510
+ },
511
+ },
512
+ },
513
+ };
514
+ const result = parseSenderDetailsResponse('SMS', response);
515
+ expect(result.domains).toHaveLength(1);
516
+ expect(result.domains[0].domainName).toBe('SingleDomain');
517
+ expect(result.domains[0].gsmSenders[0].value).toBe('GSMSINGLE');
518
+ });
519
+
520
+ it('should return empty domains for single non-array domain without domainProperties', () => {
521
+ const response = {
522
+ entity: { SMS: { id: 5, someProp: 'value' } },
523
+ };
524
+ expect(parseSenderDetailsResponse('SMS', response)).toEqual({ domains: [] });
525
+ });
526
+ });
527
+
528
+ describe('deduplication by domainId when domainName is null', () => {
529
+ it('should deduplicate two entries sharing the same domainId when domainName is null', () => {
530
+ const response = {
531
+ entity: {
532
+ SMS: [
533
+ {
534
+ id: 10,
535
+ priority: 1,
536
+ domainProperties: { domainName: null, id: 42, contactInfo: [] },
537
+ },
538
+ {
539
+ id: 11,
540
+ priority: 2,
541
+ domainProperties: { domainName: null, id: 42, contactInfo: [] },
542
+ },
543
+ ],
544
+ },
545
+ };
546
+ const result = parseSenderDetailsResponse('SMS', response);
547
+ expect(result.domains).toHaveLength(1);
548
+ expect(result.domains[0].domainId).toBe(42);
549
+ });
550
+ });
551
+
552
+ describe('typeMatches null-type filtering', () => {
553
+ it('should exclude contactInfo items with null type from gsmSenders', () => {
554
+ const response = {
555
+ entity: {
556
+ SMS: [
557
+ {
558
+ id: 1,
559
+ domainProperties: {
560
+ domainName: 'D',
561
+ id: 1,
562
+ contactInfo: [
563
+ { type: null, valid: true, value: 'EXCLUDED' },
564
+ { type: 'gsm_sender_id', valid: true, value: 'INCLUDED' },
565
+ ],
566
+ },
567
+ },
568
+ ],
569
+ },
570
+ };
571
+ const result = parseSenderDetailsResponse('SMS', response);
572
+ expect(result.domains[0].gsmSenders).toHaveLength(1);
573
+ expect(result.domains[0].gsmSenders[0].value).toBe('INCLUDED');
574
+ });
575
+ });
480
576
  });