@capillarytech/creatives-library 8.0.345-alpha.14 → 8.0.345-alpha.15

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 (129) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/tests/api.test.js +13 -0
  4. package/utils/commonUtils.js +19 -1
  5. package/utils/rcsPayloadUtils.js +92 -0
  6. package/utils/templateVarUtils.js +201 -0
  7. package/utils/tests/templateVarUtils.test.js +204 -0
  8. package/v2Components/CapActionButton/constants.js +7 -0
  9. package/v2Components/CapActionButton/index.js +167 -109
  10. package/v2Components/CapActionButton/index.scss +157 -6
  11. package/v2Components/CapActionButton/messages.js +19 -3
  12. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  13. package/v2Components/CapTagList/index.js +10 -0
  14. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  16. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  17. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  21. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  22. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  23. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  24. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  26. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  27. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  28. package/v2Components/CommonTestAndPreview/index.js +676 -186
  29. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  30. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  31. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  32. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  33. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  34. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  35. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  36. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  37. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  38. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  39. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  40. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  41. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  42. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  43. package/v2Components/FormBuilder/index.js +8 -10
  44. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  45. package/v2Components/SmsFallback/constants.js +73 -0
  46. package/v2Components/SmsFallback/index.js +955 -0
  47. package/v2Components/SmsFallback/index.scss +265 -0
  48. package/v2Components/SmsFallback/messages.js +78 -0
  49. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  50. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  51. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  52. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  53. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  54. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  55. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  56. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  57. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  58. package/v2Components/TemplatePreview/constants.js +2 -0
  59. package/v2Components/TemplatePreview/index.js +143 -28
  60. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  61. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  62. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  63. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  64. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  65. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  66. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  67. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  68. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  69. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  70. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  71. package/v2Containers/CreativesContainer/constants.js +9 -0
  72. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  73. package/v2Containers/CreativesContainer/index.js +300 -103
  74. package/v2Containers/CreativesContainer/index.scss +51 -1
  75. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  77. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  79. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  80. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  81. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  82. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  83. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  84. package/v2Containers/Email/reducer.js +3 -11
  85. package/v2Containers/Email/sagas.js +5 -9
  86. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -4
  87. package/v2Containers/Email/tests/sagas.test.js +3 -21
  88. package/v2Containers/Rcs/constants.js +119 -8
  89. package/v2Containers/Rcs/index.js +2379 -807
  90. package/v2Containers/Rcs/index.js.rej +1336 -0
  91. package/v2Containers/Rcs/index.scss +276 -6
  92. package/v2Containers/Rcs/index.scss.rej +74 -0
  93. package/v2Containers/Rcs/messages.js +38 -3
  94. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +225 -0
  95. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +98018 -70073
  96. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  97. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap.rej +128 -0
  98. package/v2Containers/Rcs/tests/index.test.js +152 -121
  99. package/v2Containers/Rcs/tests/mockData.js +38 -0
  100. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +318 -0
  101. package/v2Containers/Rcs/tests/utils.test.js +646 -30
  102. package/v2Containers/Rcs/utils.js +478 -11
  103. package/v2Containers/Sms/Create/index.js +100 -40
  104. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  105. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  106. package/v2Containers/SmsTrai/Create/index.js +9 -4
  107. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  108. package/v2Containers/SmsTrai/Edit/index.js +636 -130
  109. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  110. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  111. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4328 -2375
  112. package/v2Containers/SmsWrapper/index.js +37 -8
  113. package/v2Containers/TagList/index.js +6 -0
  114. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  115. package/v2Containers/Templates/_templates.scss +163 -2
  116. package/v2Containers/Templates/actions.js +11 -0
  117. package/v2Containers/Templates/constants.js +2 -0
  118. package/v2Containers/Templates/index.js +119 -54
  119. package/v2Containers/Templates/sagas.js +57 -12
  120. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  121. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  122. package/v2Containers/Templates/tests/sagas.test.js +193 -123
  123. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  124. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  125. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  126. package/v2Containers/TemplatesV2/index.js +86 -23
  127. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  128. package/v2Containers/Whatsapp/index.js +3 -20
  129. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -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
  });
