@capillarytech/creatives-library 8.0.318 → 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 (139) hide show
  1. package/constants/unified.js +15 -0
  2. package/package.json +1 -1
  3. package/services/api.js +6 -0
  4. package/services/tests/api.test.js +7 -0
  5. package/utils/common.js +6 -1
  6. package/utils/templateVarUtils.js +172 -0
  7. package/utils/tests/templateVarUtils.test.js +160 -0
  8. package/v2Components/CapTagList/index.js +10 -0
  9. package/v2Components/CommonTestAndPreview/CustomValuesEditor.js +70 -49
  10. package/v2Components/CommonTestAndPreview/DeliverySettings/DeliverySettings.scss +8 -2
  11. package/v2Components/CommonTestAndPreview/DeliverySettings/ModifyDeliverySettings.js +207 -21
  12. package/v2Components/CommonTestAndPreview/DeliverySettings/constants.js +16 -0
  13. package/v2Components/CommonTestAndPreview/DeliverySettings/index.js +85 -10
  14. package/v2Components/CommonTestAndPreview/DeliverySettings/messages.js +30 -0
  15. package/v2Components/CommonTestAndPreview/DeliverySettings/utils/parseSenderDetailsResponse.js +79 -11
  16. package/v2Components/CommonTestAndPreview/SendTestMessage.js +11 -5
  17. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +20 -1
  18. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +133 -4
  19. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +12 -0
  20. package/v2Components/CommonTestAndPreview/constants.js +38 -0
  21. package/v2Components/CommonTestAndPreview/index.js +693 -155
  22. package/v2Components/CommonTestAndPreview/messages.js +41 -3
  23. package/v2Components/CommonTestAndPreview/previewApiUtils.js +59 -0
  24. package/v2Components/CommonTestAndPreview/sagas.js +15 -6
  25. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +352 -0
  26. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/ModifyDeliverySettings.test.js +269 -1
  27. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/index.test.js +118 -5
  28. package/v2Components/CommonTestAndPreview/tests/DeliverySettings/utils/parseSenderDetailsResponse.test.js +341 -0
  29. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +25 -4
  30. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +199 -1
  31. package/v2Components/CommonTestAndPreview/tests/index.test.js +132 -4
  32. package/v2Components/CommonTestAndPreview/tests/previewApiUtils.test.js +67 -0
  33. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +2 -2
  34. package/v2Components/FormBuilder/index.js +7 -1
  35. package/v2Components/SmsFallback/SmsFallbackLocalSelector.js +87 -0
  36. package/v2Components/SmsFallback/constants.js +73 -0
  37. package/v2Components/SmsFallback/index.js +956 -0
  38. package/v2Components/SmsFallback/index.scss +265 -0
  39. package/v2Components/SmsFallback/messages.js +78 -0
  40. package/v2Components/SmsFallback/smsFallbackUtils.js +107 -0
  41. package/v2Components/SmsFallback/tests/SmsFallbackLocalSelector.test.js +50 -0
  42. package/v2Components/SmsFallback/tests/rcsSmsFallback.acceptance.test.js +147 -0
  43. package/v2Components/SmsFallback/tests/smsFallbackHandlers.test.js +304 -0
  44. package/v2Components/SmsFallback/tests/smsFallbackUi.test.js +197 -0
  45. package/v2Components/SmsFallback/tests/smsFallbackUtils.test.js +261 -0
  46. package/v2Components/SmsFallback/tests/useLocalTemplateList.test.js +422 -0
  47. package/v2Components/SmsFallback/useLocalTemplateList.js +92 -0
  48. package/v2Components/TestAndPreviewSlidebox/index.js +8 -1
  49. package/v2Components/TestAndPreviewSlidebox/sagas.js +11 -4
  50. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +3 -1
  51. package/v2Components/VarSegmentMessageEditor/constants.js +2 -0
  52. package/v2Components/VarSegmentMessageEditor/index.js +125 -0
  53. package/v2Components/VarSegmentMessageEditor/index.scss +46 -0
  54. package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
  55. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
  56. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
  57. package/v2Containers/CommunicationFlow/constants.js +200 -0
  58. package/v2Containers/CommunicationFlow/index.js +102 -0
  59. package/v2Containers/CommunicationFlow/messages.js +346 -0
  60. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
  61. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
  62. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
  63. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
  64. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
  65. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
  66. package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
  67. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
  68. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
  69. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
  70. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
  71. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
  72. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
  73. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
  74. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
  75. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
  76. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
  77. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
  78. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
  79. package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
  80. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
  81. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
  82. package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
  83. package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
  84. package/v2Containers/CreativesContainer/CreativesSlideBoxWrapper.js +43 -0
  85. package/v2Containers/CreativesContainer/SlideBoxContent.js +36 -4
  86. package/v2Containers/CreativesContainer/SlideBoxFooter.js +10 -1
  87. package/v2Containers/CreativesContainer/SlideBoxHeader.js +29 -4
  88. package/v2Containers/CreativesContainer/constants.js +12 -0
  89. package/v2Containers/CreativesContainer/embeddedSlideboxUtils.js +67 -0
  90. package/v2Containers/CreativesContainer/index.js +289 -99
  91. package/v2Containers/CreativesContainer/index.scss +51 -1
  92. package/v2Containers/CreativesContainer/tests/SlideBoxContent.localTemplates.test.js +90 -0
  93. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +104 -0
  94. package/v2Containers/CreativesContainer/tests/SlideBoxHeader.test.js +110 -0
  95. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +8 -0
  96. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxHeader.test.js.snap +363 -0
  97. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +20 -10
  98. package/v2Containers/CreativesContainer/tests/embeddedSlideboxUtils.test.js +258 -0
  99. package/v2Containers/CreativesContainer/tests/index.test.js +71 -9
  100. package/v2Containers/CreativesContainer/tests/useLocalTemplatesProp.test.js +125 -0
  101. package/v2Containers/Rcs/constants.js +32 -1
  102. package/v2Containers/Rcs/index.js +950 -873
  103. package/v2Containers/Rcs/index.scss +85 -6
  104. package/v2Containers/Rcs/messages.js +10 -1
  105. package/v2Containers/Rcs/rcsLibraryHydrationUtils.js +205 -0
  106. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +40834 -1963
  107. package/v2Containers/Rcs/tests/__snapshots__/utils.test.js.snap +0 -5
  108. package/v2Containers/Rcs/tests/index.test.js +41 -38
  109. package/v2Containers/Rcs/tests/mockData.js +38 -0
  110. package/v2Containers/Rcs/tests/rcsLibraryHydrationUtils.test.js +251 -0
  111. package/v2Containers/Rcs/tests/utils.test.js +379 -1
  112. package/v2Containers/Rcs/utils.js +358 -10
  113. package/v2Containers/Sms/Create/index.js +81 -36
  114. package/v2Containers/Sms/smsFormDataHelpers.js +67 -0
  115. package/v2Containers/Sms/tests/smsFormDataHelpers.test.js +253 -0
  116. package/v2Containers/SmsTrai/Create/index.js +9 -4
  117. package/v2Containers/SmsTrai/Edit/constants.js +2 -0
  118. package/v2Containers/SmsTrai/Edit/index.js +609 -128
  119. package/v2Containers/SmsTrai/Edit/index.scss +121 -0
  120. package/v2Containers/SmsTrai/Edit/messages.js +9 -4
  121. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4327 -2374
  122. package/v2Containers/SmsWrapper/index.js +37 -8
  123. package/v2Containers/TagList/index.js +6 -0
  124. package/v2Containers/Templates/TemplatesActionBar.js +101 -0
  125. package/v2Containers/Templates/_templates.scss +61 -2
  126. package/v2Containers/Templates/actions.js +11 -0
  127. package/v2Containers/Templates/constants.js +2 -0
  128. package/v2Containers/Templates/index.js +90 -40
  129. package/v2Containers/Templates/sagas.js +57 -12
  130. package/v2Containers/Templates/tests/TemplatesActionBar.test.js +120 -0
  131. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +1043 -1079
  132. package/v2Containers/Templates/tests/sagas.test.js +193 -12
  133. package/v2Containers/Templates/tests/smsTemplatesListApi.test.js +180 -0
  134. package/v2Containers/Templates/utils/smsTemplatesListApi.js +79 -0
  135. package/v2Containers/TemplatesV2/TemplatesV2.style.js +72 -1
  136. package/v2Containers/TemplatesV2/index.js +86 -23
  137. package/v2Containers/TemplatesV2/tests/TemplatesV2.localTemplates.test.js +131 -0
  138. package/v2Containers/Whatsapp/index.js +3 -20
  139. 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,291 @@
