@capillarytech/creatives-library 8.0.329 → 8.0.330-alpha.1

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 (89) hide show
  1. package/constants/unified.js +4 -0
  2. package/package.json +1 -1
  3. package/utils/commonUtils.js +19 -1
  4. package/utils/templateVarUtils.js +35 -6
  5. package/utils/tests/tagValidations.test.js +20 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/constants.js +7 -0
  8. package/v2Components/CapActionButton/index.js +167 -109
  9. package/v2Components/CapActionButton/index.scss +157 -6
  10. package/v2Components/CapActionButton/messages.js +19 -3
  11. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  12. package/v2Components/CapTagList/index.js +28 -23
  13. package/v2Components/CapTagList/style.scss +29 -0
  14. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +63 -0
  15. package/v2Components/CapTagListWithInput/index.js +4 -0
  16. package/v2Components/CapWhatsappCTA/index.js +2 -0
  17. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +1 -0
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  19. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  20. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +323 -77
  21. package/v2Components/CommonTestAndPreview/index.js +49 -57
  22. package/v2Components/CommonTestAndPreview/messages.js +8 -0
  23. package/v2Components/CommonTestAndPreview/reducer.js +3 -1
  24. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  25. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  26. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  27. package/v2Components/FormBuilder/index.js +1 -0
  28. package/v2Components/HtmlEditor/HTMLEditor.js +6 -1
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +1 -0
  30. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +927 -2
  31. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +3 -0
  32. package/v2Components/SmsFallback/smsFallbackUtils.js +14 -3
  33. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +16 -0
  34. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  35. package/v2Components/TemplatePreview/constants.js +2 -0
  36. package/v2Components/TemplatePreview/index.js +143 -28
  37. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  38. package/v2Components/TestAndPreviewSlidebox/index.js +5 -0
  39. package/v2Components/mockdata.js +1 -0
  40. package/v2Containers/BeeEditor/index.js +19 -1
  41. package/v2Containers/CreativesContainer/SlideBoxContent.js +28 -1
  42. package/v2Containers/CreativesContainer/index.js +9 -3
  43. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -0
  44. package/v2Containers/Email/index.js +78 -39
  45. package/v2Containers/Email/reducer.js +2 -2
  46. package/v2Containers/Email/sagas.js +3 -1
  47. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -2
  48. package/v2Containers/Email/tests/sagas.test.js +230 -0
  49. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +6 -1
  50. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +3 -0
  51. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +20 -2
  52. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +16 -1
  53. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +3 -1
  54. package/v2Containers/EmailWrapper/index.js +4 -0
  55. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +1 -0
  56. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +9 -0
  57. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +1 -0
  58. package/v2Containers/MobilePush/Create/index.js +2 -0
  59. package/v2Containers/MobilePush/Edit/index.js +2 -0
  60. package/v2Containers/MobilepushWrapper/index.js +3 -1
  61. package/v2Containers/Rcs/constants.js +85 -7
  62. package/v2Containers/Rcs/index.js +1592 -156
  63. package/v2Containers/Rcs/index.js.rej +1336 -0
  64. package/v2Containers/Rcs/index.scss +191 -0
  65. package/v2Containers/Rcs/index.scss.rej +74 -0
  66. package/v2Containers/Rcs/messages.js +28 -2
  67. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +20 -0
  68. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +69178 -117691
  69. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  70. package/v2Containers/Rcs/tests/index.test.js +132 -94
  71. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +67 -0
  72. package/v2Containers/Rcs/tests/utils.test.js +276 -38
  73. package/v2Containers/Rcs/utils.js +130 -7
  74. package/v2Containers/Sms/Edit/index.js +2 -0
  75. package/v2Containers/SmsTrai/Edit/index.js +27 -0
  76. package/v2Containers/SmsTrai/Edit/messages.js +5 -0
  77. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +6 -6
  78. package/v2Containers/SmsWrapper/index.js +2 -0
  79. package/v2Containers/TagList/index.js +73 -20
  80. package/v2Containers/TagList/messages.js +4 -0
  81. package/v2Containers/TagList/tests/TagList.test.js +124 -20
  82. package/v2Containers/TagList/tests/mockdata.js +17 -0
  83. package/v2Containers/Templates/_templates.scss +99 -0
  84. package/v2Containers/Templates/index.js +29 -14
  85. package/v2Containers/Viber/index.js +3 -0
  86. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +0 -2
  87. package/v2Containers/WebPush/Create/index.js +10 -2
  88. package/v2Containers/Whatsapp/index.js +5 -0
  89. package/v2Containers/Zalo/index.js +2 -0
