@capillarytech/creatives-library 9.0.29 → 9.0.31

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 (147) hide show
  1. package/constants/unified.js +29 -0
  2. package/global-styles.js +1 -1
  3. package/package.json +1 -1
  4. package/services/tests/api.test.js +35 -20
  5. package/utils/commonUtils.js +22 -1
  6. package/utils/rcsPayloadUtils.js +102 -0
  7. package/utils/templateVarUtils.js +198 -0
  8. package/utils/tests/rcsPayloadUtils.test.js +295 -0
  9. package/utils/tests/templateVarUtils.test.js +204 -0
  10. package/v2Components/CapActionButton/constants.js +21 -1
  11. package/v2Components/CapActionButton/index.js +261 -144
  12. package/v2Components/CapActionButton/index.scss +245 -14
  13. package/v2Components/CapActionButton/messages.js +32 -3
  14. package/v2Components/CapActionButton/tests/index.test.js +74 -19
  15. package/v2Components/CapImageUpload/index.js +2 -2
  16. package/v2Components/CapTagList/index.js +9 -0
  17. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -48
  18. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  19. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +214 -21
  20. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  21. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +83 -9
  22. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  23. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +58 -11
  24. package/v2Components/CommonTestAndPreview/SendTestMessage.js +10 -5
  25. package/v2Components/CommonTestAndPreview/UnifiedPreview/RcsPreviewContent.js +186 -22
  26. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +381 -80
  27. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +155 -8
  28. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +11 -0
  29. package/v2Components/CommonTestAndPreview/constants.js +35 -2
  30. package/v2Components/CommonTestAndPreview/index.js +774 -231
  31. package/v2Components/CommonTestAndPreview/messages.js +45 -3
  32. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  33. package/v2Components/CommonTestAndPreview/sagas.js +25 -6
  34. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +1 -0
  35. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +412 -257
  36. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +337 -63
  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 +108 -15
  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 +133 -4
  44. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  45. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +31 -24
  46. package/v2Components/CommonTestAndPreview/tests/utils.test.js +151 -0
  47. package/v2Components/CommonTestAndPreview/utils.js +84 -0
  48. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +91 -0
  49. package/v2Components/SmsFallback/constants.js +94 -0
  50. package/v2Components/SmsFallback/index.js +958 -0
  51. package/v2Components/SmsFallback/index.scss +266 -0
  52. package/v2Components/SmsFallback/messages.js +78 -0
  53. package/v2Components/SmsFallback/smsFallbackUtils.js +120 -0
  54. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  55. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  56. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  57. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +208 -0
  58. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +309 -0
  59. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +471 -0
  60. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  61. package/v2Components/TemplatePreview/_templatePreview.scss +45 -25
  62. package/v2Components/TemplatePreview/constants.js +2 -0
  63. package/v2Components/TemplatePreview/index.js +147 -32
  64. package/v2Components/TemplatePreview/tests/index.test.js +142 -0
  65. package/v2Components/TestAndPreviewSlidebox/CustomValuesEditor.js +6 -6
  66. package/v2Components/TestAndPreviewSlidebox/index.js +13 -1
  67. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  68. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  69. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  70. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  71. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  72. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +4 -1
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +4 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +20 -20
  75. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +27 -0
  76. package/v2Containers/CreativesContainer/SlideBoxContent.js +37 -4
  77. package/v2Containers/CreativesContainer/SlideBoxFooter.js +14 -5
  78. package/v2Containers/CreativesContainer/SlideBoxHeader.js +36 -5
  79. package/v2Containers/CreativesContainer/constants.js +11 -0
  80. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +77 -0
  81. package/v2Containers/CreativesContainer/index.js +322 -106
  82. package/v2Containers/CreativesContainer/index.scss +102 -1
  83. package/v2Containers/CreativesContainer/tests/CreativesSlideBoxWrapper.test.js +58 -0
  84. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  85. package/v2Containers/CreativesContainer/tests/SlideBoxContent.smsDltPreview.test.js +73 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxContent.test.js +37 -0
  87. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +103 -34
  88. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +193 -15
  89. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +88 -0
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +549 -57
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -15
  92. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  93. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  94. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  95. package/v2Containers/MobilePush/Create/test/saga.test.js +2 -2
  96. package/v2Containers/Rcs/carouselUtils.js +224 -0
  97. package/v2Containers/Rcs/components/CarouselCard.js +317 -0
  98. package/v2Containers/Rcs/components/CarouselCardButtons.js +113 -0
  99. package/v2Containers/Rcs/components/CarouselCardMedia.js +136 -0
  100. package/v2Containers/Rcs/components/CarouselCharacterCount.js +31 -0
  101. package/v2Containers/Rcs/components/CarouselDimensionSelection.js +80 -0
  102. package/v2Containers/Rcs/constants.js +132 -16
  103. package/v2Containers/Rcs/index.js +1822 -946
  104. package/v2Containers/Rcs/index.scss +443 -8
  105. package/v2Containers/Rcs/messages.js +45 -22
  106. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +494 -0
  107. package/v2Containers/Rcs/tests/CarouselCard.test.js +464 -0
  108. package/v2Containers/Rcs/tests/CarouselCardButtons.test.js +211 -0
  109. package/v2Containers/Rcs/tests/CarouselCardMedia.test.js +160 -0
  110. package/v2Containers/Rcs/tests/CarouselCharacterCount.test.js +50 -0
  111. package/v2Containers/Rcs/tests/CarouselDimensionSelection.test.js +119 -0
  112. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +74440 -39387
  113. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +10 -74
  114. package/v2Containers/Rcs/tests/carouselUtils.test.js +916 -0
  115. package/v2Containers/Rcs/tests/index.test.js +275 -40
  116. package/v2Containers/Rcs/tests/mockData.js +38 -0
  117. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +912 -0
  118. package/v2Containers/Rcs/tests/utils.test.js +682 -30
  119. package/v2Containers/Rcs/utils.js +514 -12
  120. package/v2Containers/Sms/Create/index.js +115 -48
  121. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  122. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  123. package/v2Containers/SmsTrai/Create/index.js +9 -4
  124. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  125. package/v2Containers/SmsTrai/Edit/index.js +678 -169
  126. package/v2Containers/SmsTrai/Edit/index.scss +126 -0
  127. package/v2Containers/SmsTrai/Edit/messages.js +14 -4
  128. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +5615 -3014
  129. package/v2Containers/SmsWrapper/index.js +37 -8
  130. package/v2Containers/TagList/index.js +5 -0
  131. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  132. package/v2Containers/Templates/_templates.scss +196 -12
  133. package/v2Containers/Templates/actions.js +11 -0
  134. package/v2Containers/Templates/constants.js +2 -0
  135. package/v2Containers/Templates/index.js +131 -59
  136. package/v2Containers/Templates/sagas.js +57 -13
  137. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +129 -0
  138. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1060 -1015
  139. package/v2Containers/Templates/tests/sagas.test.js +199 -16
  140. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  141. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  142. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  143. package/v2Containers/TemplatesV2/index.js +88 -25
  144. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  145. package/v2Containers/WeChat/MapTemplates/test/saga.test.js +9 -9
  146. package/v2Containers/Whatsapp/index.js +3 -20
  147. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Shared message editor that renders template text with {{var}} and/or DLT `{#var#}` segments as
