@capillarytech/creatives-library 8.0.319 → 8.0.320

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 (106) hide show
  1. package/constants/unified.js +14 -0
  2. package/package.json +1 -1
  3. package/utils/templateVarUtils.js +172 -0
  4. package/utils/tests/templateVarUtils.test.js +160 -0
  5. package/v2Components/CapTagList/index.js +10 -0
  6. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  7. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  8. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  9. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  13. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  15. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  17. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  18. package/v2Components/CommonTestAndPreview/index.js +693 -155
  19. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  20. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  21. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  22. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  23. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  24. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  25. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  26. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  27. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  28. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  29. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  30. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  31. package/v2Components/FormBuilder/index.js +7 -1
  32. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  33. package/v2Components/SmsFallback/constants.js +73 -0
  34. package/v2Components/SmsFallback/index.js +956 -0
  35. package/v2Components/SmsFallback/index.scss +265 -0
  36. package/v2Components/SmsFallback/messages.js +78 -0
  37. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  38. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  39. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  40. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  41. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  42. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  43. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  44. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  45. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  46. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  47. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  48. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  49. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  50. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  51. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  52. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  53. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  54. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  55. package/v2Containers/CreativesContainer/constants.js +9 -0
  56. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  57. package/v2Containers/CreativesContainer/index.js +289 -99
  58. package/v2Containers/CreativesContainer/index.scss +51 -1
  59. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  60. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  61. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  62. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  63. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  64. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  65. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  66. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  67. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  68. package/v2Containers/Rcs/constants.js +32 -1
  69. package/v2Containers/Rcs/index.js +950 -873
  70. package/v2Containers/Rcs/index.scss +85 -6
  71. package/v2Containers/Rcs/messages.js +10 -1
  72. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  73. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  74. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  75. package/v2Containers/Rcs/tests/index.test.js +41 -38
  76. package/v2Containers/Rcs/tests/mockData.js +38 -0
  77. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  78. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  79. package/v2Containers/Rcs/utils.js +358 -10
  80. package/v2Containers/Sms/Create/index.js +81 -36
  81. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  82. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  83. package/v2Containers/SmsTrai/Create/index.js +9 -4
  84. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  85. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  86. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  87. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  88. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  89. package/v2Containers/SmsWrapper/index.js +37 -8
  90. package/v2Containers/TagList/index.js +6 -0
  91. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  92. package/v2Containers/Templates/_templates.scss +61 -2
  93. package/v2Containers/Templates/actions.js +11 -0
  94. package/v2Containers/Templates/constants.js +2 -0
  95. package/v2Containers/Templates/index.js +90 -40
  96. package/v2Containers/Templates/sagas.js +57 -12
  97. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  98. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  99. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  100. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  101. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  102. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  103. package/v2Containers/TemplatesV2/index.js +86 -23
  104. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  105. package/v2Containers/Whatsapp/index.js +3 -20
  106. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +578 -34