@@ -386,6 +386,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
386
386
  render() {
387
387
  const {
388
388
  hidePopover = false, intl = {}, moduleFilterEnabled, label, modalProps, channel, fetchingSchemaError = false,
389
+ overlayStyle,
390
+ overlayClassName,
391
+ getPopupContainer,
389
392
  } = this.props;
390
393
  const {formatMessage} = intl;
391
394
  const {
@@ -478,6 +481,9 @@ class CapTagList extends React.Component { // eslint-disable-line react/prefer-s
478
481
  content={contentSection}
479
482
  trigger="click"
480
483
  placement={this.props.popoverPlacement || (channel === EMAIL.toUpperCase() ? "leftTop" : "rightTop")}
484
+ overlayStyle={overlayStyle}
485
+ overlayClassName={overlayClassName}
486
+ getPopupContainer={getPopupContainer}
481
487
  >
482
488
  <CapTooltip
483
489
  title={
@@ -549,6 +555,10 @@ CapTagList.propTypes = {
549
555
  disableTooltipMsg: PropTypes.string,
550
556
  fetchingSchemaError: PropTypes.bool,
551
557
  popoverPlacement: PropTypes.string,
558
+ overlayStyle: PropTypes.object,
559
+ overlayClassName: PropTypes.string,
560
+ /** e.g. () => document.body — avoids overflow/stacking issues inside slideboxes */
561
+ getPopupContainer: PropTypes.func,
552
562
  };
553
563
 
554
564
  CapTagList.defaultValue = {
@@ -8,6 +8,7 @@ import CapButton from '@capillarytech/cap-ui-library/CapButton';
8
8
  import CapInput from '@capillarytech/cap-ui-library/CapInput';
9
9
  import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
10
10
  import messages from './messages';
11
+ import { CUSTOM_VALUES_EDITOR_SECTION_FALLBACK_KEY } from './constants';
11
12
 
12
13
  const CustomValuesEditor = ({
13
14
  isExtractingTags,
@@ -16,15 +17,16 @@ const CustomValuesEditor = ({
16
17
  setShowJSON,
17
18
  customValues,
18
19
  handleJSONTextChange,
19
- extractedTags,
20
- requiredTags,
21
- optionalTags,
20
+ sections,
22
21
  handleCustomValueChange,
23
22
  handleDiscardCustomValues,
24
23
  handleUpdatePreview,
25
24
  isUpdatingPreview,
26
25
  formatMessage,
27
26
  }) => {
27
+ /** Same as SMS Test & Preview: show token path from extract-tags (fullPath or name). */
28
+ const getPersonalizationTagColumnLabel = (tagNode) => tagNode?.fullPath ?? tagNode?.name ?? '';
29
+
28
30
  if (isExtractingTags) {
29
31
  return (
30
32
  <CapRow className="loading-container">
@@ -77,52 +79,68 @@ const CustomValuesEditor = ({
77
79
  </CapRow>
78
80
  ) : (
79
81
  <>
80
- {extractedTags?.length > 0 && (
81
- <CapRow className="values-table">
82
- <CapRow className="table-header">
83
- <CapLabel type="label31" className="header-cell">
84
- <FormattedMessage {...messages.personalizationTags} />
85
- </CapLabel>
86
- <CapLabel type="label31" className="header-cell">
87
- <FormattedMessage {...messages.customValues} />
82
+ {(sections || []).filter((tagsSection) =>
83
+ (tagsSection?.requiredTags?.length || 0) + (tagsSection?.optionalTags?.length || 0) > 0).map((section) => (
84
+ <React.Fragment key={section.key || section.title?.id || section.title || CUSTOM_VALUES_EDITOR_SECTION_FALLBACK_KEY}>
85
+ {section.title ? (
86
+ <CapLabel type="label2" className="tags-section-title">
87
+ {typeof section.title === 'string' ? section.title : <FormattedMessage {...section.title} />}
88
88
  </CapLabel>
89
- </CapRow>
90
- {requiredTags.map((tag) => (
91
- <CapRow key={tag.fullPath} className="value-row">
92
- <CapRow className="tag-name">
93
- {tag.fullPath}
94
- <span className="required-tag-indicator">*</span>
95
- </CapRow>
96
- <CapRow className="tag-input">
97
- <CapInput
98
- type="text"
99
- isRequired
100
- className="tag-input-field"
101
- value={customValues[tag.fullPath] || ''}
102
- onChange={(e) => handleCustomValueChange(tag.fullPath, e.target.value)}
103
- placeholder={formatMessage(messages.enterValue)}
104
- size="small"
105
- />
106
- </CapRow>
89
+ ) : null}
90
+ <CapRow className="values-table">
91
+ <CapRow className="table-header">
92
+ <CapLabel type="label31" className="header-cell">
93
+ <FormattedMessage {...messages.personalizationTags} />
94
+ </CapLabel>
95
+ <CapLabel type="label31" className="header-cell">
96
+ <FormattedMessage {...messages.customValues} />
97
+ </CapLabel>
107
98
  </CapRow>
108
- ))}
109
- {optionalTags?.map((tag) => (
110
- <CapRow key={tag.fullPath} className="value-row">
111
- <CapRow className="tag-name">{tag.fullPath}</CapRow>
112
- <CapRow className="tag-input">
113
- <CapInput
114
- type="text"
115
- className="tag-input-field"
116
- value={customValues[tag.fullPath] || ''}
117
- onChange={(e) => handleCustomValueChange(tag.fullPath, e.target.value)}
118
- placeholder={formatMessage(messages.enterValue)}
119
- size="small"
120
- />
99
+ {(section?.requiredTags || []).map((tag, tagIndex) => {
100
+ const personalizationTagColumnText = getPersonalizationTagColumnLabel(tag);
101
+ return (
102
+ <CapRow key={tag?.fullPath ?? `required-${tagIndex}`} className="value-row">
103
+ <CapRow className="tag-name">
104
+ {personalizationTagColumnText}
105
+ <span className="required-tag-indicator">*</span>
106
+ </CapRow>
107
+ <CapRow className="tag-input">
108
+ <CapInput
109
+ type="text"
110
+ isRequired
111
+ className="tag-input-field"
112
+ value={customValues?.[tag?.fullPath] ?? ''}
113
+ onChange={(e) => handleCustomValueChange(tag?.fullPath, e.target.value)}
114
+ placeholder={formatMessage(messages.enterValue)}
115
+ size="small"
116
+ />
117
+ </CapRow>
121
118
  </CapRow>
122
- </CapRow>
123
- ))}
124
- </CapRow>
125
- )}
119
+ );
120
+ })}
121
+ {(section?.optionalTags || []).map((tag, tagIndex) => {
122
+ const personalizationTagColumnText = getPersonalizationTagColumnLabel(tag);
123
+ return (
124
+ <CapRow key={tag?.fullPath ?? `optional-${tagIndex}`} className="value-row">
125
+ <CapRow className="tag-name">
126
+ {personalizationTagColumnText}
127
+ </CapRow>
128
+ <CapRow className="tag-input">
129
+ <CapInput
130
+ type="text"
131
+ className="tag-input-field"
132
+ value={customValues?.[tag?.fullPath] ?? ''}
133
+ onChange={(e) => handleCustomValueChange(tag?.fullPath, e.target.value)}
134
+ placeholder={formatMessage(messages.enterValue)}
135
+ size="small"
136
+ />
137
+ </CapRow>
138
+ </CapRow>
139
+ );
140
+ })}
141
+ </CapRow>
142
+ </React.Fragment>
143
+ ))}
126
144
  </>
127
145
  )}
128
146
  <CapRow className="editor-actions">
@@ -156,9 +174,12 @@ CustomValuesEditor.propTypes = {
156
174
  setShowJSON: PropTypes.func.isRequired,
157
175
  customValues: PropTypes.object.isRequired,
158
176
  handleJSONTextChange: PropTypes.func.isRequired,
159
- extractedTags: PropTypes.array.isRequired,
160
- requiredTags: PropTypes.array.isRequired,
161
- optionalTags: PropTypes.array.isRequired,
177
+ sections: PropTypes.arrayOf(PropTypes.shape({
178
+ key: PropTypes.string,
179
+ title: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
180
+ requiredTags: PropTypes.array,
181
+ optionalTags: PropTypes.array,
182
+ })).isRequired,
162
183
  handleCustomValueChange: PropTypes.func.isRequired,
163
184
  handleDiscardCustomValues: PropTypes.func.isRequired,
164
185
  handleUpdatePreview: PropTypes.func.isRequired,
@@ -18,11 +18,17 @@
18
18
  }
19
19
 
20
20
  &__summary-entry {
21
+ display: flex;
22
+ align-items: baseline;
23
+ gap: 0;
21
24
  margin-right: $CAP_SPACE_18;
22
25
  }
23
26
 
24
- &__summary-key {
25
- line-height: $CAP_SPACE_16;
27
+ &__summary-key,
28
+ &__summary-value {
29
+ line-height: 1.4;
30
+ margin-top: 0;
31
+ margin-bottom: 0;
26
32
  }
27
33
 
28
34
  &__edit-icon {