@capillarytech/creatives-library 8.0.317 → 8.0.319
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.
- package/constants/unified.js +1 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/constants.js +3 -0
- package/v2Containers/CreativesContainer/index.js +1 -1
- package/v2Containers/Rcs/index.js +4 -2
package/constants/unified.js
CHANGED
|
@@ -60,6 +60,7 @@ export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
|
|
|
60
60
|
export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
|
|
61
61
|
export const ENABLE_NEW_LEFT_NAVIGATION = 'ENABLE_NEW_LEFT_NAVIGATION';
|
|
62
62
|
export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
|
|
63
|
+
export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
|
|
63
64
|
export const EMBEDDED = 'embedded';
|
|
64
65
|
// --- Tag/Validation Constants ---
|
|
65
66
|
export const CARD_RELATED_TAGS = [
|
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -334,6 +334,12 @@ export const getTemplateDetails = async ({id, channel}) => {
|
|
|
334
334
|
return { ...compressedTemplatesData, response: decompressData};
|
|
335
335
|
};
|
|
336
336
|
|
|
337
|
+
export const getDomainProperties = (channels, orgUnitId) => {
|
|
338
|
+
const queryString = channels?.map((channel) => `channel=${channel?.toUpperCase()}`)?.join('&');
|
|
339
|
+
const url = `${CAMPAIGNS_API_ORG_ENDPOINT}/meta/domainProperties?${queryString}`;
|
|
340
|
+
return request(url, getAPICallObject('GET', null, false, true, orgUnitId));
|
|
341
|
+
};
|
|
342
|
+
|
|
337
343
|
export const getMediaDetails = async ({ id }) => {
|
|
338
344
|
const url = `${API_ENDPOINT}/media/${id}`;
|
|
339
345
|
return request(url, getAPICallObject('GET'));
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getSenderDetails,
|
|
3
|
+
getDomainProperties,
|
|
3
4
|
uploadFile,
|
|
4
5
|
getCdnTransformationConfig,
|
|
5
6
|
createWhatsappTemplate,
|
|
@@ -52,6 +53,12 @@ describe('getSenderDetails -- Test with valid responses', () => {
|
|
|
52
53
|
expect(getSenderDetails('WHATSAPP', -1)).toEqual(Promise.resolve()));
|
|
53
54
|
});
|
|
54
55
|
|
|
56
|
+
describe('getDomainProperties -- Test with valid responses', () => {
|
|
57
|
+
it('Should return correct response', () => {
|
|
58
|
+
expect(getDomainProperties(['SMS', 'EMAIL'], null)).toEqual(Promise.resolve());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
55
62
|
describe('getCdnTransformationConfigs -- Test with valid responses', () => {
|
|
56
63
|
it('Should return correct response', () =>
|
|
57
64
|
expect(getCdnTransformationConfig()).toEqual(Promise.resolve()));
|
package/utils/common.js
CHANGED
|
@@ -25,7 +25,8 @@ import {
|
|
|
25
25
|
ENABLE_WEBPUSH,
|
|
26
26
|
SUPPORT_CK_EDITOR,
|
|
27
27
|
ENABLE_NEW_MPUSH,
|
|
28
|
-
ENABLE_NEW_EDITOR_FLOW_INAPP
|
|
28
|
+
ENABLE_NEW_EDITOR_FLOW_INAPP,
|
|
29
|
+
SUPPORT_ENGAGEMENT_MODULE,
|
|
29
30
|
} from '../constants/unified';
|
|
30
31
|
import { apiMessageFormatHandler } from './commonUtils';
|
|
31
32
|
|
|
@@ -96,6 +97,10 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
|
|
|
96
97
|
null,
|
|
97
98
|
SUPPORT_CK_EDITOR,
|
|
98
99
|
);
|
|
100
|
+
export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
|
|
101
|
+
null,
|
|
102
|
+
SUPPORT_ENGAGEMENT_MODULE,
|
|
103
|
+
);
|
|
99
104
|
|
|
100
105
|
export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
|
|
101
106
|
null,
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
jest.mock('../../CreativesContainer', () => function MockCreativesContainer({
|
|
4
|
+
getCreativesData,
|
|
5
|
+
handleCloseCreatives,
|
|
6
|
+
creativesMode,
|
|
7
|
+
channel,
|
|
8
|
+
}) {
|
|
9
|
+
return (
|
|
10
|
+
<div data-testid="creatives-mock" data-creatives-mode={creativesMode} data-creatives-channel={channel}>
|
|
11
|
+
<button
|
|
12
|
+
type="button"
|
|
13
|
+
data-testid="creatives-save"
|
|
14
|
+
onClick={() => getCreativesData({ smsBody: 'Saved' })}
|
|
15
|
+
>
|
|
16
|
+
Save creative
|
|
17
|
+
</button>
|
|
18
|
+
<button type="button" data-testid="creatives-close" onClick={handleCloseCreatives}>
|
|
19
|
+
Close creative
|
|
20
|
+
</button>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
import { Provider } from 'react-redux';
|
|
26
|
+
import { configureStore } from '@capillarytech/vulcan-react-sdk/utils';
|
|
27
|
+
import {
|
|
28
|
+
render as rtlRender, screen, waitFor, within,
|
|
29
|
+
} from '@testing-library/react';
|
|
30
|
+
import userEvent from '@testing-library/user-event';
|
|
31
|
+
import '@testing-library/jest-dom';
|
|
32
|
+
import { IntlProvider } from 'react-intl';
|
|
33
|
+
import history from '../../../utils/history';
|
|
34
|
+
import { initialReducer } from '../../../initialReducer';
|
|
35
|
+
import CommunicationFlow from '../CommunicationFlow';
|
|
36
|
+
import { getEnabledSteps } from '../utils/getEnabledSteps';
|
|
37
|
+
import {
|
|
38
|
+
CHANNELS,
|
|
39
|
+
CHANNEL_PRIORITY,
|
|
40
|
+
SINGLE_TEMPLATE,
|
|
41
|
+
STEPS,
|
|
42
|
+
} from '../constants';
|
|
43
|
+
|
|
44
|
+
let store;
|
|
45
|
+
|
|
46
|
+
beforeAll(() => {
|
|
47
|
+
store = configureStore({}, initialReducer, history);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/** Shared option lists for feature config */
|
|
51
|
+
const MESSAGE_OPTIONS = [
|
|
52
|
+
{ value: 'promotional', label: 'Promotional' },
|
|
53
|
+
{ value: 'transactional', label: 'Transactional' },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const STRATEGY_OPTIONS = [
|
|
57
|
+
{ value: SINGLE_TEMPLATE, label: 'Single template', isMultiChannel: false },
|
|
58
|
+
{ value: CHANNEL_PRIORITY, label: 'Channel priority', isMultiChannel: true },
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const DYNAMIC_CONTROLS = [
|
|
62
|
+
{ key: 'sendToControl', label: 'Send to control', description: 'Help text' },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
/** Base config for all CommunicationFlow tests */
|
|
66
|
+
const baseConfig = {
|
|
67
|
+
consumer: 'campaigns',
|
|
68
|
+
mode: 'create',
|
|
69
|
+
features: {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const CAP_DATA = {};
|
|
73
|
+
|
|
74
|
+
/** Reusable feature presets merged into config.features */
|
|
75
|
+
const FEATURES = {
|
|
76
|
+
messageType: {
|
|
77
|
+
messageTypeData: {
|
|
78
|
+
required: true,
|
|
79
|
+
options: MESSAGE_OPTIONS,
|
|
80
|
+
defaultOption: { value: 'promotional', label: 'Promotional' },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
strategyContent: {
|
|
84
|
+
communicationStrategyData: { required: true, options: STRATEGY_OPTIONS, disabled: false },
|
|
85
|
+
contentTemplateData: { required: true, channels: CHANNELS },
|
|
86
|
+
},
|
|
87
|
+
strategyContentDynamic: {
|
|
88
|
+
communicationStrategyData: { required: true, options: STRATEGY_OPTIONS, disabled: false },
|
|
89
|
+
contentTemplateData: { required: true, channels: CHANNELS },
|
|
90
|
+
dynamicControlsData: { required: true, controls: DYNAMIC_CONTROLS },
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
function renderFlow(ui) {
|
|
95
|
+
return rtlRender(
|
|
96
|
+
<Provider store={store}>
|
|
97
|
+
<IntlProvider locale="en" messages={{}} defaultLocale="en">
|
|
98
|
+
{ui}
|
|
99
|
+
</IntlProvider>
|
|
100
|
+
</Provider>,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Renders connected CommunicationFlow with shared defaults.
|
|
106
|
+
* @param {object} [options]
|
|
107
|
+
* @param {object} [options.features] — config.features (use FEATURES.* or custom)
|
|
108
|
+
* @param {object|null} [options.initialData]
|
|
109
|
+
* @param {object} [options.capData]
|
|
110
|
+
* @param {jest.Mock} [options.onSave] — defaults to jest.fn()
|
|
111
|
+
* @param {jest.Mock} [options.onCancel] — defaults to jest.fn()
|
|
112
|
+
* @param {jest.Mock} [options.onChange]
|
|
113
|
+
*/
|
|
114
|
+
function renderWithFlow({
|
|
115
|
+
features = {},
|
|
116
|
+
initialData = null,
|
|
117
|
+
capData = CAP_DATA,
|
|
118
|
+
onSave,
|
|
119
|
+
onCancel,
|
|
120
|
+
onChange,
|
|
121
|
+
...rest
|
|
122
|
+
} = {}) {
|
|
123
|
+
return renderFlow(
|
|
124
|
+
<CommunicationFlow
|
|
125
|
+
config={{ ...baseConfig, features }}
|
|
126
|
+
initialData={initialData}
|
|
127
|
+
onSave={onSave ?? jest.fn()}
|
|
128
|
+
onCancel={onCancel ?? jest.fn()}
|
|
129
|
+
onChange={onChange}
|
|
130
|
+
capData={capData}
|
|
131
|
+
{...rest}
|
|
132
|
+
/>,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function selectCommunicationStrategy(label) {
|
|
137
|
+
const combo = screen.getByRole('combobox');
|
|
138
|
+
await userEvent.click(combo);
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
expect(screen.getByRole('listbox')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
await userEvent.click(within(screen.getByRole('listbox')).getByText(label));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
describe('getEnabledSteps', () => {
|
|
146
|
+
it('includes delivery settings when deliverySettingsData.required is true', () => {
|
|
147
|
+
const steps = getEnabledSteps({
|
|
148
|
+
...baseConfig,
|
|
149
|
+
features: {
|
|
150
|
+
deliverySettingsData: { required: true },
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
expect(steps).toContain(STEPS.DELIVERY_SETTINGS);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('omits delivery settings when deliverySettingsData.required is absent or false', () => {
|
|
157
|
+
expect(getEnabledSteps({ ...baseConfig, features: {} })).not.toContain(
|
|
158
|
+
STEPS.DELIVERY_SETTINGS,
|
|
159
|
+
);
|
|
160
|
+
expect(
|
|
161
|
+
getEnabledSteps({
|
|
162
|
+
...baseConfig,
|
|
163
|
+
features: { deliverySettingsData: { required: false } },
|
|
164
|
+
}),
|
|
165
|
+
).not.toContain(STEPS.DELIVERY_SETTINGS);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('CommunicationFlow', () => {
|
|
170
|
+
it('renders only steps whose features are required', () => {
|
|
171
|
+
renderWithFlow({ features: FEATURES.messageType });
|
|
172
|
+
expect(screen.getByText(/message type/i)).toBeInTheDocument();
|
|
173
|
+
expect(screen.queryByText(/communication strategy/i)).not.toBeInTheDocument();
|
|
174
|
+
expect(screen.queryByText(/content template/i)).not.toBeInTheDocument();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('hides channel selection and dynamic controls until a communication strategy is set', async () => {
|
|
178
|
+
renderWithFlow({ features: FEATURES.strategyContentDynamic });
|
|
179
|
+
|
|
180
|
+
expect(screen.queryByText(/content template/i)).not.toBeInTheDocument();
|
|
181
|
+
expect(screen.queryByText(/other controls/i)).not.toBeInTheDocument();
|
|
182
|
+
|
|
183
|
+
await selectCommunicationStrategy('Single template');
|
|
184
|
+
|
|
185
|
+
await waitFor(() => {
|
|
186
|
+
expect(screen.getAllByText(/content template/i).length).toBeGreaterThan(0);
|
|
187
|
+
});
|
|
188
|
+
expect(screen.getByText(/other controls/i)).toBeInTheDocument();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it('calls onChange with aggregated data when step data updates', async () => {
|
|
192
|
+
const onChange = jest.fn();
|
|
193
|
+
renderWithFlow({ features: FEATURES.messageType, onChange });
|
|
194
|
+
|
|
195
|
+
onChange.mockClear();
|
|
196
|
+
await userEvent.click(screen.getByRole('radio', { name: /transactional/i }));
|
|
197
|
+
|
|
198
|
+
expect(onChange).toHaveBeenCalled();
|
|
199
|
+
const payload = onChange.mock.calls[onChange.mock.calls.length - 1][0];
|
|
200
|
+
expect(payload.messageType).toBe('transactional');
|
|
201
|
+
expect(payload.communicationStrategy).toBeNull();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('passes single-channel aggregate to onSave', async () => {
|
|
205
|
+
const onSave = jest.fn();
|
|
206
|
+
renderWithFlow({ features: FEATURES.strategyContent, onSave });
|
|
207
|
+
|
|
208
|
+
await selectCommunicationStrategy('Single template');
|
|
209
|
+
|
|
210
|
+
await userEvent.click(screen.getByRole('button', { name: /content template/i }));
|
|
211
|
+
await waitFor(() => expect(screen.getByRole('menu')).toBeInTheDocument());
|
|
212
|
+
await userEvent.click(within(screen.getByRole('menu')).getByText('SMS'));
|
|
213
|
+
|
|
214
|
+
await waitFor(() => expect(screen.getByTestId('creatives-mock')).toBeInTheDocument());
|
|
215
|
+
await userEvent.click(screen.getByRole('button', { name: /save creative/i }));
|
|
216
|
+
|
|
217
|
+
await waitFor(() => expect(screen.queryByTestId('creatives-mock')).not.toBeInTheDocument());
|
|
218
|
+
|
|
219
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
220
|
+
|
|
221
|
+
expect(onSave).toHaveBeenCalledTimes(1);
|
|
222
|
+
const saved = onSave.mock.calls[0][0];
|
|
223
|
+
expect(saved.communicationStrategy).toBe(SINGLE_TEMPLATE);
|
|
224
|
+
expect(saved.channel).toBe('SMS');
|
|
225
|
+
expect(saved.channels).toEqual([]);
|
|
226
|
+
expect(saved.contentItems.length).toBeGreaterThanOrEqual(1);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('clears channel and keeps channels list in aggregate for multi-channel strategy', async () => {
|
|
230
|
+
const onSave = jest.fn();
|
|
231
|
+
renderWithFlow({
|
|
232
|
+
features: FEATURES.strategyContent,
|
|
233
|
+
initialData: {
|
|
234
|
+
communicationStrategy: CHANNEL_PRIORITY,
|
|
235
|
+
channels: ['SMS', 'EMAIL'],
|
|
236
|
+
},
|
|
237
|
+
onSave,
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
await userEvent.click(screen.getByRole('button', { name: /^save$/i }));
|
|
241
|
+
|
|
242
|
+
const saved = onSave.mock.calls[0][0];
|
|
243
|
+
expect(saved.communicationStrategy).toBe(CHANNEL_PRIORITY);
|
|
244
|
+
expect(saved.channel).toBeNull();
|
|
245
|
+
expect(saved.channels).toEqual(['SMS', 'EMAIL']);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it('initializes message type from initialData when provided', () => {
|
|
249
|
+
renderWithFlow({
|
|
250
|
+
features: FEATURES.messageType,
|
|
251
|
+
initialData: { messageType: 'transactional' },
|
|
252
|
+
});
|
|
253
|
+
expect(screen.getByRole('radio', { name: /transactional/i })).toBeChecked();
|
|
254
|
+
});
|
|
255
|
+
});
|