3
+ * variable inputs and static text as headings.
4
+ * Reused by RCS (title/description), SmsTrai Edit (SMS fallback), and WhatsApp (edit message/header).
5
+ */
6
+ import React from 'react';
7
+ import PropTypes from 'prop-types';
8
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
9
+ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
10
+ import CapInput from '@capillarytech/cap-ui-library/CapInput';
11
+ import {
12
+ splitTemplateVarString,
13
+ DEFAULT_MUSTACHE_VAR_REGEX,
14
+ isAnyTemplateVarToken,
15
+ } from '../../utils/templateVarUtils';
16
+
17
+ import './index.scss';
18
+ import { VAR_SEGMENT_PLACEHOLDER_PREFIX } from './constants';
19
+
20
+ const { TextArea } = CapInput;
21
+
22
+ export function VarSegmentMessageEditor({
23
+ templateString = '',
24
+ valueMap = {},
25
+ onChange,
26
+ onFocus,
27
+ placeholderPrefix = VAR_SEGMENT_PLACEHOLDER_PREFIX,
28
+ getPlaceholder,
29
+ wrapperClassName = 'rcs_text_area_wrapper',
30
+ rowClassName = 'rcs-edit-template-message-input',
31
+ headingClassName = 'rcs-edit-template-message-split',
32
+ varRegex,
33
+ readOnly = false,
34
+ disabled = false,
35
+ footerContent,
36
+ renderVarFooter,
37
+ }) {
38
+ const segments = splitTemplateVarString(templateString, varRegex || DEFAULT_MUSTACHE_VAR_REGEX);
39
+ if (!segments?.length) return null;
40
+
41
+ return (
42
+ <div className={wrapperClassName}>
43
+ <CapRow className={rowClassName}>
44
+ {segments.map((segmentToken, segmentIndex) => {
45
+ const isVar =
46
+ typeof segmentToken === 'string' && isAnyTemplateVarToken(segmentToken);
47
+ if (isVar) {
48
+ const varSegmentFieldId = `${segmentToken}_${segmentIndex}`;
49
+ const slotValueFromMap = valueMap?.[varSegmentFieldId];
50
+ // Missing key: show empty (not the raw {{…}} token) so cleared slots and incomplete maps
51
+ // cannot resurrect the token; placeholder still guides the user.
52
+ const value =
53
+ slotValueFromMap !== undefined && slotValueFromMap !== null ? slotValueFromMap : '';
54
+ if (readOnly) {
55
+ return (
56
+ <CapHeading
57
+ key={varSegmentFieldId}
58
+ type="h4"
59
+ className={`${headingClassName} var-segment-message-editor__read-only-value`.trim()}
60
+ >
61
+ {value}
62
+ </CapHeading>
63
+ );
64
+ }
65
+ const fromGet = getPlaceholder && getPlaceholder(segmentToken, segmentIndex);
66
+ const placeholder =
67
+ fromGet !== undefined && fromGet !== null && fromGet !== ''
68
+ ? fromGet
69
+ : `${placeholderPrefix}${segmentToken}`;
70
+ return (
71
+ <div key={varSegmentFieldId} className="var-segment-message-editor__var-slot">
72
+ <TextArea
73
+ id={varSegmentFieldId}
74
+ placeholder={placeholder}
75
+ autosize={{ minRows: 1, maxRows: 3 }}
76
+ value={value}
77
+ onFocus={() => onFocus && onFocus(varSegmentFieldId)}
78
+ onChange={(e) =>
79
+ onChange && onChange(varSegmentFieldId, e?.target?.value ?? '')}
80
+ disabled={disabled}
81
+ />
82
+ {renderVarFooter
83
+ ? renderVarFooter(segmentToken, segmentIndex, varSegmentFieldId)
84
+ : null}
85
+ </div>
86
+ );
87
+ }
88
+ if (segmentToken) {
89
+ return (
90
+ <CapHeading
91
+ key={`static_${segmentIndex}_${segmentToken}`}
92
+ type="h4"
93
+ className={headingClassName}
94
+ >
95
+ {segmentToken}
96
+ </CapHeading>
97
+ );
98
+ }
99
+ return null;
100
+ })}
101
+ </CapRow>
102
+ {footerContent}
103
+ </div>
104
+ );
105
+ }
106
+
107
+ VarSegmentMessageEditor.propTypes = {
108
+ templateString: PropTypes.string,
109
+ valueMap: PropTypes.object,
110
+ onChange: PropTypes.func,
111
+ onFocus: PropTypes.func,
112
+ placeholderPrefix: PropTypes.string,
113
+ getPlaceholder: PropTypes.func,
114
+ wrapperClassName: PropTypes.string,
115
+ rowClassName: PropTypes.string,
116
+ headingClassName: PropTypes.string,
117
+ varRegex: PropTypes.object,
118
+ readOnly: PropTypes.bool,
119
+ disabled: PropTypes.bool,
120
+ footerContent: PropTypes.node,
121
+ /** Optional hint below a variable field (e.g. DLT `{#var#}` max length). */
122
+ renderVarFooter: PropTypes.func,
123
+ };
124
+
125
+ export default VarSegmentMessageEditor;
@@ -0,0 +1,46 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables';
2
+
3
+ /* Same look as RCS edit message block: background, spacing, text color */
4
+ .rcs_text_area_wrapper {
5
+ .rcs-edit-template-message-input {
6
+ background-color: $CAP_G10;
7
+ padding: $CAP_SPACE_12 $CAP_SPACE_16 $CAP_SPACE_16;
8
+ }
9
+
10
+ .rcs-edit-template-message-split {
11
+ margin: 0 0 $CAP_SPACE_04 0;
12
+ overflow: hidden;
13
+ text-overflow: ellipsis;
14
+ color: $FONT_COLOR_04;
15
+ font-weight: 500;
16
+ }
17
+
18
+ /* Variable chips: match RCS edit (white field, light border, 4px radius) */
19
+ .rcs-edit-template-message-input .ant-input,
20
+ .rcs-edit-template-message-input textarea.ant-input {
21
+ margin: 0 0 0.125rem 0;
22
+ border-radius: 0.25rem;
23
+ border: 0.0625rem solid $CAP_G07;
24
+ background-color: $CAP_WHITE;
25
+ overflow: hidden;
26
+ }
27
+
28
+ /* Small gap between tag border and the next line (static text) */
29
+ .rcs-edit-template-message-input :not(:first-child) {
30
+ margin-top: $CAP_SPACE_08;
31
+ }
32
+
33
+ .rcs-edit-template-message-input > *:last-child {
34
+ margin-bottom: 0;
35
+ }
36
+
37
+ .var-segment-message-editor__var-slot {
38
+ display: flex;
39
+ flex-direction: column;
40
+ width: 100%;
41
+ }
42
+ }
43
+
44
+ .var-segment-message-editor__read-only-value {
45
+ margin: 0;
46
+ }
@@ -30,7 +30,7 @@ jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
30
30
  import { Provider } from 'react-redux';