@@ -1,14 +1,27 @@
1
1
  @import '~@capillarytech/cap-ui-library/styles/_variables';
2
2
 
3
- #rcs-cta-type{
4
- width: 90%;
3
+ // Horizontal RCS button type radios (Phone number | URL | Quick reply)
4
+ .cap-rcs-cta-type-radio.cap-radio-group-v2 {
5
+ display: flex;
6
+ flex-wrap: wrap;
7
+ align-items: center;
8
+ margin-top: 0.5rem;
9
+
10
+ .ant-radio-wrapper {
11
+ margin-right: $CAP_SPACE_24;
12
+ margin-bottom: 0;
13
+
14
+ &:last-child {
15
+ margin-right: 0;
16
+ }
17
+ }
5
18
  }
6
19
 
7
20
  .cap-rcs-saved-cta {
8
21
  position: relative;
9
22
  border: solid $CAP_SPACE_01 $CAP_G06;
10
23
  padding: 0.625rem;
11
- border-radius: $CAP_SPACE_04;
24
+ border-radius: 0.25rem; // 4px
12
25
  margin-bottom: $CAP_SPACE_12;
13
26
 
14
27
  div:not(:last-child) {
@@ -39,9 +52,139 @@
39
52
  }
40
53
  }
41
54
 
42
- .rcs-button-cta-create-container {
43
- border: solid 1px $CAP_G06;
44
- padding: 10px;
55
+ // Button text / URL: count via CapInput `suffix` (antd affix, right-aligned inside field).
56
+ // Phone: react-phone-input-2 overlay count + padding on .form-control
57
+ .rcs-cta-inner-char-count {
58
+ font-size: 0.75rem;
59
+ line-height: 1.25rem;
60
+ color: $FONT_COLOR_03;
61
+ white-space: nowrap;
62
+ }
63
+
64
+ .rcs-cta-input-with-inner-count {
65
+ position: relative;
66
+ width: 100%;
67
+
68
+ &--phone .rcs-cta-inner-char-count {
69
+ position: absolute;
70
+ right: 0.6875rem;
71
+ top: 50%;
72
+ transform: translateY(-50%);
73
+ z-index: 2;
74
+ pointer-events: none;
75
+ }
76
+ }
77
+
78
+ // RCS phone field: Figma — one light border, 4px radius, ~antd Input large height, full width, count inside right
79
+ .rcs-button-cta-create-container .rcs-cta-input-with-inner-count--phone {
80
+ .react-tel-input.rcs-cta-phone-input {
81
+ width: 100%;
82
+ }
83
+
84
+ .rcs-cta-phone-input .form-control {
85
+ width: 100% !important;
86
+ max-width: none;
87
+ height: 2.5rem;
88
+ min-height: 2.5rem;
89
+ padding-top: 0;
90
+ padding-bottom: 0;
91
+ padding-left: 3rem;
92
+ padding-right: 3.5rem;
93
+ margin: 0;
94
+ font-size: 0.875rem;
95
+ line-height: 2.5rem;
96
+ color: $CAP_G01;
97
+ background: $CAP_WHITE;
98
+ border: 1px solid $CAP_G06;
99
+ border-radius: 0.25rem;
100
+ box-shadow: none;
101
+
102
+ &::placeholder {
103
+ color: $CAP_G05;
104
+ }
105
+
106
+ &:hover {
107
+ border-color: $CAP_G11;
108
+ }
109
+
110
+ &:focus {
111
+ border-color: $CAP_G01;
112
+ box-shadow: none;
113
+ outline: none;
114
+ }
115
+ }
116
+
117
+ .rcs-cta-phone-input .flag-dropdown {
118
+ top: 1px;
119
+ bottom: 1px;
120
+ left: 1px;
121
+ padding: 0;
122
+ background: $CAP_WHITE;
123
+ border: none;
124
+ border-radius: 0.1875rem 0 0 0.1875rem; // inset from outer 4px radius
125
+ }
126
+
127
+ .rcs-cta-phone-input .flag-dropdown.open {
128
+ border-radius: 0.1875rem 0 0 0;
129
+ }
130
+
131
+ .rcs-cta-phone-input .selected-flag {
132
+ width: 2.75rem;
133
+ padding: 0 0 0 0.5rem;
134
+ background: transparent;
135
+ border-radius: 0.1875rem 0 0 0.1875rem;
136
+ }
137
+
138
+ .rcs-cta-phone-input .selected-flag:hover,
139
+ .rcs-cta-phone-input .selected-flag:focus {
140
+ background: $CAP_G09;
141
+ }
142
+
143
+ .rcs-cta-phone-input .selected-flag .arrow {
144
+ left: 1.375rem;
145
+ border-top-color: $CAP_G04;
146
+ }
147
+ }
148
+
149
+ // URL CTA: URL type (narrow) + URL field (~1:3) per Figma
150
+ .rcs-cta-url-fields-row {
151
+ .rcs-cta-url-type-col .ant-select {
152
+ width: 100%;
153
+ }
154
+ }
155
+
156
+ // RCS “create CTA” card: 0.5rem padding + gaps (Figma-tight; fixes excess space above first label)
157
+ .rcs-button-cta-create-container.cap-row-v2 {
158
+ display: flex;
159
+ flex-direction: column;
160
+ flex-wrap: nowrap;
161
+ width: 100%;
162
+ border: solid 1px $CAP_G06;
163
+ padding: 1.25rem;
164
+ border-radius: 0.25rem; // 4px — match .cap-rcs-saved-cta
165
+
166
+ // Stack “Button type” (radios) above “Button text”
167
+ .rcs-button-cta-create.cap-row-v2 {
168
+ display: flex;
169
+ flex-direction: column;
170
+ flex-wrap: nowrap;
171
+ margin: 0;
172
+ }
173
+
174
+ // Space after each field label (CapHeading margins are often zeroed by the library)
175
+ .cta-label {
176
+ margin-bottom: 0;
177
+ margin-top: 0.75rem;
178
+ }
179
+
180
+ .cta-label + * {
181
+ margin-top: 0.5rem;
182
+ }
183
+
184
+ // “Button type” heading has no .cta-label — keep same label→control gap as other fields
185
+ .rcs-button-cta-create > .ant-col:first-of-type .cap-rcs-cta-type-radio {
186
+ margin-top: 0.5rem;
187
+ }
45
188
  }
46
189
 
47
190
  .disabled {
@@ -55,4 +198,12 @@
55
198
 
56
199
  .button-disabled-tooltip-wrapper {
57
200
  display: inline-block;
201
+ margin-top: 1.25rem;
202
+ }
203
+
204
+ // Space between Save and Delete on RCS / carousel CTA rows
205
+ .rcs-cta-save-delete-btn {
206
+ .rcs-cta-delete-btn {
207
+ margin-left: 0.75rem;
208
+ }
58
209
  }
@@ -12,7 +12,7 @@ export default defineMessages({
12
12
  },
13
13
  ctaQr: {
14
14
  id: `${prefix}.ctaQr`,
15
- defaultMessage: 'Quick Reply',
15
+ defaultMessage: 'Quick reply',
16
16
  },
17
17
  ctaPhoneNo: {
18
18
  id: `${prefix}.ctaPhoneNo`,
@@ -47,9 +47,17 @@ export default defineMessages({
47
47
  id: `${prefix}.templateMessageLength`,
48
48
  defaultMessage: 'Characters count: {currentLength}/{maxLength}',
49
49
  },
50
+ ctaFieldCharCountInline: {
51
+ id: `${prefix}.ctaFieldCharCountInline`,
52
+ defaultMessage: '{current}/{max}',
53
+ },
50
54
  ctaType: {
51
55
  id: `${prefix}.ctaType`,
52
- defaultMessage: 'Type of action',
56
+ defaultMessage: 'Button type',
57
+ },
58
+ ctaUrlRadio: {
59
+ id: `${prefix}.ctaUrlRadio`,
60
+ defaultMessage: 'URL',
53
61
  },
54
62
  templateButtonTextPlaceholder: {
55
63
  id: `${prefix}.templateButtonTextPlaceholder`,
@@ -145,7 +153,15 @@ export default defineMessages({
145
153
  },
146
154
  ctaWebsiteType: {
147
155
  id: `${prefix}.ctaWebsiteType`,
148
- defaultMessage: 'URL Type',
156
+ defaultMessage: 'URL type',
157
+ },
158
+ ctaUrlField: {
159
+ id: `${prefix}.ctaUrlField`,
160
+ defaultMessage: 'URL',
161
+ },
162
+ ctaEnterUrlPlaceholder: {
163
+ id: `${prefix}.ctaEnterUrlPlaceholder`,
164
+ defaultMessage: 'Enter URL',
149
165
  },
150
166
  ctaWebsiteTypeStatic: {
151
167
  id: `${prefix}.ctaWebsiteTypeStatic`,
@@ -4,7 +4,7 @@ import '@testing-library/jest-dom';
4
4
  import { render, screen, fireEvent } from '../../../utils/test-utils';
5
5
  import { CapActionButton } from '../index';
6
6
  import { BTN_MAX_LENGTH, PHONE_NUMBER_MAX_LENGTH, URL_MAX_LENGTH } from '../constants';
7
- import { RCS_BUTTON_TYPES } from '../../../v2Containers/Rcs/constants';
7
+ import { RCS_BUTTON_TYPES, HOST_ICS } from '../../../v2Containers/Rcs/constants';
8
8
 
9
9
  const updateHandler = jest.fn();
10
10
  const deleteHandler = jest.fn();
@@ -12,6 +12,7 @@ const initializeComponent = (
12
12
  data,
13
13
  isEditFlow = false,
14
14
  maxButtons = 3,
15
+ host = '',
15
16
  ) => {
16
17
  // Normalize legacy test data shape to match component props
17
18
  const normalizeType = (legacyType) => {
@@ -37,6 +38,7 @@ const initializeComponent = (
37
38
  isEditFlow={isEditFlow}
38
39
  maxButtons={maxButtons}
39
40
  isFullMode={true}
41
+ host={host}
40
42
  />,
41
43
  );
42
44
  };
@@ -59,7 +61,7 @@ describe('CapActionButton', () => {
59
61
  isSaved: false,
60
62
  },
61
63
  ]);
62
- expect(screen.getByText('Type of action')).toBeInTheDocument();
64
+ expect(screen.getByText('Button type')).toBeInTheDocument();
63
65
  expect(screen.getByText('Button text')).toBeInTheDocument();
64
66
  expect(screen.getAllByText('Phone number')[1]).toBeInTheDocument();
65
67
  expect(screen.getByRole('button', { name: /save/i })).toBeDisabled();
@@ -122,7 +124,7 @@ describe('CapActionButton', () => {
122
124
  id: 1,
123
125
  },
124
126
  };
125
- const urlInput = await screen.getByPlaceholderText('Enter website URL');
127
+ const urlInput = await screen.getByPlaceholderText('Enter URL');
126
128
  fireEvent.change(urlInput, urlEvent);
127
129
  expect(updateHandler).toHaveBeenCalledWith(
128
130
  {
@@ -162,6 +164,27 @@ describe('CapActionButton', () => {
162
164
  );
163
165
  });
164
166
 
167
+ it('should show delete for quick reply index 0 for non-ICS hosts but hide it for ICS (stop button)', () => {
168
+ const button = {
169
+ index: 0,
170
+ type: RCS_BUTTON_TYPES.QUICK_REPLY,
171
+ text: 'Reply',
172
+ phoneNumber: '',
173
+ url: '',
174
+ postback: 'Reply',
175
+ isSaved: false,
176
+ };
177
+
178
+ // Non-ICS (Infobip/others): delete should be available
179
+ const { unmount } = initializeComponent([button], false, 3, 'rcsinfobipbulk');
180
+ expect(screen.getByRole('button', { name: /delete/i })).toBeInTheDocument();
181
+ unmount();
182
+
183
+ // ICS: index 0 is reserved; delete should be hidden
184
+ initializeComponent([button], false, 3, HOST_ICS);
185
+ expect(screen.queryByRole('button', { name: /delete/i })).not.toBeInTheDocument();
186
+ });
187
+
165
188
  it('should respect character length limits', async () => {
166
189
  const initialData = {
167
190
  index: 0,
@@ -198,7 +221,7 @@ describe('CapActionButton', () => {
198
221
  isSaved: false,
199
222
  };
200
223
  initializeComponent([initialData]);
201
- const urlInput = await screen.getByPlaceholderText('Enter website URL');
224
+ const urlInput = await screen.getByPlaceholderText('Enter URL');
202
225
  const longUrl = 'https://example.com/' + 'a'.repeat(URL_MAX_LENGTH + 1);
203
226
  fireEvent.change(urlInput, { target: { value: longUrl, id: 0 } });
204
227
  expect(urlInput.value.length).toBeLessThanOrEqual(URL_MAX_LENGTH);
@@ -245,7 +268,8 @@ describe('CapActionButton', () => {
245
268
  { index: 0, ctaType: RCS_BUTTON_TYPES.QUICK_REPLY, displayText: 'stop', phoneNumber: '', url: '', postback: 'stop', isSaved: true },
246
269
  { index: 1, ctaType: RCS_BUTTON_TYPES.QUICK_REPLY, displayText: 'Saved', phoneNumber: '', url: '', postback: 'Saved', isSaved: true },
247
270
  ];
248
- const { container } = initializeComponent(suggestions);
271
+ // Use ICS host so index 0 (STOP) stays non-deletable and we only get delete icon for index 1
272
+ const { container } = initializeComponent(suggestions, false, 3, HOST_ICS);
249
273
  const deleteIcons = container.querySelectorAll('.rcs-saved-cta-delete-icon');
250
274
  expect(deleteIcons.length).toBeGreaterThan(0);
251
275
  fireEvent.click(deleteIcons[0]);
@@ -303,7 +327,8 @@ describe('CapActionButton', () => {
303
327
  postback: 'stop',
304
328
  isSaved: true,
305
329
  };
306
- const { container } = initializeComponent([stopButton]);
330
+ // Only ICS treats index 0 as a mandatory STOP quick reply
331
+ const { container } = initializeComponent([stopButton], false, 3, HOST_ICS);
307
332
  expect(container.querySelector('.rcs-saved-cta-delete-icon')).not.toBeInTheDocument();
308
333
  });
309
334
 
@@ -424,7 +449,7 @@ describe('CapActionButton', () => {
424
449
  isSaved: false,
425
450
  };
426
451
  initializeComponent([button]);
427
- expect(screen.getByPlaceholderText(/enter website url/i)).toBeInTheDocument();
452
+ expect(screen.getByPlaceholderText(/enter url/i)).toBeInTheDocument();
428
453
  });
429
454
 
430
455
  it('should update both displayText and postback when button text changes (updateDisplayAndPostback)', () => {
@@ -504,7 +529,7 @@ describe('CapActionButton', () => {
504
529
  isSaved: false,
505
530
  };
506
531
  initializeComponent([initial]);
507
- const urlInput = screen.getByPlaceholderText('Enter website URL');
532
+ const urlInput = screen.getByPlaceholderText('Enter URL');
508
533
  fireEvent.change(urlInput, { target: { value: 'http://localhost:3030/creatives/ui/v2', id: 1 } });
509
534
  expect(screen.getByText(/url is not valid/i)).toBeInTheDocument();
510
535
  });
@@ -590,7 +615,7 @@ describe('CapActionButton', () => {
590
615
  isSaved: false,
591
616
  };
592
617
  initializeComponent([initial]);
593
- const urlInput = screen.getByPlaceholderText('Enter website URL');
618
+ const urlInput = screen.getByPlaceholderText('Enter URL');
594
619
  // Enter invalid URL
595
620
  fireEvent.change(urlInput, { target: { value: 'badurl', id: 0 } });
596
621
  expect(screen.getByText(/url is not valid/i)).toBeInTheDocument();
@@ -849,15 +874,14 @@ describe('CapActionButton function logic', () => {
849
874
  expect(renderCtaOptions(label, tooltipLabel, false)).toBe('Test');
850
875
  });
851
876
 
852
- it('renderLength returns correct CapHeading', () => {
853
- const CapHeading = ({ type, className, children }) => <div className={className}>{children}</div>;
854
- const formatMessage = (msg, values) => `${values.currentLength}/${values.maxLength}`;
855
- const renderLength = (len, max) => (
856
- <CapHeading type="label1" className="rcs-render-btn-length">
857
- {formatMessage({}, { currentLength: len, maxLength: max })}
858
- </CapHeading>
877
+ it('inner char count uses current/max format', () => {
878
+ const formatMessage = (msg, values) => `${values.current}/${values.max}`;
879
+ const renderInnerCharCount = (len, max) => (
880
+ <span className="rcs-cta-inner-char-count">
881
+ {formatMessage({}, { current: len, max })}
882
+ </span>
859
883
  );
860
- const result = renderLength(5, 25);
884
+ const result = renderInnerCharCount(5, 25);
861
885
  expect(result.props.children).toBe('5/25');
862
886
  });
863
887
  });
@@ -44,6 +44,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
44
44
  super(props);
45
45
  this.state = {
46
46
  tagValue: '',
47
+ selectedNodeKey: '',
47
48
  expandedKeys: [],
48
49
  searchValue: '',
49
50
  autoExpandParent: true,
@@ -122,9 +123,11 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
122
123
  this.handleOnExpand(info.node.props.eventKey);
123
124
  };
124
125
 
125
- getSearchedExpandedKeys(tags, value = '') {
126
+ getSearchedExpandedKeys(tags, value = '', parentPath = '') {
126
127
  let list = [];
127
128
  _.forEach(tags, (val = {}, key) => {
129
+ const rawKey = val?.incentiveSeriesId ? `${key}(${val?.incentiveSeriesId})` : `${key}`;
130
+ const nodeKey = parentPath ? `${parentPath}.${rawKey}` : rawKey;
128
131
  const tagName =
129
132
  typeof val?.name === 'string'
130
133
  ? _.toLower(_.get(val, "name", ""))
@@ -137,16 +140,16 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
137
140
  && (tagName.includes(searchStringLower)
138
141
  || tagNameWithoutUnderscore.includes(searchStringLower))
139
142
  ) {
140
- list.push(key);
143
+ list.push(nodeKey);
141
144
  }
142
- const temp = this.getSearchedExpandedKeys(val?.subtags, value);
145
+ const temp = this.getSearchedExpandedKeys(val?.subtags, value, nodeKey);
143
146
  list = list.concat(temp);
144
147
  } else if (
145
148
  val?.name
146
149
  && (tagName.includes(searchStringLower)
147
150
  || tagNameWithoutUnderscore.includes(searchStringLower))
148
151
  ) {
149
- list.push(key);
152
+ list.push(nodeKey);
150
153
  }
151
154
  });
152
155
  return list;
@@ -200,12 +203,17 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
200
203
  handleOnSelect = (selectedKeys, info) => {
201
204
  if (selectedKeys.length > 0) {
202
205
  if (info && info.selectedNodes && info.selectedNodes.length > 0 && info.selectedNodes[0].props.isLeaf) {
203
- this.setState({tagValue: selectedKeys[0]});
204
- const ifDynamicTag = this.checkIfDynamicTag(selectedKeys[0]);
206
+ const selectedNode = info.selectedNodes[0];
207
+ const selectedTagValue = selectedNode?.props?.tagKey || selectedKeys[0];
208
+ this.setState({
209
+ tagValue: selectedTagValue,
210
+ selectedNodeKey: selectedKeys[0],
211
+ });
212
+ const ifDynamicTag = this.checkIfDynamicTag(selectedTagValue);
205
213
  if (ifDynamicTag) {
206
214
  this.renderDynamicTagFlow();
207
215
  } else {
208
- this.props.onSelect(selectedKeys, info);
216
+ this.props.onSelect([selectedTagValue], info);
209
217
  this.setState({visible: false});
210
218
  }
211
219
  } else if (info && info.selectedNodes && info.selectedNodes.length > 0 && !info.selectedNodes[0].props.isLeaf) {
@@ -237,7 +245,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
237
245
  this.setState({showModal: true, visible: false});
238
246
  };
239
247
 
240
- renderTags = (tags) => {
248
+ renderTags = (tags, parentPath = '') => {
241
249
  const searchString = this.state.searchValue || '';
242
250
  const {
243
251
  disableRelatedTags, childTagsToDisable, parentTagstoDisable,
@@ -260,6 +268,8 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
260
268
  clonedTags = _.omit(clonedTags, CUSTOMER_BARCODE_TAG);
261
269
  }
262
270
  _.forEach(clonedTags, (val = '', key) => {
271
+ const rawKey = val?.incentiveSeriesId ? `${key}(${val?.incentiveSeriesId})` : `${key}`;
272
+ const nodeKey = parentPath ? `${parentPath}.${rawKey}` : rawKey;
263
273
  let supportedTagsString = '';
264
274
  _.forEach(val.supportedTags, (supportedTag) => {
265
275
  supportedTagsString += `${supportedTag} ,`;
@@ -276,13 +286,14 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
276
286
  || tagNameWithoutUnderscore.includes(searchStringLower));
277
287
  if (_.has(val, 'subtags')) {
278
288
  const disabled = disableRelatedTags ? parentTagstoDisable.includes(key) : false;
279
- const temp = this.renderTags(val?.subtags);
289
+ const temp = this.renderTags(val?.subtags, nodeKey);
280
290
  if (temp?.length) {
281
291
  const tagValue = (
282
292
  <CapTreeNode
283
293
  title={disabled ? <CapTooltip title={loyaltyAttrDisableText}>{val?.name}</CapTooltip> : val?.name}
284
294
  tag={val}
285
- key={val?.incentiveSeriesId ? `${key}(${val?.incentiveSeriesId})` : `${key}`}
295
+ tagKey={rawKey}
296
+ key={nodeKey}
286
297
  disabled={disabled}
287
298
  >
288
299
  {temp}
@@ -317,12 +328,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
317
328
  )
318
329
  }
319
330
  tag={val}
331
+ tagKey={rawKey}
320
332
  isLeaf
321
- key={
322
- val?.incentiveSeriesId
323
- ? `${key}(${val?.incentiveSeriesId})`
324
- : `${key}`
325
- }
333
+ key={nodeKey}
326
334
  disabled={childDisabled}
327
335
  >
328
336
  </CapTreeNode>
@@ -353,12 +361,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
353
361
  )
354
362
  }
355
363
  tag={val}
364
+ tagKey={rawKey}
356
365
  isLeaf
357
- key={
358
- val?.incentiveSeriesId
359
- ? `${key}(${val?.incentiveSeriesId})`
360
- : `${key}`
361
- }
366
+ key={nodeKey}
362
367
  disabled={childDisabled}
363
368
  >
364
369
  </CapTreeNode>
@@ -388,7 +393,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
388
393
  } = this.props;
389
394
  const {formatMessage} = intl;
390
395
  const {
391
- tagValue, expandedKeys, autoExpandParent, visible, translationLang, selectedContext, isLoadingLoyaltyTags, isLoadingContextChange,
396
+ tagValue, selectedNodeKey, expandedKeys, autoExpandParent, visible, translationLang, selectedContext, isLoadingLoyaltyTags, isLoadingContextChange,
392
397
  } = this.state;
393
398
 
394
399
  // Show loading spinner if general loading OR if specifically loading loyalty tags OR if context change is in progress
@@ -407,7 +412,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
407
412
  },
408
413
  ];
409
414
  const contentSection = (
410
- <CapRow>
415
+ <CapRow className="cap-tag-list-popover-inner">
411
416
  <CapSpin tip={formatMessage(messages.gettingTags)} spinning={shouldShowLoading}>
412
417
  <Search
413
418
  style={{ marginBottom: 8, width: '250px'}}
@@ -427,7 +432,7 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
427
432
  <CapTree
428
433
  styling={{height: '350px', overflow: 'auto'}}
429
434
  onSelect={this.handleOnSelect}
430
- selectedKeys={tagValue}
435
+ selectedKeys={selectedNodeKey ? [selectedNodeKey] : []}
431
436
  expandedKeys={expandedKeys}
432
437
  autoExpandParent={autoExpandParent}
433
438
  onExpand={this.onExpand}
@@ -1,5 +1,34 @@
1
1
  @import "~@capillarytech/cap-ui-library/styles/_variables";
2
2
 
3
+ .cap-tag-list-popover-inner {
4
+ max-width: 20rem;
5
+ min-width: 0;
6
+ box-sizing: border-box;
7
+
8
+ .ant-tree.cap-tree-v2.ant-tree-icon-hide {
9
+ width: 100%;
10
+ max-width: 100%;
11
+
12
+ ul {
13
+ max-width: 100%;
14
+ }
15
+
16
+ li {
17
+ overflow: hidden;
18
+ max-width: 100%;
19
+ }
20
+
21
+ li .ant-tree-node-content-wrapper {
22
+ width: calc(100% - 3.5rem); // leave room for switcher (~24px)
23
+ max-width: calc(100% - 3.5rem);
24
+ overflow: hidden;
25
+ vertical-align: top;
26
+ box-sizing: border-box;
27
+ text-overflow: ellipsis;
28
+ }
29
+ }
30
+ }
31
+
3
32
  @media (max-height: 25rem) {
4
33
  .ant-tree.cap-tree-v2.ant-tree-icon-hide {
5
34
  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
  )}
@@ -24,6 +24,7 @@ const ExistingCustomerModal = ({ customerModal, onCloseCustomerModal, customerDa
24
24
  type="primary"
25
25
  onClick={() => onSave({}, setIsLoading)}
26
26
  loading={isLoading}
27
+ disabled={isLoading}
27
28
  >
28
29
  <FormattedMessage {...messages.saveButton} />
29
30
  </CapButton>