@capillarytech/creatives-library 8.0.345-alpha.13 → 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 (138) hide show
  1. package/constants/unified.js +29 -0
  2. package/package.json +1 -1
  3. package/services/api.js +0 -20
  4. package/services/tests/api.test.js +13 -59
  5. package/utils/commonUtils.js +19 -1
  6. package/utils/rcsPayloadUtils.js +92 -0
  7. package/utils/templateVarUtils.js +201 -0
  8. package/utils/tests/templateVarUtils.test.js +204 -0
  9. package/v2Components/CapActionButton/constants.js +7 -0
  10. package/v2Components/CapActionButton/index.js +167 -109
  11. package/v2Components/CapActionButton/index.scss +157 -6
  12. package/v2Components/CapActionButton/messages.js +19 -3
  13. package/v2Components/CapActionButton/tests/index.test.js +41 -17
  14. package/v2Components/CapCustomSkeleton/index.js +1 -1
  15. package/v2Components/CapCustomSkeleton/tests/__snapshots__/index.test.js.snap +12 -12
  16. package/v2Components/CapTagList/index.js +10 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +160 -15
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js.rej +18 -0
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +341 -76
  28. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  29. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  30. package/v2Components/CommonTestAndPreview/constants.js +38 -2
  31. package/v2Components/CommonTestAndPreview/index.js +676 -186
  32. package/v2Components/CommonTestAndPreview/messages.js +49 -3
  33. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  34. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +308 -284
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +231 -65
  37. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  38. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  39. package/v2Components/CommonTestAndPreview/tests/PreviewSection.test.js +8 -1
  40. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +34 -13
  41. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/RcsPreviewContent.test.js +281 -283
  42. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  43. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  46. package/v2Components/FormBuilder/index.js +8 -10
  47. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  48. package/v2Components/SmsFallback/constants.js +73 -0
  49. package/v2Components/SmsFallback/index.js +955 -0
  50. package/v2Components/SmsFallback/index.scss +265 -0
  51. package/v2Components/SmsFallback/messages.js +78 -0
  52. package/v2Components/SmsFallback/smsFallbackUtils.js +118 -0
  53. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  54. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  55. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +277 -0
  58. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  59. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  60. package/v2Components/TemplatePreview/_templatePreview.scss +33 -23
  61. package/v2Components/TemplatePreview/constants.js +2 -0
  62. package/v2Components/TemplatePreview/index.js +143 -28
  63. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  64. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  65. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  66. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  67. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  68. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  69. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  70. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  71. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  72. package/v2Containers/CreativesContainer/SlideBoxFooter.js +11 -4
  73. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  74. package/v2Containers/CreativesContainer/constants.js +9 -0
  75. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  76. package/v2Containers/CreativesContainer/index.js +300 -108
  77. package/v2Containers/CreativesContainer/index.scss +51 -1
  78. package/v2Containers/CreativesContainer/messages.js +0 -4
  79. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  80. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +78 -34
  81. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +79 -16
  82. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  83. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +357 -98
  84. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -18
  85. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  86. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  87. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  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/ChannelTypeIllustration.js +6 -23
  115. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  116. package/v2Containers/Templates/_templates.scss +181 -126
  117. package/v2Containers/Templates/actions.js +11 -36
  118. package/v2Containers/Templates/constants.js +2 -23
  119. package/v2Containers/Templates/index.js +142 -333
  120. package/v2Containers/Templates/messages.js +0 -68
  121. package/v2Containers/Templates/reducer.js +0 -68
  122. package/v2Containers/Templates/sagas.js +55 -98
  123. package/v2Containers/Templates/selectors.js +0 -12
  124. package/v2Containers/Templates/tests/ChannelTypeIllustration.test.js +0 -12
  125. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  126. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1042 -1256
  127. package/v2Containers/Templates/tests/index.test.js +0 -6
  128. package/v2Containers/Templates/tests/reducer.test.js +0 -178
  129. package/v2Containers/Templates/tests/sagas.test.js +200 -436
  130. package/v2Containers/Templates/tests/selector.test.js +0 -32
  131. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  132. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  133. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  134. package/v2Containers/TemplatesV2/index.js +86 -23
  135. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  136. package/v2Containers/Whatsapp/index.js +3 -20
  137. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
  138. package/v2Containers/Assets/images/archive_Empty_Illustration.svg +0 -9