31
31
  import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
32
32
  import {
33
- render as rtlRender, screen, waitFor, within,
33
+ render as rtlRender, screen, waitFor, within, fireEvent,
34
34
  } from '@testing-library/react';
35
35
  import userEvent from '@testing-library/user-event';
36
36
  import '@testing-library/jest-dom';
@@ -47,6 +47,9 @@ import {
47
47
  STEPS,
48
48
  } from '../constants';
49
49
 
50
+ // antd Select/Dropdown interactions are slow under the parallel jest suite
51
+ jest.setTimeout(15000);
52
+
50
53
  let store;
51
54
 
52
55
  beforeAll(() => {
@@ -24,6 +24,10 @@ jest.mock('../../../../../services/localStorageApi', () => ({
24
24
  loadItem: jest.fn(),
25
25
  }));
26
26
 
27
+ // SenderDetails uses antd Select whose click→open→select chains are slow in the
28
+ // parallel jest suite; mirror the timeout used in SenderDetails.test.js.
29
+ jest.setTimeout(15000);
30
+
27
31
  const WABA_ID = 'waba-test-1';
28
32
 
29
33
  /** Typical `getDomainProperties` payloads per channel (merged when testing multi-channel). */
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import {
3
- render, screen, waitFor, within,
3
+ render, screen, waitFor, within, fireEvent,
4
4
  } from '@testing-library/react';
5
5
  import userEvent from '@testing-library/user-event';
6
6
  import '@testing-library/jest-dom';
@@ -183,9 +183,9 @@ const WAIT_OPTIONS = { timeout: 10000 };
183
183
 
184
184
  async function openSelectAndChoose(root, comboIndex, optionText) {
185
185
  const combos = within(root).getAllByRole('combobox');
186
- await userEvent.click(combos[comboIndex]);
186
+ fireEvent.mouseDown(combos[comboIndex]);
187
187
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
188
- await userEvent.click(await findVisibleSelectOption(optionText, WAIT_OPTIONS));
188
+ fireEvent.click(await findVisibleSelectOption(optionText, WAIT_OPTIONS));
189
189
  }
190
190
 
191
191
  describe('SenderDetails', () => {
@@ -334,7 +334,7 @@ describe('SenderDetails', () => {
334
334
  await waitFor(() => expect(within(root).getByText('beta@other.com')).toBeInTheDocument());
335
335
 
336
336
  const resetLinks = within(root).getAllByText('Reset');
337
- await userEvent.click(resetLinks[0]);
337
+ fireEvent.click(resetLinks[0]);
338
338
 
339
339
  await waitFor(() => {
340
340
  expect(within(root).getByText('alpha@brand.com')).toBeInTheDocument();
@@ -349,12 +349,12 @@ describe('SenderDetails', () => {
349
349
  await waitFor(() => expect(within(root).getByText('+1002003001')).toBeInTheDocument());
350
350
 
351
351
  const combos = within(root).getAllByRole('combobox');
352
- await userEvent.click(combos[1]);
352
+ fireEvent.mouseDown(combos[1]);
353
353
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
354
- await userEvent.click(await findVisibleSelectOption('+1002003002', WAIT_OPTIONS));
354
+ fireEvent.click(await findVisibleSelectOption('+1002003002', WAIT_OPTIONS));
355
355
 
356
356
  const resetLinks = within(root).getAllByText('Reset');
357
- await userEvent.click(resetLinks[1]);
357
+ fireEvent.click(resetLinks[1]);
358
358
 
359
359
  await waitFor(() => expect(within(root).getByText('+1002003001')).toBeInTheDocument());
360
360
  });
@@ -370,13 +370,13 @@ describe('SenderDetails', () => {
370
370
  await waitFor(() => expect(within(root).getByText('My Business')).toBeInTheDocument());
371
371
 
372
372
  const combos = within(root).getAllByRole('combobox');
373
- await userEvent.click(combos[combos.length - 1]);
373
+ fireEvent.mouseDown(combos[combos.length - 1]);
374
374
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
375
- await userEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
375
+ fireEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
376
376
 
377
377
  const saveBtn = within(root).getByRole('button', { name: /save changes/i });
378
378
  await waitFor(() => expect(saveBtn).not.toBeDisabled());
379
- await userEvent.click(saveBtn);
379
+ fireEvent.click(saveBtn);
380
380
 
381
381
  expect(onSave).toHaveBeenCalledWith(
382
382
  expect.objectContaining({
@@ -425,9 +425,9 @@ describe('SenderDetails', () => {
425
425
  await waitFor(() => expect(within(root).getByText('Gateway A')).toBeInTheDocument());
426
426
 
427
427
  const combos = within(root).getAllByRole('combobox');
428
- await userEvent.click(combos[0]);
428
+ fireEvent.mouseDown(combos[0]);
429
429
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
430
- await userEvent.click(await findVisibleSelectOption('Gateway B', WAIT_OPTIONS));
430
+ fireEvent.click(await findVisibleSelectOption('Gateway B', WAIT_OPTIONS));
431
431
 
432
432
  // The sender ID should now auto-update to Gateway B's sender
433
433
  await waitFor(() => expect(within(root).getByText('+222')).toBeInTheDocument());
@@ -444,9 +444,9 @@ describe('SenderDetails', () => {
444
444
 
445
445
  // Switch to Mail Beta domain
446
446
  const combos = within(root).getAllByRole('combobox');
447
- await userEvent.click(combos[0]);
447
+ fireEvent.mouseDown(combos[0]);
448
448
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
449
- await userEvent.click(await findVisibleSelectOption('Mail Beta', WAIT_OPTIONS));
449
+ fireEvent.click(await findVisibleSelectOption('Mail Beta', WAIT_OPTIONS));
450
450
 
451
451
  // Sender ID should auto-update to beta@other.com
452
452
  await waitFor(() => expect(within(root).getByText('beta@other.com')).toBeInTheDocument());
@@ -468,13 +468,13 @@ describe('SenderDetails', () => {
468
468
  // Change sender to enable save
469
469
  const combos = within(root).getAllByRole('combobox');
470
470
  const senderCombo = combos[combos.length - 1];
471
- await userEvent.click(senderCombo);
471
+ fireEvent.mouseDown(senderCombo);
472
472
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
473
- await userEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
473
+ fireEvent.click(await findVisibleSelectOption('+91900111333', WAIT_OPTIONS));
474
474
 
475
475
  const saveBtn = within(root).getByRole('button', { name: /save changes/i });
476
476
  await waitFor(() => expect(saveBtn).not.toBeDisabled());
477
- await userEvent.click(saveBtn);
477
+ fireEvent.click(saveBtn);
478
478
 
479
479
  // Should have called onSave with whatsappDomainId derived from the matched WABA domain
480
480
  expect(onSave).toHaveBeenCalledWith(
@@ -507,9 +507,9 @@ describe('SenderDetails', () => {
507
507
  // Change to an alternate sender
508
508
  const combos = within(root).getAllByRole('combobox');
509
509
  const senderCombo = combos[combos.length - 1];
510
- await userEvent.click(senderCombo);
510
+ fireEvent.mouseDown(senderCombo);
511
511
  await waitFor(() => expect(screen.getByRole('listbox')).toBeInTheDocument(), WAIT_OPTIONS);
512
- await userEvent.click(await findVisibleSelectOption('+viber-alt', WAIT_OPTIONS));
512
+ fireEvent.click(await findVisibleSelectOption('+viber-alt', WAIT_OPTIONS));
513
513
 
514
514
  // antd v6 also renders selected option in an aria-live polite region for
515
515
  // screen readers, so the text appears twice. Match only the visible select
@@ -521,7 +521,7 @@ describe('SenderDetails', () => {
521
521
  // Now click reset — for viberSenderId the else branch in handleReset is hit
522
522
  // (viberSenderId is not smsDomain/emailDomain, so getDefaultValueForField returns '')
523
523
  const resetLinks = within(root).getAllByText('Reset');
524
- await userEvent.click(resetLinks[resetLinks.length - 1]);
524
+ fireEvent.click(resetLinks[resetLinks.length - 1]);
525
525
  // After reset, the field goes to the default value returned by getDefaultValueForField
526
526
  // For viberSenderId this is '' (no specific default handler). The visible
527
527
  // select tile no longer shows +viber-alt; the aria-live polite span may
@@ -0,0 +1,27 @@
1
+ import React from 'react';
2
+ import classnames from 'classnames';
3
+ import { CAP_SPACE_32, CAP_SPACE_56, CAP_SPACE_64 } from '@capillarytech/cap-ui-library/styled/variables';
4
+
5
+ /* getSlideBoxWrapperMarginFromLiquidErrors only ever returns 0 or one of these three tokens
6
+ * (see embeddedSlideboxUtils.js) — mapped to static modifier classes so no inline style is needed. */
7
+ const WRAPPER_MARGIN_CLASS_BY_VALUE = {
8
+ [CAP_SPACE_32]: 'creatives-slide-box-wrapper--margin-32',
9
+ [CAP_SPACE_56]: 'creatives-slide-box-wrapper--margin-56',
10
+ [CAP_SPACE_64]: 'creatives-slide-box-wrapper--margin-64',
11
+ };
12
+
13
+ const CreativesSlideBoxWrapper = ({ slideBoxWrapperMargin, shouldApplyFooterMargin, children, className, ...rest }) => (
14
+ <section
15
+ className={classnames(
16
+ 'creatives-slide-box-wrapper',
17
+ WRAPPER_MARGIN_CLASS_BY_VALUE[slideBoxWrapperMargin],
18
+ { 'creatives-slide-box-wrapper--footer-margin': shouldApplyFooterMargin },
19
+ className,
20
+ )}
21
+ {...rest}
22
+ >
23
+ {children}
24
+ </section>
25
+ );
26
+
27
+ export default CreativesSlideBoxWrapper;
@@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
3
3
  import styled from 'styled-components';
4
4
  import get from 'lodash/get';
5
5
  import isEmpty from 'lodash/isEmpty';
6
+ import pick from 'lodash/pick';
6
7
  import cloneDeep from 'lodash/cloneDeep';
7
8
  import TemplatesV2 from '../TemplatesV2';
8
9
  import TemplatePreview from '../../v2Components/TemplatePreview';
@@ -25,6 +26,7 @@ import Viber from '../Viber';
25
26
  import Whatsapp from '../Whatsapp';
26
27
  import InApp from '../InApp';
27
28
  import Rcs from '../Rcs';
29
+ import { isRcsTextOnlyCardMediaType, resolveRcsCardPreviewStrings } from '../Rcs/utils';
28
30
  import { getWhatsappContent } from '../Whatsapp/utils';
29
31
  import * as commonUtil from '../../utils/common';
30
32
  import Zalo from '../Zalo';
@@ -180,6 +182,8 @@ export function SlideBoxContent(props) {
180
182
  isTestAndPreviewMode,
181
183
  onHtmlEditorValidationStateChange,
182
184
  } = props;
185
+ const localTemplatesConfig = props.localTemplatesConfig || pick(props, constants.LOCAL_TEMPLATE_CONFIG_KEYS);
186
+ const useLocalTemplates = !!get(localTemplatesConfig, 'useLocalTemplates');
183
187
  const type = (messageDetails.type || '').toLowerCase(); // type is context in get tags values : outbound | dvs | referral | loyalty | coupons
184
188
  const query = { type: !isFullMode && 'embedded', module: isFullMode ? 'default' : 'library', isEditFromCampaigns: (templateData || {}).isEditFromCampaigns};
185
189
  const creativesLocationProps = {
@@ -399,12 +403,37 @@ export function SlideBoxContent(props) {
399
403
  }
400
404
  case constants.RCS: {
401
405
  const template = cloneDeep(templateDataObject);
402
- const { description = "", media: { mediaUrl = "" } = {}, title = "", suggestions = [] } = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0]', {});
406
+ const cardPath = 'versions.base.content.RCS.rcsContent.cardContent[0]';
407
+ const card = get(template, cardPath, {}) || {};
408
+ const {
409
+ description = '',
410
+ media: { mediaUrl = '' } = {},
411
+ title = '',
412
+ mediaType: cardMediaType,
413
+ suggestions = [],
414
+ cardVarMapped: nestedCardVarMapped,
415
+ } = card;
416
+ const rootMirror = templateDataObject?.rcsCardVarMapped;
417
+ const nestedRecord =
418
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
419
+ ? nestedCardVarMapped
420
+ : {};
421
+ const rootRecord =
422
+ rootMirror != null && typeof rootMirror === 'object' ? rootMirror : {};
423
+ const mergedCardVarMapped = { ...rootRecord, ...nestedRecord };
424
+ const textOnlyCard = isRcsTextOnlyCardMediaType(cardMediaType);
425
+ const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
426
+ title,
427
+ description,
428
+ mergedCardVarMapped,
429
+ !isFullMode,
430
+ textOnlyCard,
431
+ );
403
432
  return {
404
433
  rcsPreviewContent: {
405
434
  rcsImageSrc: mediaUrl,
406
- rcsTitle: title,
407
- rcsDesc: description,
435
+ rcsTitle,
436
+ rcsDesc,
408
437
  ...(suggestions.length > 0 && {
409
438
  buttonText: suggestions[0]?.text,
410
439
  }),
@@ -426,11 +455,12 @@ export function SlideBoxContent(props) {
426
455
  getFormData,
427
456
  eventContextTags,
428
457
  waitEventContextTags,
458
+ handleClose,
429
459
  };
430
460
 
431
461
  return (
432
462
  <CreativesWrapper>
433
- {!isFullMode && slidBoxContent === 'templates' && (
463
+ {slidBoxContent === 'templates' && (!isFullMode || useLocalTemplates) && (
434
464
  <TemplatesV2
435
465
  isFullMode={isFullMode}
436
466
  onSelectTemplate={onSelectTemplate}
@@ -463,6 +493,7 @@ export function SlideBoxContent(props) {
463
493
  waitEventContextTags={waitEventContextTags}
464
494
  loyaltyMetaData={loyaltyMetaData}
465
495
  isLoyaltyModule={isLoyaltyModule}
496
+ localTemplatesConfig={localTemplatesConfig}
466
497
  />
467
498
  )}
468
499
  {isPreview && (
@@ -633,6 +664,7 @@ export function SlideBoxContent(props) {
633
664
  route={{ name: 'sms' }}
634
665
  isCreateSms={isCreateSms}
635
666
  isComponent
667
+ templateData={templateData}
636
668
  isGetFormData={isGetFormData}
637
669
  getFormSubscriptionData={getFormData}
638
670
  getLiquidTags={getLiquidTags}
@@ -1242,6 +1274,7 @@ export function SlideBoxContent(props) {
1242
1274
  )}
1243
1275
  {isCreateRcs && (<Rcs
1244
1276
  {...rcsCommonProps}
1277
+ templateData={templateData}
1245
1278
  showLiquidErrorInFooter={showLiquidErrorInFooter}
1246
1279
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
1247
1280
  handleTestAndPreview={handleTestAndPreview}
@@ -49,6 +49,8 @@ function SlideBoxFooter(props) {
49
49
  isAnonymousType = false,
50
50
  templateData = {},
51
51
  hasPersonalizationTokenError: hasPersonalizationTokenErrorProp = false,
52
+ /** When set (e.g. SMS library create), overrides `creativesTemplatesSave` (“Done”) for the primary button */
53
+ primarySaveButtonMessage,
52
54
  } = props;
53
55
  // Calculate if buttons should be disabled
54
56
  // Only apply validation state checks for EMAIL channel in HTML Editor mode (not BEE/DragDrop)
@@ -165,6 +167,12 @@ function SlideBoxFooter(props) {
165
167
  // Use prop from parent (FormBuilder flow) when available; else fall back to templateData check
166
168
  const hasPersonalizationTokenError = hasPersonalizationTokenErrorProp === true || (restrictPersonalization && hasPersonalizationTokens());
167
169
 
170
+ function getSaveButtonLabel() {
171
+ if (primarySaveButtonMessage) return <FormattedMessage {...primarySaveButtonMessage} />;
172
+ if (isFullMode) return getFullModeSaveBtn(slidBoxContent, isCreatingTemplate);
173
+ return <FormattedMessage {...messages.creativesTemplatesSave} />;
174
+ }
175
+
168
176
  return (
169
177
  <div className="template-footer-width">
170
178
  {shouldShowErrorInfoNote && (
@@ -187,11 +195,7 @@ function SlideBoxFooter(props) {
187
195
  onClick={onSave}
188
196
  disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons || hasPersonalizationTokenError}
189
197
  >
190
- {isFullMode ? (
191
- getFullModeSaveBtn(slidBoxContent, isCreatingTemplate)
192
- ) : (
193
- <FormattedMessage {...messages.creativesTemplatesSave} />
194
- )}
198
+ {getSaveButtonLabel()}
195
199
  </CapButton>
196
200
  {showTestAndPreviewButton && (
197
201
  <CapButton
@@ -264,6 +268,10 @@ SlideBoxFooter.propTypes = {
264
268
  templateData: PropTypes.object,
265
269
  formData: PropTypes.array,
266
270
  hasPersonalizationTokenError: PropTypes.bool,
271
+ primarySaveButtonMessage: PropTypes.shape({
272
+ id: PropTypes.string,
273
+ defaultMessage: PropTypes.string,
274
+ }),
267
275
  };
268
276
 
269
277
  SlideBoxFooter.defaultProps = {
@@ -291,5 +299,6 @@ SlideBoxFooter.defaultProps = {
291
299
  selectedEmailCreateMode: '',
292
300
  formData: [],
293
301
  hasPersonalizationTokenError: false,
302
+ primarySaveButtonMessage: undefined,
294
303
  };
295
304
  export default SlideBoxFooter;
@@ -16,6 +16,7 @@ import { isTraiDLTEnable } from '../../utils/common';
16
16
  import { formatString } from '../../utils/Formatter';
17
17
  import {
18
18
  CAP_SPACE_12,
19
+ CAP_SPACE_16,
19
20
  ICON_SIZE_M,
20
21
  } from '@capillarytech/cap-ui-library/styled/variables';
21
22
  import { WHATSAPP_HELP_DOC_LINK, JOURNEY } from './constants';
@@ -28,7 +29,7 @@ const StyledLabel = styled(CapLabelInline)`
28
29
  // the inherited font-size. Pin it to the medium icon size (24px) and center it so
29
30
  // the back arrow matches the header title instead of collapsing to a tiny glyph.
30
31
  const PrefixWrapper = styled.div`
31
- margin-right: 16px;
32
+ margin-right: ${CAP_SPACE_16};
32
33
  display: inline-flex;
33
34
  align-items: center;
34
35
  font-size: ${ICON_SIZE_M};
@@ -40,7 +41,25 @@ const renderData = (type, value, channel) => (
40
41
  );
41
42
 
42
43
  export function SlideBoxHeader(props) {
43
- const { slidBoxContent, templateData, onShowTemplates, creativesMode, isFullMode, showPrefix, shouldShowTemplateName, channel, templateNameRenderProp, weChatTemplateType, onWeChatMaptemplateStepChange, weChatMaptemplateStep, templateStep, smsRegister, handleClose, moduleType } = props;
44
+ const {
45
+ slidBoxContent,
46
+ templateData,
47
+ onShowTemplates,
48
+ creativesMode,
49
+ isFullMode,
50
+ showPrefix,
51
+ shouldShowTemplateName,
52
+ channel,
53
+ templateNameRenderProp,
54
+ weChatTemplateType,
55
+ onWeChatMaptemplateStepChange,
56
+ weChatMaptemplateStep,
57
+ templateStep,
58
+ smsRegister,
59
+ handleClose,
60
+ moduleType,
61
+ useLocalTemplates = false,
62
+ } = props;
44
63
  const showTemplateNameHeader = isFullMode && shouldShowTemplateName;
45
64
  const mapTemplateCreate = !showTemplateNameHeader && slidBoxContent === 'createTemplate' && weChatTemplateType === MAP_TEMPLATE && templateStep !== 'modeSelection';
46
65
  const isTraiDlt = isTraiDLTEnable(isFullMode, smsRegister);
@@ -61,7 +80,7 @@ export function SlideBoxHeader(props) {
61
80
  } = templateData || {};
62
81
  const templateName = whatsappTemplateName || templateData?.name;
63
82
  const showRcsTemplateName = channel.toLowerCase() === RCS && slidBoxContent === 'editTemplate' && !isFullMode && templateName;
64
- const showTemplateName = isWhatsappEdit || showRcsTemplateName || isZaloEdit;
83
+ const showTemplateName = isWhatsappEdit || isZaloEdit;
65
84
  const whatsappCategory = whatsappTemplateCategory || get(templateData, `versions.base.content.whatsapp.category`, '');
66
85
  const whatsappLanguageCode = whatsappTemplateLanguageCode || get(templateData, `versions.base.content.whatsapp.languages[0].language`, '');
67
86
 
@@ -88,6 +107,9 @@ export function SlideBoxHeader(props) {
88
107
  window.open(WHATSAPP_HELP_DOC_LINK, '_blank');
89
108
  };
90
109
 
110
+ const showCreativesTemplatesBackButton =
111
+ !isFullMode && (moduleType === JOURNEY || useLocalTemplates);
112
+
91
113
  return (
92
114
  <div key="creatives-container-slidebox-header-content">
93
115
  {slidBoxContent === 'templates' && !showTemplateNameHeader && (
@@ -96,7 +118,7 @@ export function SlideBoxHeader(props) {
96
118
  description={![NO_COMMUNICATION, FTP].includes(channel) &&
97
119
  <FormattedMessage {...messages.creativeTemplatesDesc} />
98
120
  }
99
- prefix={!isFullMode && moduleType === JOURNEY &&
121
+ prefix={showCreativesTemplatesBackButton &&
100
122
  <PrefixWrapper>
101
123
  <CapIcons.BackIcon onClick={handleClose} />
102
124
  </PrefixWrapper>
@@ -130,6 +152,12 @@ export function SlideBoxHeader(props) {
130
152
  <StyledLabel className='whatsapp-rcs-slidebox-template-name' type="label2">{templateName}</StyledLabel>
131
153
  </>
132
154
  ) : ""}
155
+ {showRcsTemplateName && (
156
+ <>
157
+ {renderData("label1", <FormattedMessage {...globalMessages.creativeNameLabel} />, channel)}
158
+ <StyledLabel className='whatsapp-rcs-slidebox-template-name' type="label2">{templateName}</StyledLabel>
159
+ </>
160
+ )}
133
161
  {isWhatsappEdit && (
134
162
  <>
135
163
  {renderData("label3", <FormattedMessage {...whatsppMsg.labelSeperator} />)}
@@ -142,7 +170,7 @@ export function SlideBoxHeader(props) {
142
170
  }
143
171
  </>
144
172
  }
145
- prefix={creativesMode !== 'edit' && !isFullMode && showPrefix &&
173
+ prefix={!isFullMode && showPrefix &&
146
174
  <PrefixWrapper>
147
175
  <CapIcons.BackIcon onClick={onShowTemplates} />
148
176
  </PrefixWrapper>
@@ -198,5 +226,8 @@ SlideBoxHeader.propTypes = {
198
226
  shouldShowTemplateName: PropTypes.bool,
199
227
  templateNameRenderProp: PropTypes.func,
200
228
  smsRegister: PropTypes.any,
229
+ handleClose: PropTypes.func,
230
+ moduleType: PropTypes.string,
231
+ useLocalTemplates: PropTypes.bool,
201
232
  };
202
233
  export default SlideBoxHeader;
@@ -73,3 +73,14 @@ export const ALLOWED_CHANNELS_FOR_ANONYMOUS = ['mobilepush', 'webpush'];
73
73
  export const ALL_CHANNELS_NEW = [
74
74
  'sms', 'email', 'whatsapp', 'facebook', 'line', 'viber', 'rcs', 'zalo', 'inapp', 'call_task', 'ftp',
75
75
  ];
76
+
77
+ export const LOCAL_TEMPLATE_CONFIG_KEYS = ['useLocalTemplates', 'localTemplates', 'localTemplatesLoading', 'localTemplatesFilterContent', 'localTemplatesUseSkeleton', 'localTemplatesOnPageChange'];
78
+
79
+ /** Keys passed from parents into `Templates` when using local SMS template list (extends `LOCAL_TEMPLATE_CONFIG_KEYS`). */
80
+ export const LOCAL_TEMPLATE_CONFIG_KEYS_FOR_PICK = [
81
+ ...LOCAL_TEMPLATE_CONFIG_KEYS,
82
+ 'localTemplatesLoadingTip',
83
+ 'localTemplatesFooterContent',
84
+ ];
85
+
86
+ export const DEFAULT_SMS_TEMPLATE_NAME = 'Campaign message SMS content';