@@ -0,0 +1,92 @@
1
+ import { useState, useCallback, useRef } from 'react';
2
+
3
+ /**
4
+ * @param {Object} options
5
+ * @param {(params: { page: number, search: string, reset: boolean }) => Promise<{ templates: Array, totalCount: number }>} options.fetchTemplates
6
+ * @param {number} [options.perPage=25]
7
+ */
8
+ export function useLocalTemplateList({ fetchTemplates, perPage = 25 }) {
9
+ const [listData, setListData] = useState({ templates: [], totalCount: 0 });
10
+ const [loading, setLoading] = useState(false);
11
+ const [page, setPage] = useState(1);
12
+ const [search, setSearchState] = useState('');
13
+ const searchRef = useRef('');
14
+ /** Drops stale responses when a newer fetch starts (search / reset while a request is in flight). */
15
+ const fetchGenerationRef = useRef(0);
16
+ const setSearch = useCallback((value) => {
17
+ const term = typeof value === 'string' ? value : '';
18
+ searchRef.current = term;
19
+ setSearchState(term);
20
+ }, []);
21
+ const lastFetchFullPageRef = useRef(false);
22
+
23
+ const { templates = [], totalCount = 0 } = listData ?? {};
24
+ const hasKnownTotal = (totalCount ?? 0) > 0;
25
+ const hasMoreByTotal = (totalCount ?? 0) > (templates?.length ?? 0);
26
+ const hasMoreByFullPage =
27
+ !hasKnownTotal && lastFetchFullPageRef.current && (templates?.length ?? 0) > 0;
28
+ const canLoadMore = (hasMoreByTotal || hasMoreByFullPage) && !loading;
29
+
30
+ const runFetch = useCallback(
31
+ async ({ page: p = 1, reset = true, search: searchTerm } = {}) => {
32
+ const term = searchTerm !== undefined ? searchTerm : searchRef.current;
33
+ const gen = ++fetchGenerationRef.current;
34
+ setLoading(true);
35
+ try {
36
+ const result = await fetchTemplates({ page: p, search: term, reset });
37
+ if (gen !== fetchGenerationRef.current) {
38
+ return;
39
+ }
40
+ const nextTemplates = result?.templates ?? [];
41
+ const nextTotalCount = result?.totalCount ?? 0;
42
+ lastFetchFullPageRef.current = nextTemplates.length >= perPage;
43
+ setListData((prev) => ({
44
+ templates: reset ? nextTemplates : [...(prev.templates || []), ...nextTemplates],
45
+ totalCount: nextTotalCount > 0 ? nextTotalCount : (reset ? 0 : prev.totalCount),
46
+ }));
47
+ setPage(p);
48
+ } catch (e) {
49
+ if (gen !== fetchGenerationRef.current) {
50
+ return;
51
+ }
52
+ lastFetchFullPageRef.current = false;
53
+ if (reset) {
54
+ setListData({ templates: [], totalCount: 0 });
55
+ setPage(1);
56
+ }
57
+ } finally {
58
+ if (gen === fetchGenerationRef.current) {
59
+ setLoading(false);
60
+ }
61
+ }
62
+ },
63
+ [fetchTemplates, perPage]
64
+ );
65
+
66
+ const loadMore = useCallback(() => {
67
+ if (!canLoadMore) return;
68
+ runFetch({ page: page + 1, reset: false, search: searchRef.current });
69
+ }, [canLoadMore, page, runFetch]);
70
+
71
+ const reset = useCallback(
72
+ (searchTerm) => {
73
+ const term = searchTerm !== undefined ? searchTerm : searchRef.current;
74
+ setSearch(term);
75
+ lastFetchFullPageRef.current = false;
76
+ runFetch({ page: 1, reset: true, search: term });
77
+ },
78
+ [runFetch, setSearch]
79
+ );
80
+
81
+ return {
82
+ templates,
83
+ totalCount,
84
+ loading,
85
+ page,
86
+ search,
87
+ setSearch,
88
+ loadMore,
89
+ reset,
90
+ canLoadMore,
91
+ };
92
+ }
@@ -18,7 +18,7 @@ import injectReducer from '../../utils/injectReducer';
18
18
  import injectSaga from '../../utils/injectSaga';
19
19
 
20
20
  import CommonTestAndPreview from '../CommonTestAndPreview';
21
- import { CHANNELS } from '../CommonTestAndPreview/constants';
21
+ import { CHANNELS, RCS_SMS_FALLBACK_VAR_MAPPED_PROP } from '../CommonTestAndPreview/constants';
22
22
  import * as commonTestAndPreviewActions from '../CommonTestAndPreview/actions';
23
23
  import { commonTestAndPreviewSaga } from '../CommonTestAndPreview/sagas';
24
24
  import commonTestAndPreviewReducer from '../CommonTestAndPreview/reducer';
