@capillarytech/creatives-library 9.0.56-alpha.3 → 9.0.56-alpha.5
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/package.json +1 -1
- package/services/api.js +8 -6
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +2 -1
- package/v2Components/CommonTestAndPreview/actions.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +6 -0
- package/v2Components/CommonTestAndPreview/index.js +23 -21
- 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/actions.test.js +15 -0
- 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/TestAndPreviewSlidebox/index.js +3 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +40 -37
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +29 -1
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +2 -4
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +16 -3
- package/v2Containers/CommunicationFlow/constants.js +12 -8
- package/v2Containers/CommunicationFlow/index.js +6 -8
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +2 -22
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
- package/v2Containers/CreativesContainer/index.js +0 -1
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +79 -7
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -0
- package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
- package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +204 -0
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
makeSelectMessageMetaConfigId,
|
|
37
37
|
makeSelectPrefilledValues,
|
|
38
38
|
makeSelectIsSendingTestMessage,
|
|
39
|
+
makeSelectIsSendingCcsTestMessage,
|
|
39
40
|
makeSelectUpdatePreviewError,
|
|
40
41
|
makeSelectUpdatePreviewErrors,
|
|
41
42
|
makeSelectFetchPrefilledValuesError,
|
|
@@ -103,6 +104,7 @@ TestAndPreviewSlidebox.propTypes = {
|
|
|
103
104
|
messageMetaConfigId: PropTypes.string,
|
|
104
105
|
prefilledValues: PropTypes.object,
|
|
105
106
|
isSendingTestMessage: PropTypes.bool.isRequired,
|
|
107
|
+
isSendingCcsTestMessage: PropTypes.bool.isRequired,
|
|
106
108
|
intl: PropTypes.object.isRequired,
|
|
107
109
|
senderDetailsByChannel: PropTypes.object,
|
|
108
110
|
wecrmAccounts: PropTypes.array,
|
|
@@ -151,6 +153,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
151
153
|
fetchPrefilledValuesError: makeSelectFetchPrefilledValuesError(),
|
|
152
154
|
fetchPrefilledValuesErrors: makeSelectFetchPrefilledValuesErrors(),
|
|
153
155
|
isSendingTestMessage: makeSelectIsSendingTestMessage(),
|
|
156
|
+
isSendingCcsTestMessage: makeSelectIsSendingCcsTestMessage(),
|
|
154
157
|
senderDetailsByChannel: makeSelectSenderDetailsByChannel(),
|
|
155
158
|
wecrmAccounts: makeSelectWeCrmAccounts(),
|
|
156
159
|
isLoadingSenderDetails: makeSelectIsLoadingSenderDetails(),
|
|
@@ -13,14 +13,13 @@ 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
19
|
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
21
|
-
|
|
22
|
-
// import injectReducer from '../../utils/injectReducer';
|
|
20
|
+
import injectReducer from '../../utils/injectReducer';
|
|
23
21
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
22
|
+
import capReducer from '../Cap/reducer';
|
|
24
23
|
import { createCommDefinition, editCommDefinition } from '../../services/api';
|
|
25
24
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
26
25
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
@@ -41,20 +40,15 @@ import {
|
|
|
41
40
|
CCS_CHANNEL_DELIVERY_KEY_MAP,
|
|
42
41
|
CCS_CHANNEL_NAME_MAP,
|
|
43
42
|
CCS_CONTENT_TRANSFORMS,
|
|
43
|
+
REFERENCE_ID_EXISTS,
|
|
44
44
|
} from './constants';
|
|
45
45
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
46
46
|
import messages from './messages';
|
|
47
47
|
import './CommunicationFlow.scss';
|
|
48
48
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
// reducer: CouponsCapContainer.couponsCapReducer,
|
|
53
|
-
// });
|
|
54
|
-
// const withCouponsSaga = injectSaga({
|
|
55
|
-
// key: 'couponsCapSaga',
|
|
56
|
-
// saga: CouponsCapContainer.couponsCapSaga,
|
|
57
|
-
// });
|
|
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 });
|
|
58
52
|
|
|
59
53
|
const getDeliveryChannels = (contentItems) => {
|
|
60
54
|
const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
|
|
@@ -67,8 +61,8 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
67
61
|
return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
|
|
68
62
|
};
|
|
69
63
|
|
|
70
|
-
//
|
|
71
|
-
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message ===
|
|
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;
|
|
72
66
|
|
|
73
67
|
const CommunicationFlow = ({
|
|
74
68
|
config,
|
|
@@ -165,12 +159,10 @@ const CommunicationFlow = ({
|
|
|
165
159
|
if (shouldUseCCS) {
|
|
166
160
|
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
|
|
167
161
|
const contentItem = (aggregatedData.contentItems || [])[0];
|
|
168
|
-
// Consumer-supplied
|
|
169
|
-
// has no name input of its own, so this is mandatory input from config.context.
|
|
162
|
+
// Consumer-supplied (e.g. CapNotify's Alert Name field) — CommunicationFlow has no name input of its own.
|
|
170
163
|
const name = config?.context?.name;
|
|
171
164
|
|
|
172
|
-
// CCS create
|
|
173
|
-
// carry multiple content items with no CCS equivalent yet.
|
|
165
|
+
// CCS create only supports SINGLE strategy; CHANNEL_PRIORITY/AB_TEST have no CCS equivalent yet.
|
|
174
166
|
if (!isMultiChannel && contentItem && name) {
|
|
175
167
|
const rawChannel = (contentItem.channel || '').toUpperCase();
|
|
176
168
|
const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
|
|
@@ -180,8 +172,7 @@ const CommunicationFlow = ({
|
|
|
180
172
|
const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
|
|
181
173
|
const { dynamicControls = {} } = aggregatedData;
|
|
182
174
|
const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
|
|
183
|
-
//
|
|
184
|
-
// approval) — sent as '' rather than auto-generated/omitted when left blank.
|
|
175
|
+
// Optional fields — sent as '' rather than omitted when left blank.
|
|
185
176
|
const referenceId = config?.context?.referenceId || '';
|
|
186
177
|
const description = config?.context?.description || '';
|
|
187
178
|
|
|
@@ -202,29 +193,35 @@ const CommunicationFlow = ({
|
|
|
202
193
|
singleChannelStrategy: {
|
|
203
194
|
variant: {
|
|
204
195
|
channel,
|
|
205
|
-
// `channel`
|
|
196
|
+
// `channel` spread last so CCS's normalized enum overrides the UI's internal identifier.
|
|
206
197
|
...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
|
|
207
198
|
...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
|
|
208
199
|
},
|
|
209
200
|
},
|
|
210
201
|
};
|
|
211
202
|
|
|
212
|
-
//
|
|
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.
|
|
213
205
|
const { existingCommDefinitionId } = config?.context || {};
|
|
206
|
+
const {
|
|
207
|
+
referenceId: payloadReferenceId,
|
|
208
|
+
description: payloadDescription,
|
|
209
|
+
strategyType,
|
|
210
|
+
settings,
|
|
211
|
+
singleChannelStrategy,
|
|
212
|
+
} = payload;
|
|
214
213
|
|
|
215
214
|
try {
|
|
216
215
|
const res = existingCommDefinitionId
|
|
217
216
|
? await editCommDefinition(existingCommDefinitionId, {
|
|
218
|
-
referenceId:
|
|
219
|
-
description:
|
|
220
|
-
strategyType
|
|
221
|
-
settings
|
|
222
|
-
singleChannelStrategy
|
|
217
|
+
referenceId: payloadReferenceId,
|
|
218
|
+
description: payloadDescription,
|
|
219
|
+
strategyType,
|
|
220
|
+
settings,
|
|
221
|
+
singleChannelStrategy,
|
|
223
222
|
})
|
|
224
223
|
: await createCommDefinition(payload);
|
|
225
224
|
if (isDuplicateReferenceIdError(res)) {
|
|
226
|
-
// Duplicate referenceId in this org — block the save so the user can
|
|
227
|
-
// change it, rather than silently proceeding without a CCS comm.
|
|
228
225
|
setSaveError(formatMessage(messages.duplicateReferenceIdError));
|
|
229
226
|
return;
|
|
230
227
|
}
|
|
@@ -293,9 +290,17 @@ const CommunicationFlow = ({
|
|
|
293
290
|
<CapDivider />
|
|
294
291
|
</CapRow>
|
|
295
292
|
);
|
|
296
|
-
case STEPS.COMMUNICATION_STRATEGY:
|
|
293
|
+
case STEPS.COMMUNICATION_STRATEGY: {
|
|
294
|
+
// No divider when there's no content yet, but the spacing above the Channel
|
|
295
|
+
// card must stay either way — see .communication-strategy-row in the scss.
|
|
296
|
+
const templateStepFollows = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
|
|
297
|
+
const hasContent = stepData.contentItems?.length > 0;
|
|
297
298
|
return (
|
|
298
|
-
<CapRow
|
|
299
|
+
<CapRow
|
|
300
|
+
useLegacy
|
|
301
|
+
key={step}
|
|
302
|
+
className={(!templateStepFollows || hasContent) ? '' : 'communication-strategy-row--no-divider'}
|
|
303
|
+
>
|
|
299
304
|
<CommunicationStrategyStep
|
|
300
305
|
{...commonProps}
|
|
301
306
|
value={stepData.communicationStrategy}
|
|
@@ -304,9 +309,10 @@ const CommunicationFlow = ({
|
|
|
304
309
|
disabled={communicationStrategyData.disabled}
|
|
305
310
|
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
306
311
|
/>
|
|
307
|
-
<CapDivider />
|
|
312
|
+
{(!templateStepFollows || hasContent) && <CapDivider />}
|
|
308
313
|
</CapRow>
|
|
309
314
|
);
|
|
315
|
+
}
|
|
310
316
|
case STEPS.CHANNEL_SELECTION:
|
|
311
317
|
// Only show ChannelSelectionStep if communication strategy is selected
|
|
312
318
|
if (!stepData.communicationStrategy) {
|
|
@@ -322,7 +328,6 @@ const CommunicationFlow = ({
|
|
|
322
328
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
323
329
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
324
330
|
disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
|
|
325
|
-
autoOpenExistingContent={!!contentTemplateData.autoOpenExistingContent}
|
|
326
331
|
creativesMode={config.mode || 'create'}
|
|
327
332
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
328
333
|
incentivesData={{
|
|
@@ -376,7 +381,7 @@ const CommunicationFlow = ({
|
|
|
376
381
|
|
|
377
382
|
CommunicationFlow.propTypes = {
|
|
378
383
|
config: PropTypes.shape({
|
|
379
|
-
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
384
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
|
|
380
385
|
channel: PropTypes.string,
|
|
381
386
|
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
382
387
|
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
@@ -411,7 +416,6 @@ CommunicationFlow.propTypes = {
|
|
|
411
416
|
required: PropTypes.bool,
|
|
412
417
|
channels: PropTypes.object,
|
|
413
418
|
disablePreviewAndTest: PropTypes.bool,
|
|
414
|
-
autoOpenExistingContent: PropTypes.bool,
|
|
415
419
|
}),
|
|
416
420
|
enableIncentives: PropTypes.bool,
|
|
417
421
|
enableDeliverySettings: PropTypes.bool,
|
|
@@ -444,8 +448,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
444
448
|
const withConnect = connect(mapStateToProps);
|
|
445
449
|
|
|
446
450
|
export default compose(
|
|
447
|
-
|
|
448
|
-
// withCouponsSaga, // cap-coupons flows disabled
|
|
451
|
+
withCapReducer,
|
|
449
452
|
withConnect,
|
|
450
453
|
injectIntl,
|
|
451
454
|
)(CommunicationFlow);
|
|
@@ -128,15 +128,43 @@
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
.communication-flow-container {
|
|
131
|
+
// Anchors the Save footer to the bottom of the slidebox's scrollable content
|
|
132
|
+
// area regardless of how much step content is above it. <CapRow useLegacy>
|
|
133
|
+
// carries cap-ui-library's `.ant-row-legacy` rule (`display: block !important`),
|
|
134
|
+
// which silently defeats a plain `display:flex`/`min-height` here unless the
|
|
135
|
+
// selector is compounded with `.ant-row-legacy` and marked `!important` too.
|
|
136
|
+
// Deliberately position-based, not flex + margin-top: auto — a flex version
|
|
137
|
+
// makes this element (and its .ant-row-legacy children) a flex container, and
|
|
138
|
+
// flexbox's default align-items: stretch leaks into deeply nested descendants
|
|
139
|
+
// (e.g. ChannelSelectionStep's fixed-width empty-state card), stretching them
|
|
140
|
+
// to fill the container instead of keeping their own size.
|
|
141
|
+
&.ant-row-legacy {
|
|
142
|
+
position: relative !important;
|
|
143
|
+
min-height: calc(100vh - 6.571rem) !important;
|
|
144
|
+
padding-bottom: 6rem !important;
|
|
145
|
+
}
|
|
146
|
+
|
|
131
147
|
.step-divider {
|
|
132
148
|
margin: $CAP_SPACE_32 0;
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
// CommunicationStrategyStep's own trailing divider is hidden until content
|
|
152
|
+
// exists (no dangling line above an empty "Add creative" state), but the gap
|
|
153
|
+
// it would have provided (.step-divider's margin) must stay — otherwise the
|
|
154
|
+
// section sits flush against the Channel card in the empty state.
|
|
155
|
+
.communication-strategy-row--no-divider {
|
|
156
|
+
margin-bottom: $CAP_SPACE_32;
|
|
157
|
+
}
|
|
158
|
+
|
|
135
159
|
.heading-style {
|
|
136
160
|
margin-bottom: $CAP_SPACE_08;
|
|
137
161
|
}
|
|
138
162
|
|
|
139
|
-
&__footer {
|
|
163
|
+
&__footer.ant-row-legacy {
|
|
164
|
+
position: absolute !important;
|
|
165
|
+
left: 0;
|
|
166
|
+
right: 0;
|
|
167
|
+
bottom: 0 !important;
|
|
140
168
|
padding: 3.142rem 0 $CAP_SPACE_08;
|
|
141
169
|
}
|
|
142
170
|
}
|
|
@@ -111,10 +111,8 @@ const CommunicationFlowCard = ({
|
|
|
111
111
|
const senderIdValue = getSenderIdValue(firstItem, savedData);
|
|
112
112
|
const controls = config?.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG;
|
|
113
113
|
const dynamicControlKeys = Object.keys(savedData?.dynamicControls || {});
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// false render their own "Advanced controls" section elsewhere, so this summary
|
|
117
|
-
// card's right column would otherwise show a redundant, always-empty control list.
|
|
114
|
+
// Consumers that set showDynamicControls: false render their own "Advanced
|
|
115
|
+
// controls" section elsewhere, so skip this card's redundant control list.
|
|
118
116
|
const showDynamicControls = config?.features?.showDynamicControls !== false;
|
|
119
117
|
const channel = firstItem.channel?.toUpperCase();
|
|
120
118
|
const senderLabel = [WHATSAPP, RCS].includes(channel)
|
|
@@ -1029,9 +1029,22 @@ describe('step dividers', () => {
|
|
|
1029
1029
|
contentItems: [],
|
|
1030
1030
|
},
|
|
1031
1031
|
});
|
|
1032
|
-
//
|
|
1033
|
-
//
|
|
1034
|
-
// empty "Add creative" state.
|
|
1032
|
+
// Neither divider renders — both CommunicationStrategyStep's and ChannelSelectionStep's
|
|
1033
|
+
// own trailing dividers are conditional on content existing, to avoid a dangling line
|
|
1034
|
+
// around an empty "Add creative" state.
|
|
1035
|
+
expect(container.querySelectorAll('.ant-divider')).toHaveLength(0);
|
|
1036
|
+
});
|
|
1037
|
+
|
|
1038
|
+
it('renders the divider after CommunicationStrategyStep unconditionally when no CHANNEL_SELECTION step follows', () => {
|
|
1039
|
+
const { container } = renderWithFlow({
|
|
1040
|
+
features: {
|
|
1041
|
+
communicationStrategyData: { required: true, options: STRATEGY_OPTIONS },
|
|
1042
|
+
// contentTemplateData.required omitted — no CHANNEL_SELECTION step in this flow.
|
|
1043
|
+
},
|
|
1044
|
+
initialData: {
|
|
1045
|
+
communicationStrategy: SINGLE_TEMPLATE,
|
|
1046
|
+
},
|
|
1047
|
+
});
|
|
1035
1048
|
expect(container.querySelectorAll('.ant-divider')).toHaveLength(1);
|
|
1036
1049
|
});
|
|
1037
1050
|
});
|
|
@@ -38,15 +38,16 @@ export const CHANNEL_PRIORITY = 'CHANNEL_PRIORITY';
|
|
|
38
38
|
export const AB_TEST = 'AB_TEST';
|
|
39
39
|
export const SINGLE_TEMPLATE = 'SINGLE_TEMPLATE';
|
|
40
40
|
|
|
41
|
-
// CCS
|
|
41
|
+
// CCS create is only supported for SINGLE (CHANNEL_PRIORITY/AB_TEST skip CCS entirely).
|
|
42
42
|
export const CCS_STRATEGY_TYPE_SINGLE = 'SINGLE';
|
|
43
43
|
|
|
44
|
-
// Mirrors CCS SingleChannelStrategy field names
|
|
44
|
+
// Mirrors CCS SingleChannelStrategy field names; kept separate from the legacy
|
|
45
|
+
// messageMeta CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP.
|
|
45
46
|
export const CCS_CHANNEL_CONTENT_KEY_MAP = {
|
|
46
47
|
SMS: 'smsMessageContent',
|
|
47
48
|
EMAIL: 'emailMessageContent',
|
|
48
49
|
WHATSAPP: 'whatsappMessageContent',
|
|
49
|
-
// CCS's channel enum
|
|
50
|
+
// CCS's channel enum for mobile push is 'PUSH', but content/delivery keys stay 'mpush*'.
|
|
50
51
|
PUSH: 'mpushMessageContent',
|
|
51
52
|
INAPP: 'inAppMessageContent',
|
|
52
53
|
ANDROID: 'androidMessageContent',
|
|
@@ -58,17 +59,16 @@ export const CCS_CHANNEL_CONTENT_KEY_MAP = {
|
|
|
58
59
|
RCS: 'rcsMessageContent',
|
|
59
60
|
};
|
|
60
61
|
|
|
61
|
-
//
|
|
62
|
+
// Transforms legacy messageMeta field names to CCS's before sending, for channels
|
|
63
|
+
// where the two shapes don't already match.
|
|
62
64
|
export const CCS_CONTENT_TRANSFORMS = {
|
|
63
65
|
EMAIL: (templateData = {}) => ({
|
|
64
66
|
messageSubject: templateData.emailSubject,
|
|
65
67
|
messageBody: templateData.emailBody || templateData.emailHtml,
|
|
66
68
|
}),
|
|
67
|
-
// SMS: legacy stores text as `messageBody`, while CCS expects `message`; map the field explicitly to avoid CCS returning smsMessageContent.message as null.
|
|
68
69
|
SMS: (templateData = {}) => ({
|
|
69
70
|
message: templateData.messageBody || '',
|
|
70
71
|
}),
|
|
71
|
-
// WEBPUSH: the legacy editor nests data under messageContent.content; flatten it to CCS's { messageSubject, accountId, content } shape and drop the legacy-only `offers` field.
|
|
72
72
|
WEBPUSH: (templateData = {}) => {
|
|
73
73
|
const inner = templateData.messageContent?.content || templateData;
|
|
74
74
|
return {
|
|
@@ -94,12 +94,14 @@ export const CCS_CHANNEL_DELIVERY_KEY_MAP = {
|
|
|
94
94
|
RCS: 'rcsDeliverySettings',
|
|
95
95
|
};
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// UI uses `MOBILEPUSH`; CCS uses `PUSH` for the channel enum. Other channels pass through.
|
|
98
98
|
export const CCS_CHANNEL_NAME_MAP = {
|
|
99
99
|
MOBILEPUSH: 'PUSH',
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
-
// Channel config — single source of truth for CreativesContainer and TemplatesV2
|
|
102
|
+
// Channel config — single source of truth for CreativesContainer and TemplatesV2.
|
|
103
|
+
// paneKey: TemplatesV2 defaultPanes key used by channelsToHide.
|
|
104
|
+
// channelProp: CreativesContainer channel prop that must match pane.key for the tab to be active.
|
|
103
105
|
export const CHANNELS = [
|
|
104
106
|
{
|
|
105
107
|
value: 'SMS',
|
|
@@ -292,3 +294,5 @@ export const INCENTIVE_TYPES = [
|
|
|
292
294
|
value: 'badges', label: <FormattedMessage {...messages.badges} />, isActive: false, disabled: false,
|
|
293
295
|
},
|
|
294
296
|
];
|
|
297
|
+
|
|
298
|
+
export const REFERENCE_ID_EXISTS = 'REFERENCE_ID_EXISTS';
|
|
@@ -30,7 +30,7 @@ const CommunicationFlowContainer = ({
|
|
|
30
30
|
|
|
31
31
|
CommunicationFlowContainer.propTypes = {
|
|
32
32
|
config: PropTypes.shape({
|
|
33
|
-
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
33
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
|
|
34
34
|
channel: PropTypes.string, // optional if channel step is skipped
|
|
35
35
|
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
36
36
|
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
@@ -80,15 +80,13 @@ CommunicationFlowContainer.propTypes = {
|
|
|
80
80
|
controls: PropTypes.array,
|
|
81
81
|
}),
|
|
82
82
|
}),
|
|
83
|
-
context: PropTypes.object, // ouId, campaignId, programId
|
|
84
|
-
//
|
|
85
|
-
useCCS: PropTypes.bool, // If false, skips the CCS createCommDefinition call on save. Defaults to true.
|
|
83
|
+
context: PropTypes.object, // ouId, campaignId, programId, name, referenceId, description
|
|
84
|
+
useCCS: PropTypes.bool, // skips the CCS createCommDefinition call on save when false; defaults to true
|
|
86
85
|
}).isRequired,
|
|
87
86
|
initialData: PropTypes.object, // for edit/preview mode
|
|
88
|
-
onSave: PropTypes.func.isRequired, // (data) => void
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
onChange: PropTypes.func, // (data) => void - optional, called on data changes
|
|
87
|
+
onSave: PropTypes.func.isRequired, // (data) => void, data.ccsCommDefinition set when CCS create succeeded
|
|
88
|
+
onCancel: PropTypes.func.isRequired, // () => void
|
|
89
|
+
onChange: PropTypes.func, // (data) => void
|
|
92
90
|
};
|
|
93
91
|
|
|
94
92
|
CommunicationFlowContainer.defaultProps = {
|
|
@@ -4,9 +4,7 @@
|
|
|
4
4
|
* Content template selection with channel dropdown
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
8
|
-
useState, useCallback, useMemo, useEffect, useRef,
|
|
9
|
-
} from 'react';
|
|
7
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
10
8
|
import PropTypes from 'prop-types';
|
|
11
9
|
import { injectIntl } from 'react-intl';
|
|
12
10
|
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
@@ -48,7 +46,6 @@ const ChannelSelectionStep = ({
|
|
|
48
46
|
channelsToHide = [],
|
|
49
47
|
channelsToDisable = [],
|
|
50
48
|
disablePreviewAndTest = false,
|
|
51
|
-
autoOpenExistingContent = false,
|
|
52
49
|
creativesMode = 'create',
|
|
53
50
|
selectedOfferDetails = [],
|
|
54
51
|
incentivesData,
|
|
@@ -121,21 +118,6 @@ const ChannelSelectionStep = ({
|
|
|
121
118
|
}
|
|
122
119
|
}, [contentItems]);
|
|
123
120
|
|
|
124
|
-
// Editing an existing single-channel alert (e.g. CapNotify's rejected-alert edit page) should
|
|
125
|
-
// land straight in that channel's template editor, not the one-card content list — there's
|
|
126
|
-
// only ever one item under the SINGLE strategy, so the list is just an extra click. Runs once
|
|
127
|
-
// on mount only; the ref guard keeps a later content-item change (e.g. after closing/re-saving)
|
|
128
|
-
// from re-triggering the auto-open.
|
|
129
|
-
const hasAutoOpenedRef = useRef(false);
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (hasAutoOpenedRef.current) return;
|
|
132
|
-
hasAutoOpenedRef.current = true;
|
|
133
|
-
if (autoOpenExistingContent && contentItems.length > 0) {
|
|
134
|
-
handleEditContent(contentItems[0].contentId);
|
|
135
|
-
}
|
|
136
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
137
|
-
}, []);
|
|
138
|
-
|
|
139
121
|
const handleDeleteContent = useCallback((contentId) => {
|
|
140
122
|
onChange({ contentItems: contentItems.filter((c) => c.contentId !== contentId) });
|
|
141
123
|
}, [contentItems, onChange]);
|
|
@@ -437,7 +419,7 @@ const ChannelSelectionStep = ({
|
|
|
437
419
|
)}
|
|
438
420
|
|
|
439
421
|
{contentItems?.length === 0 ? (
|
|
440
|
-
<CapRow className=
|
|
422
|
+
<CapRow className={`content-template-section ${contentItems?.length === 0 ? 'no-content-items' : ''}`}>
|
|
441
423
|
<CapDropdown
|
|
442
424
|
overlay={renderChannelDropdownOverlay() || <CapMenu />}
|
|
443
425
|
onVisibleChange={handleChannelsVisibleChange}
|
|
@@ -571,7 +553,6 @@ ChannelSelectionStep.propTypes = {
|
|
|
571
553
|
channelsToHide: PropTypes.array,
|
|
572
554
|
channelsToDisable: PropTypes.array,
|
|
573
555
|
disablePreviewAndTest: PropTypes.bool,
|
|
574
|
-
autoOpenExistingContent: PropTypes.bool,
|
|
575
556
|
creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
|
|
576
557
|
selectedOfferDetails: PropTypes.array,
|
|
577
558
|
incentivesData: PropTypes.shape({
|
|
@@ -594,7 +575,6 @@ ChannelSelectionStep.defaultProps = {
|
|
|
594
575
|
channelsToHide: [],
|
|
595
576
|
channelsToDisable: [],
|
|
596
577
|
disablePreviewAndTest: false,
|
|
597
|
-
autoOpenExistingContent: false,
|
|
598
578
|
creativesMode: 'create',
|
|
599
579
|
selectedOfferDetails: [],
|
|
600
580
|
incentivesData: null,
|
|
@@ -164,7 +164,6 @@ export class Creatives extends React.Component {
|
|
|
164
164
|
isFullMode: props.isFullMode,
|
|
165
165
|
useLocalTemplates,
|
|
166
166
|
});
|
|
167
|
-
|
|
168
167
|
this.state = {
|
|
169
168
|
isLoadingContent: true,
|
|
170
169
|
templateStep: 1, //modeSelection: for template name and (for email selecting upload or editor), and createTemplate to create template create based on modeSelection
|