@capillarytech/creatives-library 9.0.56-betaa.0 → 9.0.57-alpha.0
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 +7 -1
- package/package.json +1 -1
- package/services/api.js +25 -4
- package/utils/common.js +0 -4
- package/utils/templateVarUtils.js +16 -0
- package/utils/tests/templateVarUtils.test.js +44 -0
- package/v2Components/CapActionButton/index.js +73 -60
- package/v2Components/CapActionButton/index.scss +44 -28
- package/v2Components/CapActionButton/messages.js +7 -3
- package/v2Components/CapActionButton/tests/index.test.js +17 -1
- package/v2Components/CapWhatsappCTA/messages.js +4 -0
- package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
- package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
- package/v2Components/CommonTestAndPreview/actions.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +8 -0
- package/v2Components/CommonTestAndPreview/index.js +55 -3
- package/v2Components/CommonTestAndPreview/reducer.js +20 -0
- package/v2Components/CommonTestAndPreview/sagas.js +28 -0
- package/v2Components/CommonTestAndPreview/selectors.js +6 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
- package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +36 -22
- package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +5 -13
- package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +6 -4
- package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +13 -14
- package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +15 -8
- package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +31 -0
- package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +13 -0
- package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +26 -0
- package/v2Components/FormBuilder/_formBuilder.scss +8 -0
- package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
- package/v2Containers/CommunicationFlow/constants.js +65 -4
- package/v2Containers/CommunicationFlow/index.js +16 -23
- package/v2Containers/CommunicationFlow/messages.js +11 -3
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
- package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
- package/v2Containers/CreativesContainer/index.js +29 -3
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
- package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
- package/v2Containers/MobilePush/Create/index.js +18 -1
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
- package/v2Containers/MobilePush/Edit/index.js +18 -1
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
- package/v2Containers/MobilePush/commonMethods.js +49 -1
- package/v2Containers/MobilePushNew/index.js +29 -4
- package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
- package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
- package/v2Containers/MobilePushNew/utils.js +34 -1
- package/v2Containers/MobilepushWrapper/index.js +3 -1
- package/v2Containers/Rcs/index.js +34 -0
- package/v2Containers/Rcs/index.scss +15 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
- package/v2Containers/Rcs/tests/index.test.js +67 -0
- package/v2Containers/Sms/Create/index.js +3 -1
- package/v2Containers/Sms/Edit/index.js +25 -0
- package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
- package/v2Containers/Viber/index.js +24 -1
- package/v2Containers/Viber/tests/index.test.js +103 -0
- package/v2Containers/WebPush/Create/index.js +19 -0
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
- package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
- package/v2Containers/Whatsapp/index.js +25 -4
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
- package/v2Containers/Whatsapp/tests/index.test.js +115 -0
- package/v2Containers/Zalo/index.js +28 -1
- package/v2Containers/Zalo/tests/index.test.js +99 -0
- package/v2Containers/mockdata.js +25 -0
|
@@ -13,14 +13,14 @@ import { injectIntl } from 'react-intl';
|
|
|
13
13
|
import { compose } from 'redux';
|
|
14
14
|
import { connect } from 'react-redux';
|
|
15
15
|
import { createStructuredSelector } from 'reselect';
|
|
16
|
-
// import { CouponsCapContainer } from '@capillarytech/cap-coupons'; // Commented - cap-coupons flows disabled
|
|
17
16
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
18
17
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
19
18
|
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
20
|
+
import injectReducer from '../../utils/injectReducer';
|
|
22
21
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
23
|
-
import
|
|
22
|
+
import capReducer from '../Cap/reducer';
|
|
23
|
+
import { createCommDefinition, editCommDefinition } from '../../services/api';
|
|
24
24
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
25
25
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
26
26
|
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
@@ -35,23 +35,20 @@ import {
|
|
|
35
35
|
CHANNELS,
|
|
36
36
|
INCENTIVE_TYPES,
|
|
37
37
|
DYNAMIC_CONTROLS_CONFIG,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
CCS_STRATEGY_TYPE_SINGLE,
|
|
39
|
+
CCS_CHANNEL_CONTENT_KEY_MAP,
|
|
40
|
+
CCS_CHANNEL_DELIVERY_KEY_MAP,
|
|
41
|
+
CCS_CHANNEL_NAME_MAP,
|
|
42
|
+
CCS_CONTENT_TRANSFORMS,
|
|
43
|
+
REFERENCE_ID_EXISTS,
|
|
41
44
|
} from './constants';
|
|
42
45
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
43
46
|
import messages from './messages';
|
|
44
47
|
import './CommunicationFlow.scss';
|
|
45
48
|
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
// reducer: CouponsCapContainer.couponsCapReducer,
|
|
50
|
-
// });
|
|
51
|
-
// const withCouponsSaga = injectSaga({
|
|
52
|
-
// key: 'couponsCapSaga',
|
|
53
|
-
// saga: CouponsCapContainer.couponsCapSaga,
|
|
54
|
-
// });
|
|
49
|
+
// Self-injects the same 'cap' reducer CreativesContainer already injects under this key;
|
|
50
|
+
// idempotent, so it's a no-op if the consumer's own store already provides it.
|
|
51
|
+
const withCapReducer = injectReducer({ key: 'cap', reducer: capReducer });
|
|
55
52
|
|
|
56
53
|
const getDeliveryChannels = (contentItems) => {
|
|
57
54
|
const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
|
|
@@ -64,6 +61,9 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
64
61
|
return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
|
|
65
62
|
};
|
|
66
63
|
|
|
64
|
+
// Duplicate referenceId within the org returns a 409 CCS error envelope.
|
|
65
|
+
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === REFERENCE_ID_EXISTS;
|
|
66
|
+
|
|
67
67
|
const CommunicationFlow = ({
|
|
68
68
|
config,
|
|
69
69
|
initialData,
|
|
@@ -91,6 +91,7 @@ const CommunicationFlow = ({
|
|
|
91
91
|
};
|
|
92
92
|
});
|
|
93
93
|
const [validationErrors, setValidationErrors] = useState({});
|
|
94
|
+
const [saveError, setSaveError] = useState(null);
|
|
94
95
|
|
|
95
96
|
// Memoize enabled steps
|
|
96
97
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
@@ -150,74 +151,123 @@ const CommunicationFlow = ({
|
|
|
150
151
|
}, []);
|
|
151
152
|
|
|
152
153
|
const handleSave = useCallback(async () => {
|
|
154
|
+
setSaveError(null);
|
|
153
155
|
const aggregatedData = getAggregatedData();
|
|
154
156
|
const shouldUseCCS = config?.useCCS !== false;
|
|
157
|
+
let ccsCommDefinition = null;
|
|
155
158
|
|
|
156
159
|
if (shouldUseCCS) {
|
|
157
|
-
const
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
+
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
|
|
161
|
+
const contentItem = (aggregatedData.contentItems || [])[0];
|
|
162
|
+
// Consumer-supplied (e.g. CapNotify's Alert Name field) — CommunicationFlow has no name input of its own.
|
|
163
|
+
const name = config?.context?.name;
|
|
160
164
|
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
// CCS create only supports SINGLE strategy; CHANNEL_PRIORITY/AB_TEST have no CCS equivalent yet.
|
|
166
|
+
if (!isMultiChannel && contentItem && name) {
|
|
167
|
+
const rawChannel = (contentItem.channel || '').toUpperCase();
|
|
168
|
+
const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
|
|
169
|
+
const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
|
|
170
|
+
const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
|
|
171
|
+
const contentTransform = CCS_CONTENT_TRANSFORMS[channel];
|
|
172
|
+
const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
|
|
173
|
+
const { dynamicControls = {} } = aggregatedData;
|
|
174
|
+
const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
|
|
175
|
+
// Optional fields — sent as '' rather than omitted when left blank.
|
|
176
|
+
const referenceId = config?.context?.referenceId || '';
|
|
177
|
+
const description = config?.context?.description || '';
|
|
163
178
|
|
|
164
|
-
|
|
165
|
-
|
|
179
|
+
const payload = {
|
|
180
|
+
referenceId,
|
|
181
|
+
name,
|
|
182
|
+
description,
|
|
183
|
+
strategyType: CCS_STRATEGY_TYPE_SINGLE,
|
|
184
|
+
settings: {
|
|
185
|
+
additionalSettings: {
|
|
186
|
+
useTinyUrl: dynamicControls.useTinyUrl ?? false,
|
|
187
|
+
encryptUrl: dynamicControls.sendToControlCustomers ?? false,
|
|
188
|
+
linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
|
|
189
|
+
userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
|
|
190
|
+
},
|
|
191
|
+
executionParams: {},
|
|
192
|
+
},
|
|
193
|
+
singleChannelStrategy: {
|
|
194
|
+
variant: {
|
|
195
|
+
channel,
|
|
196
|
+
// `channel` spread last so CCS's normalized enum overrides the UI's internal identifier.
|
|
197
|
+
...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
|
|
198
|
+
...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
};
|
|
166
202
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
203
|
+
// existingCommDefinitionId (set when editing, e.g. a rejected alert) creates a new
|
|
204
|
+
// DRAFT version on the same CommDefinition instead of a new one.
|
|
205
|
+
const { existingCommDefinitionId } = config?.context || {};
|
|
206
|
+
const {
|
|
207
|
+
referenceId: payloadReferenceId,
|
|
208
|
+
description: payloadDescription,
|
|
209
|
+
strategyType,
|
|
210
|
+
settings,
|
|
211
|
+
singleChannelStrategy,
|
|
212
|
+
} = payload;
|
|
173
213
|
|
|
174
|
-
if (contentItems.length > 0) {
|
|
175
214
|
try {
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
215
|
+
const res = existingCommDefinitionId
|
|
216
|
+
? await editCommDefinition(existingCommDefinitionId, {
|
|
217
|
+
referenceId: payloadReferenceId,
|
|
218
|
+
description: payloadDescription,
|
|
219
|
+
strategyType,
|
|
220
|
+
settings,
|
|
221
|
+
singleChannelStrategy,
|
|
222
|
+
})
|
|
223
|
+
: await createCommDefinition(payload);
|
|
224
|
+
if (res?.isError) {
|
|
225
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (isDuplicateReferenceIdError(res)) {
|
|
229
|
+
setSaveError(formatMessage(messages.duplicateReferenceIdError));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
const data = res?.response?.data;
|
|
233
|
+
if (existingCommDefinitionId) {
|
|
234
|
+
if (data) {
|
|
235
|
+
ccsCommDefinition = {
|
|
236
|
+
id: existingCommDefinitionId,
|
|
237
|
+
referenceId: referenceId || data.referenceId,
|
|
238
|
+
version: data.version?.version ?? data.version ?? 1,
|
|
239
|
+
status: data.status,
|
|
201
240
|
};
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
241
|
+
} else {
|
|
242
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
} else if (data?.id) {
|
|
246
|
+
ccsCommDefinition = {
|
|
247
|
+
id: data.id,
|
|
248
|
+
referenceId: data.referenceId,
|
|
249
|
+
version: data.version?.version ?? 1,
|
|
250
|
+
status: data.status,
|
|
251
|
+
};
|
|
252
|
+
} else {
|
|
253
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
254
|
+
return;
|
|
213
255
|
}
|
|
214
256
|
} catch (error) {
|
|
215
|
-
console.error('[CommunicationFlow] CCS
|
|
257
|
+
console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
|
|
258
|
+
setSaveError(formatMessage(messages.genericSaveError));
|
|
259
|
+
return;
|
|
216
260
|
}
|
|
261
|
+
} else if (!name) {
|
|
262
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
|
|
263
|
+
} else if (isMultiChannel) {
|
|
264
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
|
|
265
|
+
} else if (!contentItem) {
|
|
266
|
+
console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
|
|
217
267
|
}
|
|
218
268
|
}
|
|
219
269
|
|
|
220
|
-
onSave(aggregatedData);
|
|
270
|
+
onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
|
|
221
271
|
}, [getAggregatedData, config, onSave]);
|
|
222
272
|
|
|
223
273
|
// Call onChange callback when stepData changes
|
|
@@ -252,9 +302,17 @@ const CommunicationFlow = ({
|
|
|
252
302
|
<CapDivider />
|
|
253
303
|
</CapRow>
|
|
254
304
|
);
|
|
255
|
-
case STEPS.COMMUNICATION_STRATEGY:
|
|
305
|
+
case STEPS.COMMUNICATION_STRATEGY: {
|
|
306
|
+
// No divider when there's no content yet, but the spacing above the Channel
|
|
307
|
+
// card must stay either way — see .communication-strategy-row in the scss.
|
|
308
|
+
const templateStepFollows = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
|
|
309
|
+
const hasContent = stepData.contentItems?.length > 0;
|
|
256
310
|
return (
|
|
257
|
-
<CapRow
|
|
311
|
+
<CapRow
|
|
312
|
+
useLegacy
|
|
313
|
+
key={step}
|
|
314
|
+
className={(!templateStepFollows || hasContent) ? '' : 'communication-strategy-row--no-divider'}
|
|
315
|
+
>
|
|
258
316
|
<CommunicationStrategyStep
|
|
259
317
|
{...commonProps}
|
|
260
318
|
value={stepData.communicationStrategy}
|
|
@@ -263,9 +321,10 @@ const CommunicationFlow = ({
|
|
|
263
321
|
disabled={communicationStrategyData.disabled}
|
|
264
322
|
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
265
323
|
/>
|
|
266
|
-
<CapDivider />
|
|
324
|
+
{(!templateStepFollows || hasContent) && <CapDivider />}
|
|
267
325
|
</CapRow>
|
|
268
326
|
);
|
|
327
|
+
}
|
|
269
328
|
case STEPS.CHANNEL_SELECTION:
|
|
270
329
|
// Only show ChannelSelectionStep if communication strategy is selected
|
|
271
330
|
if (!stepData.communicationStrategy) {
|
|
@@ -280,6 +339,7 @@ const CommunicationFlow = ({
|
|
|
280
339
|
onChange={(data) => handleStepChange(step, data)}
|
|
281
340
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
282
341
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
342
|
+
disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
|
|
283
343
|
creativesMode={config.mode || 'create'}
|
|
284
344
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
285
345
|
incentivesData={{
|
|
@@ -290,7 +350,7 @@ const CommunicationFlow = ({
|
|
|
290
350
|
config={config}
|
|
291
351
|
capData={cap || capData}
|
|
292
352
|
/>
|
|
293
|
-
<CapDivider />
|
|
353
|
+
{stepData.contentItems?.length > 0 && <CapDivider />}
|
|
294
354
|
</CapRow>
|
|
295
355
|
);
|
|
296
356
|
case STEPS.DYNAMIC_CONTROLS:
|
|
@@ -317,6 +377,11 @@ const CommunicationFlow = ({
|
|
|
317
377
|
{renderSteps()}
|
|
318
378
|
{onSave && (
|
|
319
379
|
<CapRow useLegacy className="communication-flow-container__footer">
|
|
380
|
+
{saveError && (
|
|
381
|
+
<CapLabel type="label2" className="communication-flow-container__save-error">
|
|
382
|
+
{saveError}
|
|
383
|
+
</CapLabel>
|
|
384
|
+
)}
|
|
320
385
|
<CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
|
|
321
386
|
{formatMessage(messages.save)}
|
|
322
387
|
</CapButton>
|
|
@@ -328,7 +393,7 @@ const CommunicationFlow = ({
|
|
|
328
393
|
|
|
329
394
|
CommunicationFlow.propTypes = {
|
|
330
395
|
config: PropTypes.shape({
|
|
331
|
-
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
396
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
|
|
332
397
|
channel: PropTypes.string,
|
|
333
398
|
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
334
399
|
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
@@ -362,6 +427,7 @@ CommunicationFlow.propTypes = {
|
|
|
362
427
|
contentTemplateData: PropTypes.shape({
|
|
363
428
|
required: PropTypes.bool,
|
|
364
429
|
channels: PropTypes.object,
|
|
430
|
+
disablePreviewAndTest: PropTypes.bool,
|
|
365
431
|
}),
|
|
366
432
|
enableIncentives: PropTypes.bool,
|
|
367
433
|
enableDeliverySettings: PropTypes.bool,
|
|
@@ -394,8 +460,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
394
460
|
const withConnect = connect(mapStateToProps);
|
|
395
461
|
|
|
396
462
|
export default compose(
|
|
397
|
-
|
|
398
|
-
// withCouponsSaga, // cap-coupons flows disabled
|
|
463
|
+
withCapReducer,
|
|
399
464
|
withConnect,
|
|
400
465
|
injectIntl,
|
|
401
466
|
)(CommunicationFlow);
|
|
@@ -128,16 +128,32 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.communication-flow-container {
|
|
131
|
+
// Anchor the Save footer to the bottom without flex, avoiding .ant-row-legacy overrides and flexbox stretching nested fixed-size content.
|
|
132
|
+
&.ant-row-legacy {
|
|
133
|
+
position: relative !important;
|
|
134
|
+
min-height: calc(100vh - 6.571rem) !important;
|
|
135
|
+
padding-bottom: 6rem !important;
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
.step-divider {
|
|
132
139
|
margin: $CAP_SPACE_32 0;
|
|
133
140
|
}
|
|
134
141
|
|
|
142
|
+
// Hide the divider when empty but preserve its margin gap so the section doesn’t sit flush against the Channel card.
|
|
143
|
+
.communication-strategy-row--no-divider {
|
|
144
|
+
margin-bottom: $CAP_SPACE_32;
|
|
145
|
+
}
|
|
146
|
+
|
|
135
147
|
.heading-style {
|
|
136
148
|
margin-bottom: $CAP_SPACE_08;
|
|
137
149
|
}
|
|
138
150
|
|
|
139
|
-
&__footer {
|
|
140
|
-
|
|
151
|
+
&__footer.ant-row-legacy {
|
|
152
|
+
position: absolute !important;
|
|
153
|
+
left: 0;
|
|
154
|
+
right: 0;
|
|
155
|
+
bottom: 0 !important;
|
|
156
|
+
padding: 3.142rem 0 $CAP_SPACE_08;
|
|
141
157
|
}
|
|
142
158
|
}
|
|
143
159
|
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* - Configured state: shows a two-column CapCard with pen icon, channel preview (left), dynamic controls (right)
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import React, { useState, useCallback } from 'react';
|
|
9
|
+
import React, { useState, useCallback, useEffect } from 'react';
|
|
10
10
|
import PropTypes from 'prop-types';
|
|
11
11
|
import { injectIntl } from 'react-intl';
|
|
12
12
|
import { CAP_G09 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
@@ -19,6 +19,7 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
|
19
19
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
20
20
|
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
21
21
|
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
22
|
+
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
22
23
|
import CommunicationFlow from './index';
|
|
23
24
|
import { CHANNELS, DEFAULT_COMMUNICATION_STRATEGY_OPTIONS, DYNAMIC_CONTROLS_CONFIG } from './constants';
|
|
24
25
|
import {
|
|
@@ -41,7 +42,7 @@ const resolveValue = (val) => {
|
|
|
41
42
|
|
|
42
43
|
const SENDER_ID_RESOLVERS = {
|
|
43
44
|
[SMS]: (setting) => resolveValue(setting?.gsmSenderId),
|
|
44
|
-
[EMAIL]: (setting) => resolveValue(setting?.
|
|
45
|
+
[EMAIL]: (setting) => resolveValue(setting?.senderId),
|
|
45
46
|
[VIBER]: (setting) => resolveValue(setting?.sender) || resolveValue(setting?.gsmSenderId),
|
|
46
47
|
[WHATSAPP]: (setting) => resolveValue(setting?.senderMobNum),
|
|
47
48
|
[RCS]: (setting) => resolveValue(setting?.senderMobNum) || resolveValue(setting?.rcsSender),
|
|
@@ -74,11 +75,21 @@ const CommunicationFlowCard = ({
|
|
|
74
75
|
onChange,
|
|
75
76
|
cap,
|
|
76
77
|
intl,
|
|
78
|
+
disabled,
|
|
79
|
+
disabledTooltip,
|
|
77
80
|
}) => {
|
|
78
81
|
const { formatMessage } = intl || {};
|
|
79
82
|
const [showSlideBox, setShowSlideBox] = useState(false);
|
|
80
83
|
const [savedData, setSavedData] = useState(initialData || null);
|
|
81
84
|
|
|
85
|
+
// On a hard reload of an edit page, initialData is available but savedData is not yet set.
|
|
86
|
+
// This effect ensures that savedData is initialized from initialData in that case.
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (initialData && !savedData) {
|
|
89
|
+
setSavedData(initialData);
|
|
90
|
+
}
|
|
91
|
+
}, [initialData]);
|
|
92
|
+
|
|
82
93
|
const handleSave = useCallback((data) => {
|
|
83
94
|
setSavedData(data);
|
|
84
95
|
setShowSlideBox(false);
|
|
@@ -91,8 +102,9 @@ const CommunicationFlowCard = ({
|
|
|
91
102
|
}, [onCancel]);
|
|
92
103
|
|
|
93
104
|
const handleOpen = useCallback(() => {
|
|
105
|
+
if (disabled) return;
|
|
94
106
|
setShowSlideBox(true);
|
|
95
|
-
}, []);
|
|
107
|
+
}, [disabled]);
|
|
96
108
|
|
|
97
109
|
const firstItem = savedData?.contentItems?.[0];
|
|
98
110
|
const channelConfig = firstItem
|
|
@@ -107,6 +119,9 @@ const CommunicationFlowCard = ({
|
|
|
107
119
|
const senderIdValue = getSenderIdValue(firstItem, savedData);
|
|
108
120
|
const controls = config?.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG;
|
|
109
121
|
const dynamicControlKeys = Object.keys(savedData?.dynamicControls || {});
|
|
122
|
+
// Consumers that set showDynamicControls: false render their own "Advanced
|
|
123
|
+
// controls" section elsewhere, so skip this card's redundant control list.
|
|
124
|
+
const showDynamicControls = config?.features?.showDynamicControls !== false;
|
|
110
125
|
const channel = firstItem.channel?.toUpperCase();
|
|
111
126
|
const senderLabel = [WHATSAPP, RCS].includes(channel)
|
|
112
127
|
? formatMessage(messages.senderNumberLabel)
|
|
@@ -120,7 +135,11 @@ const CommunicationFlowCard = ({
|
|
|
120
135
|
<CapLabel type="label8">Message:</CapLabel>
|
|
121
136
|
<CapLabel type="label2">{strategyLabel}</CapLabel>
|
|
122
137
|
{messageTypeLabel && (
|
|
123
|
-
<CapLabel type="label1">
|
|
138
|
+
<CapLabel type="label1">
|
|
139
|
+
(
|
|
140
|
+
{messageTypeLabel}
|
|
141
|
+
)
|
|
142
|
+
</CapLabel>
|
|
124
143
|
)}
|
|
125
144
|
</CapRow>
|
|
126
145
|
)}
|
|
@@ -135,33 +154,41 @@ const CommunicationFlowCard = ({
|
|
|
135
154
|
bodyStyle={{ padding: 0 }}
|
|
136
155
|
>
|
|
137
156
|
<CapRow gutter={0} className="card-body-row">
|
|
138
|
-
<CapColumn span={12} className="card-body-left-col">
|
|
157
|
+
<CapColumn span={showDynamicControls ? 12 : 24} className="card-body-left-col">
|
|
139
158
|
<CapRow type="flex" align="middle" className="channel-header-row">
|
|
140
159
|
<CapIcon type={channelConfig?.iconType || 'sms'} size="s" />
|
|
141
160
|
<CapLabel type="label8">{channelConfig?.label || firstItem.channel}</CapLabel>
|
|
142
161
|
{senderIdValue && (
|
|
143
|
-
<CapLabel type="label1">
|
|
162
|
+
<CapLabel type="label1">
|
|
163
|
+
(
|
|
164
|
+
{senderLabel}
|
|
165
|
+
:
|
|
166
|
+
{senderIdValue}
|
|
167
|
+
)
|
|
168
|
+
</CapLabel>
|
|
144
169
|
)}
|
|
145
170
|
</CapRow>
|
|
146
171
|
{getContentBody(firstItem)}
|
|
147
172
|
</CapColumn>
|
|
148
|
-
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
{showDynamicControls && (
|
|
174
|
+
<CapColumn span={12} className="card-body-right-col">
|
|
175
|
+
<CapLabel type="label8">{formatMessage(messages.dynamicControlsTitle)}</CapLabel>
|
|
176
|
+
{dynamicControlKeys.map((key) => {
|
|
177
|
+
const controlConfig = controls.find((control) => control.key === key);
|
|
178
|
+
if (!controlConfig) return null;
|
|
179
|
+
return (
|
|
180
|
+
<CapRow key={key} type="flex" justify="space-between" className="control-row">
|
|
181
|
+
<CapLabel type="label2">{controlConfig.label}</CapLabel>
|
|
182
|
+
<CapLabel type="label2">
|
|
183
|
+
{savedData.dynamicControls[key]
|
|
184
|
+
? formatMessage(messages.yes)
|
|
185
|
+
: formatMessage(messages.no)}
|
|
186
|
+
</CapLabel>
|
|
187
|
+
</CapRow>
|
|
188
|
+
);
|
|
189
|
+
})}
|
|
190
|
+
</CapColumn>
|
|
191
|
+
)}
|
|
165
192
|
</CapRow>
|
|
166
193
|
</CapCard>
|
|
167
194
|
);
|
|
@@ -182,9 +209,17 @@ const CommunicationFlowCard = ({
|
|
|
182
209
|
<CapImage src={addCreativesIllustration} />
|
|
183
210
|
</CapColumn>
|
|
184
211
|
<CapColumn span={14} className="empty-card-action-col">
|
|
185
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
212
|
+
{disabled && disabledTooltip ? (
|
|
213
|
+
<CapTooltip title={disabledTooltip}>
|
|
214
|
+
<CapButton type="secondary" disabled>
|
|
215
|
+
{formatMessage(messages.addCreatives)}
|
|
216
|
+
</CapButton>
|
|
217
|
+
</CapTooltip>
|
|
218
|
+
) : (
|
|
219
|
+
<CapButton type="secondary" onClick={handleOpen} disabled={disabled}>
|
|
220
|
+
{formatMessage(messages.addCreatives)}
|
|
221
|
+
</CapButton>
|
|
222
|
+
)}
|
|
188
223
|
</CapColumn>
|
|
189
224
|
</CapRow>
|
|
190
225
|
</CapCard>
|
|
@@ -196,10 +231,10 @@ const CommunicationFlowCard = ({
|
|
|
196
231
|
<CapSlideBox
|
|
197
232
|
show={showSlideBox}
|
|
198
233
|
handleClose={handleClose}
|
|
199
|
-
size="size-
|
|
234
|
+
size="size-l"
|
|
200
235
|
header={(
|
|
201
236
|
<CapHeader
|
|
202
|
-
title={formatMessage(messages.
|
|
237
|
+
title={formatMessage(messages.addContent)}
|
|
203
238
|
handleClose={handleClose}
|
|
204
239
|
/>
|
|
205
240
|
)}
|
|
@@ -227,6 +262,8 @@ CommunicationFlowCard.propTypes = {
|
|
|
227
262
|
onChange: PropTypes.func,
|
|
228
263
|
cap: PropTypes.object,
|
|
229
264
|
intl: PropTypes.object.isRequired,
|
|
265
|
+
disabled: PropTypes.bool, // Disables the "Add creatives" trigger (e.g. until a consumer-required field is filled)
|
|
266
|
+
disabledTooltip: PropTypes.node, // Shown on hover when disabled is true
|
|
230
267
|
};
|
|
231
268
|
|
|
232
269
|
CommunicationFlowCard.defaultProps = {
|
|
@@ -235,6 +272,8 @@ CommunicationFlowCard.defaultProps = {
|
|
|
235
272
|
onCancel: null,
|
|
236
273
|
onChange: null,
|
|
237
274
|
cap: null,
|
|
275
|
+
disabled: false,
|
|
276
|
+
disabledTooltip: null,
|
|
238
277
|
};
|
|
239
278
|
|
|
240
279
|
export default injectIntl(CommunicationFlowCard);
|