@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.
Files changed (26) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  3. package/v2Components/CommonTestAndPreview/constants.js +6 -0
  4. package/v2Components/CommonTestAndPreview/index.js +22 -20
  5. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  6. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  7. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  8. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  9. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  10. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  11. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  12. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  13. package/v2Components/TestAndPreviewSlidebox/index.js +3 -0
  14. package/v2Containers/CommunicationFlow/CommunicationFlow.js +26 -21
  15. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +29 -1
  16. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +16 -3
  17. package/v2Containers/CommunicationFlow/constants.js +2 -0
  18. package/v2Containers/CommunicationFlow/index.js +1 -1
  19. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +2 -22
  20. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  21. package/v2Containers/CreativesContainer/index.js +0 -1
  22. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +79 -7
  23. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -0
  24. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  25. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  26. 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
- // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
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
- // Inject couponsCap reducer and saga - commented out (cap-coupons flows disabled)
50
- // const withCouponsReducer = injectReducer({
51
- // key: 'couponsCap',
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
+ // (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 === 'REFERENCE_ID_EXISTS';
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 useLegacy key={step}>
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
- // withCouponsReducer, // cap-coupons flows disabled
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
- // Only CommunicationStrategyStep's divider renders — ChannelSelectionStep's own
1033
- // divider is conditional on content existing, to avoid a dangling line above an
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
  });
@@ -292,3 +292,5 @@ export const INCENTIVE_TYPES = [
292
292
  value: 'badges', label: <FormattedMessage {...messages.badges} />, isActive: false, disabled: false,
293
293
  },
294
294
  ];
295
+
296
+ 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),
@@ -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="content-template-section">
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,
@@ -130,6 +130,10 @@
130
130
  border-bottom: 1px solid $CAP_G07;
131
131
  }
132
132
 
133
+ .no-content-items {
134
+ border-bottom: none;
135
+ }
136
+
133
137
  .add-content-template-button {
134
138
  padding: $CAP_SPACE_12 $CAP_SPACE_16;
135
139
  align-self: center !important;
@@ -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