@capillarytech/creatives-library 9.0.56-alpha.3 → 9.0.56-alpha.4
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/v2Components/CommonTestAndPreview/actions.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +6 -0
- package/v2Components/CommonTestAndPreview/index.js +22 -20
- 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 +26 -21
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +29 -1
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +16 -3
- package/v2Containers/CommunicationFlow/constants.js +2 -0
- package/v2Containers/CommunicationFlow/index.js +1 -1
- 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
|
@@ -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,18 @@ 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
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
// key: 'couponsCapSaga',
|
|
56
|
-
// saga: CouponsCapContainer.couponsCapSaga,
|
|
57
|
-
// });
|
|
49
|
+
// Self-injects the same 'cap' reducer CreativesContainer already injects under this key
|
|
50
|
+
// (see CreativesContainer/index.js's withCapReducer) — idempotent, so it's a no-op if the
|
|
51
|
+
// consumer's own store already provides it. Guarantees makeSelectAuthenticated() below never
|
|
52
|
+
// reads an undefined domain, regardless of whether a given consumer (e.g. coupons,
|
|
53
|
+
// cartPromotions) wires up its own 'cap' slice.
|
|
54
|
+
const withCapReducer = injectReducer({ key: 'cap', reducer: capReducer });
|
|
58
55
|
|
|
59
56
|
const getDeliveryChannels = (contentItems) => {
|
|
60
57
|
const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
|
|
@@ -68,7 +65,7 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
68
65
|
};
|
|
69
66
|
|
|
70
67
|
// createCommDefinition resolves with the CCS error envelope for 4xx/5xx responses (see api.js request()/checkStatus); duplicate referenceId within the org returns a 409.
|
|
71
|
-
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message ===
|
|
68
|
+
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === REFERENCE_ID_EXISTS;
|
|
72
69
|
|
|
73
70
|
const CommunicationFlow = ({
|
|
74
71
|
config,
|
|
@@ -293,9 +290,19 @@ const CommunicationFlow = ({
|
|
|
293
290
|
<CapDivider />
|
|
294
291
|
</CapRow>
|
|
295
292
|
);
|
|
296
|
-
case STEPS.COMMUNICATION_STRATEGY:
|
|
293
|
+
case STEPS.COMMUNICATION_STRATEGY: {
|
|
294
|
+
// No visible divider line when there's no content yet (matches the empty-state
|
|
295
|
+
// design), but the vertical gap above the Channel card must stay either way —
|
|
296
|
+
// see the `.communication-strategy-row` spacing rule in CommunicationFlow.scss,
|
|
297
|
+
// which is not tied to whether the divider itself renders.
|
|
298
|
+
const templateStepFollows = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
|
|
299
|
+
const hasContent = stepData.contentItems?.length > 0;
|
|
297
300
|
return (
|
|
298
|
-
<CapRow
|
|
301
|
+
<CapRow
|
|
302
|
+
useLegacy
|
|
303
|
+
key={step}
|
|
304
|
+
className={(!templateStepFollows || hasContent) ? '' : 'communication-strategy-row--no-divider'}
|
|
305
|
+
>
|
|
299
306
|
<CommunicationStrategyStep
|
|
300
307
|
{...commonProps}
|
|
301
308
|
value={stepData.communicationStrategy}
|
|
@@ -304,9 +311,10 @@ const CommunicationFlow = ({
|
|
|
304
311
|
disabled={communicationStrategyData.disabled}
|
|
305
312
|
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
306
313
|
/>
|
|
307
|
-
<CapDivider />
|
|
314
|
+
{(!templateStepFollows || hasContent) && <CapDivider />}
|
|
308
315
|
</CapRow>
|
|
309
316
|
);
|
|
317
|
+
}
|
|
310
318
|
case STEPS.CHANNEL_SELECTION:
|
|
311
319
|
// Only show ChannelSelectionStep if communication strategy is selected
|
|
312
320
|
if (!stepData.communicationStrategy) {
|
|
@@ -322,7 +330,6 @@ const CommunicationFlow = ({
|
|
|
322
330
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
323
331
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
324
332
|
disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
|
|
325
|
-
autoOpenExistingContent={!!contentTemplateData.autoOpenExistingContent}
|
|
326
333
|
creativesMode={config.mode || 'create'}
|
|
327
334
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
328
335
|
incentivesData={{
|
|
@@ -376,7 +383,7 @@ const CommunicationFlow = ({
|
|
|
376
383
|
|
|
377
384
|
CommunicationFlow.propTypes = {
|
|
378
385
|
config: PropTypes.shape({
|
|
379
|
-
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona']).isRequired,
|
|
386
|
+
consumer: PropTypes.oneOf(['campaigns', 'loyalty', 'adiona', 'coupons', 'cartPromotions']).isRequired,
|
|
380
387
|
channel: PropTypes.string,
|
|
381
388
|
mode: PropTypes.oneOf(['create', 'edit', 'preview']).isRequired,
|
|
382
389
|
channelsToHide: PropTypes.arrayOf(PropTypes.string),
|
|
@@ -411,7 +418,6 @@ CommunicationFlow.propTypes = {
|
|
|
411
418
|
required: PropTypes.bool,
|
|
412
419
|
channels: PropTypes.object,
|
|
413
420
|
disablePreviewAndTest: PropTypes.bool,
|
|
414
|
-
autoOpenExistingContent: PropTypes.bool,
|
|
415
421
|
}),
|
|
416
422
|
enableIncentives: PropTypes.bool,
|
|
417
423
|
enableDeliverySettings: PropTypes.bool,
|
|
@@ -444,8 +450,7 @@ const mapStateToProps = createStructuredSelector({
|
|
|
444
450
|
const withConnect = connect(mapStateToProps);
|
|
445
451
|
|
|
446
452
|
export default compose(
|
|
447
|
-
|
|
448
|
-
// withCouponsSaga, // cap-coupons flows disabled
|
|
453
|
+
withCapReducer,
|
|
449
454
|
withConnect,
|
|
450
455
|
injectIntl,
|
|
451
456
|
)(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
|
}
|
|
@@ -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
|
});
|
|
@@ -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),
|
|
@@ -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
|