@@ -78,6 +78,12 @@ TestAndPreviewSlidebox.propTypes = {
78
78
  content: PropTypes.string,
79
79
  beeInstance: PropTypes.object,
80
80
  currentTab: PropTypes.number,
81
+ smsFallbackContent: PropTypes.shape({
82
+ templateContent: PropTypes.string,
83
+ senderId: PropTypes.string,
84
+ templateName: PropTypes.string,
85
+ [RCS_SMS_FALLBACK_VAR_MAPPED_PROP]: PropTypes.object,
86
+ }),
81
87
  // Redux props are passed through
82
88
  actions: PropTypes.object.isRequired,
83
89
  extractedTags: PropTypes.array.isRequired,
@@ -113,6 +119,7 @@ TestAndPreviewSlidebox.defaultProps = {
113
119
  wecrmAccounts: [],
114
120
  isLoadingSenderDetails: false,
115
121
  orgUnitId: -1,
122
+ smsFallbackContent: null,
116
123
  };
117
124
 
118
125
  const mapStateToProps = createStructuredSelector({
@@ -34,6 +34,7 @@ import {
34
34
  GET_PREFILLED_VALUES_SUCCESS,
35
35
  GET_PREFILLED_VALUES_FAILURE,
36
36
  } from './constants';
37
+ import { extractPreviewFromLiquidResponse } from '../CommonTestAndPreview/previewApiUtils';
37
38
 
38
39
  // Search Customers Saga
39
40
  export function* searchCustomersSaga(action) {
@@ -80,11 +81,12 @@ export function* updatePreviewSaga(action) {
80
81
  const customValues = action.payload.resolvedTags;
81
82
 
82
83
  const response = yield call(Api.updateEmailPreview, action.payload);
83
- if (response?.data) {
84
+ const previewPayload = extractPreviewFromLiquidResponse(response);
85
+ if (previewPayload) {
84
86
  yield put({
85
87
  type: UPDATE_PREVIEW_SUCCESS,
86
88
  payload: {
87
- previewData: response.data,
89
+ previewData: previewPayload,
88
90
  customValues, // Pass custom values to be preserved
89
91
  },
90
92
  });
@@ -221,8 +223,13 @@ export function* createMessageMetaSaga(action) {
221
223
  export function* getPrefilledValuesSaga(action) {
222
224
  try {
223
225
  const response = yield call(Api.updateEmailPreview, action.payload);
224
- if (response?.data) {
225
- yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: response?.data?.resolvedTagValues } });
226
+ const body =
227
+ response?.data !== undefined && response?.data !== null
228
+ ? response.data
229
+ : response;
230
+ const resolvedTagValues = body?.resolvedTagValues;
231
+ if (resolvedTagValues != null) {
232
+ yield put({ type: GET_PREFILLED_VALUES_SUCCESS, payload: { values: resolvedTagValues } });
226
233
  } else {
227
234
  yield put({ type: GET_PREFILLED_VALUES_FAILURE, payload: { error: response.error || 'Failed to fetch prefilled values' } });
228
235
  }
@@ -136,7 +136,9 @@ describe('TestAndPreviewSlidebox Sagas', () => {
136
136
  describe('updatePreviewSaga', () => {
137
137
  it('should handle successful preview update', () => {
138
138
  const mockResponse = {
139
- data: 'Test Preview Data',
139
+ data: {
140
+ resolvedBody: 'Test Preview Data',
141
+ },
140
142
  };
141
143
  const customValues = { test: 'value' };
142
144
  return expectSaga(sagas.updatePreviewSaga, {
@@ -0,0 +1,2 @@
1
+ /** Default prefix before variable name in variable-slot placeholders. */
2
+ export const VAR_SEGMENT_PLACEHOLDER_PREFIX = 'enter the value for ';
@@ -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
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Styled wrapper for CapSlideBox used by CreativesContainer and RCS SMS fallback
3
+ * so header/content/footer margins match.
4
+ */
5
+ import styled from 'styled-components';
6
+ import { CAP_SPACE_16 } from '@capillarytech/cap-ui-library/styled/variables';
7
+
8
+ const CreativesSlideBoxWrapper = styled.div`
9
+ .cap-slide-box-v2-container {
10
+ /*
11
+ * Liquid-error spacing must stay *inside* the content column. margin-bottom on
12
+ * .slidebox-content-container added to the in-flow height past 100vh, so the outer
13
+ * .cap-slide-box-v2-container (overflow-y: auto in cap-ui) gained a second scrollbar.
14
+ */
15
+ .slidebox-header {
16
+ margin-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
17
+ padding: 0 rem;
18
+ &.has-footer {
19
+ overflow-x: hidden;
20
+ }
21
+ }
22
+ .slidebox-content-container {
23
+ margin-bottom: 0;
24
+ padding: 0 rem;
25
+ padding-bottom: ${({ slideBoxWrapperMargin }) => `${slideBoxWrapperMargin}`};
26
+ box-sizing: border-box;
27
+ &.has-footer {
28
+ overflow-x: hidden;
29
+ }
30
+ }
31
+ .slidebox-footer {
32
+ /* Only apply margin-bottom to footer when ErrorInfoNote is shown in footer (BEE editor) */
33
+ /* For HTML Editor, errors are shown in ValidationErrorDisplay (inside content area), so no footer margin needed */
34
+ margin-bottom: ${({ shouldApplyFooterMargin }) => (shouldApplyFooterMargin ? `${CAP_SPACE_16}` : '0')};
35
+ padding: 0 rem;
36
+ &.has-footer {
37
+ overflow-x: hidden;
38
+ }
39
+ }
40
+ }
41
+ `;
42
+
43
+ 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';
@@ -179,6 +181,8 @@ export function SlideBoxContent(props) {
179
181
  isTestAndPreviewMode,
180
182
  onHtmlEditorValidationStateChange,
181
183
  } = props;
184
+ const localTemplatesConfig = props.localTemplatesConfig || pick(props, constants.LOCAL_TEMPLATE_CONFIG_KEYS);
185
+ const useLocalTemplates = !!get(localTemplatesConfig, 'useLocalTemplates');
182
186
  const type = (messageDetails.type || '').toLowerCase(); // type is context in get tags values : outbound | dvs | referral | loyalty | coupons
183
187
  const query = { type: !isFullMode && 'embedded', module: isFullMode ? 'default' : 'library', isEditFromCampaigns: (templateData || {}).isEditFromCampaigns};
184
188
  const creativesLocationProps = {
@@ -398,12 +402,37 @@ export function SlideBoxContent(props) {
398
402
  }
399
403
  case constants.RCS: {
400
404
  const template = cloneDeep(templateDataObject);
401
- const { description = "", media: { mediaUrl = "" } = {}, title = "", suggestions = [] } = get(template, 'versions.base.content.RCS.rcsContent.cardContent[0]', {});
405
+ const cardPath = 'versions.base.content.RCS.rcsContent.cardContent[0]';
406
+ const card = get(template, cardPath, {}) || {};
407
+ const {
408
+ description = '',
409
+ media: { mediaUrl = '' } = {},
410
+ title = '',
411
+ mediaType: cardMediaType,
412
+ suggestions = [],
413
+ cardVarMapped: nestedCardVarMapped,
414
+ } = card;
415
+ const rootMirror = templateDataObject?.rcsCardVarMapped;
416
+ const nestedRecord =
417
+ nestedCardVarMapped != null && typeof nestedCardVarMapped === 'object'
418
+ ? nestedCardVarMapped
419
+ : {};
420
+ const rootRecord =
421
+ rootMirror != null && typeof rootMirror === 'object' ? rootMirror : {};
422
+ const mergedCardVarMapped = { ...rootRecord, ...nestedRecord };
423
+ const textOnlyCard = isRcsTextOnlyCardMediaType(cardMediaType);
424
+ const { rcsTitle, rcsDesc } = resolveRcsCardPreviewStrings(
425
+ title,
426
+ description,
427
+ mergedCardVarMapped,
428
+ !isFullMode,
429
+ textOnlyCard,
430
+ );
402
431
  return {
403
432
  rcsPreviewContent: {
404
433
  rcsImageSrc: mediaUrl,
405
- rcsTitle: title,
406
- rcsDesc: description,
434
+ rcsTitle,
435
+ rcsDesc,
407
436
  ...(suggestions.length > 0 && {
408
437
  buttonText: suggestions[0]?.text,
409
438
  }),
@@ -428,7 +457,7 @@ export function SlideBoxContent(props) {
428
457
 
429
458
  return (
430
459
  <CreativesWrapper>
431
- {!isFullMode && slidBoxContent === 'templates' && (
460
+ {slidBoxContent === 'templates' && (!isFullMode || useLocalTemplates) && (
432
461
  <TemplatesV2
433
462
  isFullMode={isFullMode}
434
463
  onSelectTemplate={onSelectTemplate}
@@ -460,6 +489,7 @@ export function SlideBoxContent(props) {
460
489
  eventContextTags={eventContextTags}
461
490
  loyaltyMetaData={loyaltyMetaData}
462
491
  isLoyaltyModule={isLoyaltyModule}
492
+ localTemplatesConfig={localTemplatesConfig}
463
493
  />
464
494
  )}
465
495
  {isPreview && (
@@ -628,6 +658,7 @@ export function SlideBoxContent(props) {
628
658
  route={{ name: 'sms' }}
629
659
  isCreateSms={isCreateSms}
630
660
  isComponent
661
+ templateData={templateData}
631
662
  isGetFormData={isGetFormData}
632
663
  getFormSubscriptionData={getFormData}
633
664
  getLiquidTags={getLiquidTags}
@@ -1203,6 +1234,7 @@ export function SlideBoxContent(props) {
1203
1234
  )}
1204
1235
  {isCreateRcs && (<Rcs
1205
1236
  {...rcsCommonProps}
1237
+ templateData={templateData}
1206
1238
  showLiquidErrorInFooter={showLiquidErrorInFooter}
1207
1239
  showTestAndPreviewSlidebox={showTestAndPreviewSlidebox}
1208
1240
  handleTestAndPreview={handleTestAndPreview}
@@ -48,6 +48,8 @@ function SlideBoxFooter(props) {
48
48
  isAnonymousType = false,
49
49
  templateData = {},
50
50
  hasPersonalizationTokenError: hasPersonalizationTokenErrorProp = false,
51
+ /** When set (e.g. SMS library create), overrides `creativesTemplatesSave` (“Done”) for the primary button */
52
+ primarySaveButtonMessage,
51
53
  } = props;
52
54
  // Calculate if buttons should be disabled
53
55
  // Only apply validation state checks for EMAIL channel in HTML Editor mode (not BEE/DragDrop)
@@ -186,7 +188,9 @@ function SlideBoxFooter(props) {
186
188
  onClick={onSave}
187
189
  disabled={isTemplateNameEmpty || fetchingCmsData || shouldDisableButtons || hasPersonalizationTokenError}
188
190
  >
189
- {isFullMode ? (
191
+ {primarySaveButtonMessage ? (
192
+ <FormattedMessage {...primarySaveButtonMessage} />
193
+ ) : isFullMode ? (
190
194
  getFullModeSaveBtn(slidBoxContent, isCreatingTemplate)
191
195
  ) : (
192
196
  <FormattedMessage {...messages.creativesTemplatesSave} />
@@ -262,6 +266,10 @@ SlideBoxFooter.propTypes = {
262
266
  templateData: PropTypes.object,
263
267
  formData: PropTypes.array,
264
268
  hasPersonalizationTokenError: PropTypes.bool,
269
+ primarySaveButtonMessage: PropTypes.shape({
270
+ id: PropTypes.string,
271
+ defaultMessage: PropTypes.string,
272
+ }),
265
273
  };
266
274
 
267
275
  SlideBoxFooter.defaultProps = {
@@ -289,5 +297,6 @@ SlideBoxFooter.defaultProps = {
289
297
  selectedEmailCreateMode: '',
290
298
  formData: [],
291
299
  hasPersonalizationTokenError: false,
300
+ primarySaveButtonMessage: undefined,
292
301
  };
293
302
  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
  } from '@capillarytech/cap-ui-library/styled/variables';
20
21
  import { WHATSAPP_HELP_DOC_LINK, JOURNEY } from './constants';
21
22
 
@@ -24,7 +25,7 @@ const StyledLabel = styled(CapLabelInline)`
24
25
  margin-right: ${CAP_SPACE_12};
25
26
  `;
26
27
  const PrefixWrapper = styled.div`
27
- margin-right: 16px;
28
+ margin-right: ${CAP_SPACE_16};
28
29
  `;
29
30
  const renderData = (type, value, channel) => (
30
31
  <StyledLabel className={channel?.toLowerCase() === ZALO ? 'zalo-template-name-spacing' : ''} type={type}>
@@ -33,7 +34,25 @@ const renderData = (type, value, channel) => (
33
34
  );
34
35
 
35
36
  export function SlideBoxHeader(props) {
36
- const { slidBoxContent, templateData, onShowTemplates, creativesMode, isFullMode, showPrefix, shouldShowTemplateName, channel, templateNameRenderProp, weChatTemplateType, onWeChatMaptemplateStepChange, weChatMaptemplateStep, templateStep, smsRegister, handleClose, moduleType } = props;
37
+ const {
38
+ slidBoxContent,
39
+ templateData,
40
+ onShowTemplates,
41
+ creativesMode,
42
+ isFullMode,
43
+ showPrefix,
44
+ shouldShowTemplateName,
45
+ channel,
46
+ templateNameRenderProp,
47
+ weChatTemplateType,
48
+ onWeChatMaptemplateStepChange,
49
+ weChatMaptemplateStep,
50
+ templateStep,
51
+ smsRegister,
52
+ handleClose,
53
+ moduleType,
54
+ useLocalTemplates = false,
55
+ } = props;
37
56
  const showTemplateNameHeader = isFullMode && shouldShowTemplateName;
38
57
  const mapTemplateCreate = !showTemplateNameHeader && slidBoxContent === 'createTemplate' && weChatTemplateType === MAP_TEMPLATE && templateStep !== 'modeSelection';
39
58
  const isTraiDlt = isTraiDLTEnable(isFullMode, smsRegister);
@@ -81,6 +100,9 @@ export function SlideBoxHeader(props) {
81
100
  window.open(WHATSAPP_HELP_DOC_LINK, '_blank');
82
101
  };
83
102
 
103
+ const showCreativesTemplatesBackButton =
104
+ !isFullMode && (moduleType === JOURNEY || useLocalTemplates);
105
+
84
106
  return (
85
107
  <div key="creatives-container-slidebox-header-content">
86
108
  {slidBoxContent === 'templates' && !showTemplateNameHeader && (
@@ -89,7 +111,7 @@ export function SlideBoxHeader(props) {
89
111
  description={![NO_COMMUNICATION, FTP].includes(channel) &&
90
112
  <FormattedMessage {...messages.creativeTemplatesDesc} />
91
113
  }
92
- prefix={!isFullMode && moduleType === JOURNEY &&
114
+ prefix={showCreativesTemplatesBackButton &&
93
115
  <PrefixWrapper>
94
116
  <CapIcons.backIcon onClick={handleClose} />
95
117
  </PrefixWrapper>
@@ -135,7 +157,7 @@ export function SlideBoxHeader(props) {
135
157
  }
136
158
  </>
137
159
  }
138
- prefix={creativesMode !== 'edit' && !isFullMode && showPrefix &&
160
+ prefix={!isFullMode && showPrefix &&
139
161
  <PrefixWrapper>
140
162
  <CapIcons.backIcon onClick={onShowTemplates} />
141
163
  </PrefixWrapper>
@@ -191,5 +213,8 @@ SlideBoxHeader.propTypes = {
191
213
  shouldShowTemplateName: PropTypes.bool,
192
214
  templateNameRenderProp: PropTypes.func,
193
215
  smsRegister: PropTypes.any,
216
+ handleClose: PropTypes.func,
217
+ moduleType: PropTypes.string,
218
+ useLocalTemplates: PropTypes.bool,
194
219
  };
195
220
  export default SlideBoxHeader;
@@ -67,3 +67,12 @@ export const ALLOWED_CHANNELS_FOR_ANONYMOUS = ['mobilepush', 'webpush'];
67
67
  export const ALL_CHANNELS_NEW = [
68
68
  'sms', 'email', 'whatsapp', 'facebook', 'line', 'viber', 'rcs', 'zalo', 'inapp', 'call_task', 'ftp',
69
69
  ];
70
+
71
+ export const LOCAL_TEMPLATE_CONFIG_KEYS = ['useLocalTemplates', 'localTemplates', 'localTemplatesLoading', 'localTemplatesFilterContent', 'localTemplatesUseSkeleton', 'localTemplatesOnPageChange'];
72
+
73
+ /** Keys passed from parents into `Templates` when using local SMS template list (extends `LOCAL_TEMPLATE_CONFIG_KEYS`). */
74
+ export const LOCAL_TEMPLATE_CONFIG_KEYS_FOR_PICK = [
75
+ ...LOCAL_TEMPLATE_CONFIG_KEYS,
76
+ 'localTemplatesLoadingTip',
77
+ 'localTemplatesFooterContent',
78
+ ];