1
+ /**
2
+ * CommunicationFlow - Orchestrator Component
3
+ *
4
+ * Manages the step flow, validation, and data aggregation.
5
+ * Renders steps based on config and calls callbacks with aggregated data.
6
+ */
7
+
8
+ import React, {
9
+ useState, useCallback, useMemo, useEffect,
10
+ } from 'react';
11
+ import PropTypes from 'prop-types';
12
+ import { injectIntl } from 'react-intl';
13
+ import { compose } from 'redux';
14
+ import { connect } from 'react-redux';
15
+ import { createStructuredSelector } from 'reselect';
16
+ // import { CouponsCapContainer } from '@capillarytech/cap-coupons'; // Commented - cap-coupons flows disabled
17
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
20
+ // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
21
+ // import injectReducer from '../../utils/injectReducer';
22
+ import { makeSelectAuthenticated } from '../Cap/selectors';
23
+ import DynamicControlsStep from './steps/DynamicControlsStep';
24
+ import MessageTypeStep from './steps/MessageTypeStep';
25
+ import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
26
+ import ChannelSelectionStep from './steps/ChannelSelectionStep';
27
+ import {
28
+ STEPS,
29
+ CHANNEL_PRIORITY,
30
+ AB_TEST,
31
+ } from './constants';
32
+ import { getEnabledSteps } from './utils/getEnabledSteps';
33
+ import messages from './messages';
34
+ import './CommunicationFlow.scss';
35
+
36
+ // Inject couponsCap reducer and saga - commented out (cap-coupons flows disabled)
37
+ // const withCouponsReducer = injectReducer({
38
+ // key: 'couponsCap',
39
+ // reducer: CouponsCapContainer.couponsCapReducer,
40
+ // });
41
+ // const withCouponsSaga = injectSaga({
42
+ // key: 'couponsCapSaga',
43
+ // saga: CouponsCapContainer.couponsCapSaga,
44
+ // });
45
+
46
+ const CommunicationFlow = ({
47
+ config,
48
+ initialData,
49
+ onSave,
50
+ onCancel, // eslint-disable-line
51
+ onChange,
52
+ intl,
53
+ capData, // From Redux - contains user/org info needed by CouponsWrapper
54
+ }) => {
55
+ const { formatMessage } = intl || {};
56
+ const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
57
+ // Initialize step data from initialData or defaults
58
+ const [stepData, setStepData] = useState(() => {
59
+ const defaultMessageType = messageTypeData.defaultOption?.value || null;
60
+ return {
61
+ messageType: initialData?.messageType || defaultMessageType,
62
+ communicationStrategy: initialData?.communicationStrategy || null,
63
+ channel: initialData?.channel || config.channel || null,
64
+ channels: initialData?.channels || [],
65
+ selectedOfferDetails: initialData?.selectedOfferDetails || [],
66
+ contentItems: initialData?.contentItems || [],
67
+ deliverySetting: initialData?.deliverySetting || {},
68
+ dynamicControls: initialData?.dynamicControls || {},
69
+ };
70
+ });
71
+ const [validationErrors, setValidationErrors] = useState({});
72
+
73
+ // Memoize enabled steps
74
+ const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
75
+
76
+ /**
77
+ * Get aggregated data from all steps
78
+ */
79
+ const getAggregatedData = useCallback(() => {
80
+ const { communicationStrategy } = stepData;
81
+ const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(communicationStrategy);
82
+
83
+ return {
84
+ messageType: stepData.messageType,
85
+ communicationStrategy: stepData.communicationStrategy,
86
+ channel: isMultiChannel ? null : stepData.channel,
87
+ channels: isMultiChannel ? stepData.channels : [],
88
+ selectedOfferDetails: stepData.selectedOfferDetails,
89
+ contentItems: stepData.contentItems || [],
90
+ deliverySetting: stepData.deliverySetting,
91
+ dynamicControls: stepData.dynamicControls,
92
+ };
93
+ }, [stepData]);
94
+
95
+ /**
96
+ * Handle step data change
97
+ */
98
+ const handleStepChange = useCallback((step, data) => {
99
+ setStepData((prevStepData) => ({
100
+ ...prevStepData,
101
+ ...data,
102
+ }));
103
+ setValidationErrors((prevErrors) => ({
104
+ ...prevErrors,
105
+ [step]: null, // Clear validation error for this step
106
+ }));
107
+ }, []);
108
+
109
+ // Call onChange callback when stepData changes
110
+ useEffect(() => {
111
+ if (onChange) {
112
+ onChange(getAggregatedData());
113
+ }
114
+ // eslint-disable-next-line react-hooks/exhaustive-deps
115
+ }, [stepData]);
116
+
117
+ /**
118
+ * Render all enabled steps
119
+ */
120
+ const renderSteps = useCallback(() => enabledSteps.map((step) => {
121
+ const commonProps = {
122
+ value: stepData,
123
+ onChange: (data) => handleStepChange(step, data),
124
+ error: validationErrors[step],
125
+ };
126
+
127
+ switch (step) {
128
+ case STEPS.MESSAGE_TYPE:
129
+ return (
130
+ <CapRow key={step}>
131
+ <MessageTypeStep
132
+ {...commonProps}
133
+ value={stepData.messageType}
134
+ options={messageTypeData.options}
135
+ defaultOption={messageTypeData.defaultOption}
136
+ onChange={(messageType) => handleStepChange(step, { messageType })}
137
+ />
138
+ <CapDivider />
139
+ </CapRow>
140
+ );
141
+ case STEPS.COMMUNICATION_STRATEGY:
142
+ return (
143
+ <CapRow key={step}>
144
+ <CommunicationStrategyStep
145
+ {...commonProps}
146
+ value={stepData.communicationStrategy}
147
+ // messageType={stepData.messageType}
148
+ options={communicationStrategyData.options}
149
+ disabled={communicationStrategyData.disabled}
150
+ onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
151
+ />
152
+ <CapDivider />
153
+ </CapRow>
154
+ );
155
+ case STEPS.CHANNEL_SELECTION:
156
+ // Only show ChannelSelectionStep if communication strategy is selected
157
+ if (!stepData.communicationStrategy) {
158
+ return null;
159
+ }
160
+ return (
161
+ <CapRow key={step}>
162
+ <ChannelSelectionStep
163
+ {...commonProps}
164
+ value={stepData}
165
+ channels={contentTemplateData.channels}
166
+ onChange={(data) => handleStepChange(step, data)}
167
+ channelsToHide={contentTemplateData.channelsToHide}
168
+ channelsToDisable={contentTemplateData.channelsToDisable}
169
+ creativesMode={config.mode || 'create'}
170
+ selectedOfferDetails={stepData.selectedOfferDetails}
171
+ incentivesData={config.features?.incentivesData}
172
+ deliverySettingsData={config.features?.deliverySettingsData}
173
+ config={config}
174
+ capData={capData}
175
+ />
176
+ <CapDivider />
177
+ </CapRow>
178
+ );
179
+ // This will be added back in when coupons/points are integrated so keeping it commented out for now
180
+ // case STEPS.INCENTIVES:
181
+ // return (
182
+ // <IncentivesStep
183
+ // key={step}
184
+ // {...commonProps}
185
+ // value={stepData.selectedOfferDetails}
186
+ // onChange={(selectedOfferDetails) => handleStepChange(step, { selectedOfferDetails })}
187
+ // />
188
+ // );
189
+ case STEPS.DYNAMIC_CONTROLS:
190
+ // Only show DynamicControlsStep if communication strategy is selected
191
+ if (!stepData.communicationStrategy) {
192
+ return null;
193
+ }
194
+ return (
195
+ <DynamicControlsStep
196
+ key={step}
197
+ {...commonProps}
198
+ value={{ dynamicControls: stepData.dynamicControls }}
199
+ controls={config.features?.dynamicControlsData?.controls || []}
200
+ onChange={(data) => handleStepChange(step, { dynamicControls: data.dynamicControls })}
201
+ />
202
+ );
203
+ default:
204
+ return null;
205
+ }
206
+ }), [enabledSteps, stepData, validationErrors, config, handleStepChange, messageTypeData, communicationStrategyData, contentTemplateData]);
207
+
208
+ return (
209
+ <CapRow className="communication-flow-container">
210
+ {renderSteps()}
211
+ {onSave && (
212
+ <CapRow className="communication-flow-container__footer">
213
+ <CapButton type="primary" onClick={() => onSave(getAggregatedData())}>
214
+ {formatMessage(messages.save)}
215
+ </CapButton>
216
+ </CapRow>
217
+ )}
218
+ </CapRow>
219
+ );
220
+ };
221
+
222
+ CommunicationFlow.propTypes = {
223
+ config: PropTypes.shape({
224
+ consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
225
+ channel: PropTypes.string,
226
+ mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
227
+ channelsToHide: PropTypes.arrayOf(PropTypes.string),
228
+ channelsToDisable: PropTypes.arrayOf(PropTypes.string),
229
+ features: PropTypes.shape({
230
+ enableMessageType: PropTypes.bool,
231
+ enableCommunicationStrategy: PropTypes.bool,
232
+ messageTypeData: PropTypes.shape({
233
+ options: PropTypes.arrayOf(PropTypes.shape({
234
+ value: PropTypes.string.isRequired,
235
+ label: PropTypes.node.isRequired,
236
+ })),
237
+ defaultOption: PropTypes.shape({
238
+ value: PropTypes.string.isRequired,
239
+ label: PropTypes.node.isRequired,
240
+ }),
241
+ required: PropTypes.bool,
242
+ }),
243
+ communicationStrategyData: PropTypes.shape({
244
+ options: PropTypes.arrayOf(PropTypes.shape({
245
+ value: PropTypes.string.isRequired,
246
+ label: PropTypes.node.isRequired,
247
+ })),
248
+ defaultOption: PropTypes.shape({
249
+ value: PropTypes.string.isRequired,
250
+ label: PropTypes.node.isRequired,
251
+ }),
252
+ required: PropTypes.bool,
253
+ disabled: PropTypes.bool,
254
+ }),
255
+ contentTemplateData: PropTypes.shape({
256
+ required: PropTypes.bool,
257
+ channels: PropTypes.object,
258
+ }),
259
+ enableIncentives: PropTypes.bool,
260
+ enableDeliverySettings: PropTypes.bool,
261
+ enableOtherSettings: PropTypes.bool,
262
+ incentivesTypes: PropTypes.arrayOf(PropTypes.oneOf(['coupons', 'points', 'promotions', 'giftVouchers', 'badges'])),
263
+ }),
264
+ context: PropTypes.object,
265
+ }).isRequired,
266
+ initialData: PropTypes.object,
267
+ onSave: PropTypes.func.isRequired,
268
+ onCancel: PropTypes.func.isRequired,
269
+ onChange: PropTypes.func,
270
+ intl: PropTypes.object.isRequired,
271
+ capData: PropTypes.object,
272
+ };
273
+
274
+ CommunicationFlow.defaultProps = {
275
+ initialData: null,
276
+ onChange: null,
277
+ capData: {},
278
+ };
279
+
280
+ const mapStateToProps = createStructuredSelector({
281
+ capData: makeSelectAuthenticated(),
282
+ });
283
+
284
+ const withConnect = connect(mapStateToProps);
285
+
286
+ export default compose(
287
+ // withCouponsReducer, // cap-coupons flows disabled
288
+ // withCouponsSaga, // cap-coupons flows disabled
289
+ withConnect,
290
+ injectIntl,
291
+ )(CommunicationFlow);
@@ -0,0 +1,25 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables.scss';
2
+
3
+ // .slidebox-content-container is a DOM ancestor of .communication-flow-container
4
+ // (the outer CapSlideBox wraps this component), so it cannot be scoped as a
5
+ // descendant selector — it must remain at root level to match correctly.
6
+ .slidebox-content-container {
7
+ padding: 0 $CAP_SPACE_48 !important; // 48px left and right, 0 top and bottom
8
+ .slidebox-footer {
9
+ padding: 0 $CAP_SPACE_48 !important;
10
+ }
11
+ }
12
+
13
+ .communication-flow-container {
14
+ .step-divider {
15
+ margin: $CAP_SPACE_32 0;
16
+ }
17
+
18
+ .heading-style {
19
+ margin-bottom: $CAP_SPACE_08;
20
+ }
21
+
22
+ &__footer {
23
+ padding: $CAP_SPACE_16 0 $CAP_SPACE_08;
24
+ }
25
+ }