@@ -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
  });
@@ -28,7 +28,7 @@ export default function CapCustomSkeleton(props) {
28
28
  xs={24}
29
29
  sm={12}
30
30
  md={8}
31
- lg={8}
31
+ lg={6}
32
32
  >
33
33
  <CapSkeleton
34
34
  active
@@ -20,7 +20,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
20
20
  >
21
21
  <CapColumn
22
22
  key="0"
23
- lg={8}
23
+ lg={6}
24
24
  md={8}
25
25
  sm={12}
26
26
  xs={24}
@@ -40,7 +40,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
40
40
  </CapColumn>
41
41
  <CapColumn
42
42
  key="1"
43
- lg={8}
43
+ lg={6}
44
44
  md={8}
45
45
  sm={12}
46
46
  xs={24}
@@ -60,7 +60,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
60
60
  </CapColumn>
61
61
  <CapColumn
62
62
  key="2"
63
- lg={8}
63
+ lg={6}
64
64
  md={8}
65
65
  sm={12}
66
66
  xs={24}
@@ -80,7 +80,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
80
80
  </CapColumn>
81
81
  <CapColumn
82
82
  key="3"
83
- lg={8}
83
+ lg={6}
84
84
  md={8}
85
85
  sm={12}
86
86
  xs={24}
@@ -100,7 +100,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
100
100
  </CapColumn>
101
101
  <CapColumn
102
102
  key="4"
103
- lg={8}
103
+ lg={6}
104
104
  md={8}
105
105
  sm={12}
106
106
  xs={24}
@@ -120,7 +120,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
120
120
  </CapColumn>
121
121
  <CapColumn
122
122
  key="5"
123
- lg={8}
123
+ lg={6}
124
124
  md={8}
125
125
  sm={12}
126
126
  xs={24}
@@ -140,7 +140,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
140
140
  </CapColumn>
141
141
  <CapColumn
142
142
  key="6"
143
- lg={8}
143
+ lg={6}
144
144
  md={8}
145
145
  sm={12}
146
146
  xs={24}
@@ -160,7 +160,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
160
160
  </CapColumn>
161
161
  <CapColumn
162
162
  key="7"
163
- lg={8}
163
+ lg={6}
164
164
  md={8}
165
165
  sm={12}
166
166
  xs={24}
@@ -180,7 +180,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
180
180
  </CapColumn>
181
181
  <CapColumn
182
182
  key="8"
183
- lg={8}
183
+ lg={6}
184
184
  md={8}
185
185
  sm={12}
186
186
  xs={24}
@@ -200,7 +200,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
200
200
  </CapColumn>
201
201
  <CapColumn
202
202
  key="9"
203
- lg={8}
203
+ lg={6}
204
204
  md={8}
205
205
  sm={12}
206
206
  xs={24}
@@ -220,7 +220,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
220
220
  </CapColumn>
221
221
  <CapColumn
222
222
  key="10"
223
- lg={8}
223
+ lg={6}
224
224
  md={8}
225
225
  sm={12}
226
226
  xs={24}
@@ -240,7 +240,7 @@ exports[`CapCustomSkeleton test renders correct CapCustomSkeleton component 1`]
240
240
  </CapColumn>
241
241
  <CapColumn
242
242
  key="11"
243
- lg={8}
243
+ lg={6}
244
244
  md={8}
245
245
  sm={12}
246
246
  xs={24}
@@ -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,