@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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +8 -6
  3. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +2 -1
  4. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  5. package/v2Components/CommonTestAndPreview/constants.js +6 -0
  6. package/v2Components/CommonTestAndPreview/index.js +23 -21
  7. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  8. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  9. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  10. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  11. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  12. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  13. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  14. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  15. package/v2Components/TestAndPreviewSlidebox/index.js +3 -0
  16. package/v2Containers/CommunicationFlow/CommunicationFlow.js +40 -37
  17. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +29 -1
  18. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +2 -4
  19. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +16 -3
  20. package/v2Containers/CommunicationFlow/constants.js +12 -8
  21. package/v2Containers/CommunicationFlow/index.js +6 -8
  22. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +2 -22
  23. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  24. package/v2Containers/CreativesContainer/index.js +0 -1
  25. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +79 -7
  26. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +18 -0
  27. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  28. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  29. 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
- // 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,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
- // 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
+ // 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
- // 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';
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 name (e.g. CapNotify's Alert Name field) — CommunicationFlow
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 is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
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
- // referenceId/description are user-editable, optional fields (up until Send for
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` must be spread last in both objects so CCS's normalized enum (e.g. `PUSH`) overrides the UI's internal identifier (e.g. `MOBILEPUSH`) from templateData.
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
- // Editing an existing alert (e.g. rejected) uses config.context.existingCommDefinitionId to create a new DRAFT version on the same CommDefinition, preserving its id/referenceId instead of creating a new alert.
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: payload.referenceId,
219
- description: payload.description,
220
- strategyType: payload.strategyType,
221
- settings: payload.settings,
222
- singleChannelStrategy: payload.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 useLegacy key={step}>
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
- // withCouponsReducer, // cap-coupons flows disabled
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
- // Mirrors getEnabledSteps.js's own gate on this flag (which skips the in-slidebox
115
- // DynamicControlsStep) consumers like CapNotify that set showDynamicControls:
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
- // 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
  });
@@ -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 CommDefinition strategyType — distinct from SINGLE_TEMPLATE above, which is this UI's own communicationStrategy value. Only SINGLE is supported this phase; CCS create is skipped entirely for CHANNEL_PRIORITY/AB_TEST.
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 exactly (per Cap Notify API design samples and real create responses); kept separate from the legacy messageMeta CHANNEL_CONTENT_KEY_MAP/CHANNEL_DELIVERY_KEY_MAP.
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 value for mobile push is 'PUSH' (verified against real create responses), even though the content/delivery keys stay 'mpush*'.
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
- // contentItem.templateData uses legacy messageMeta field names, so transform mismatched channel fields before sending to CCS; for EMAIL, map legacy `emailSubject`/`emailBody` to CCS's `messageSubject`/`messageBody`.
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
- // The UI uses `MOBILEPUSH`, while CCS uses `PUSH` for the channel enum (verified against real create responses); normalize this value before building the CCS payload, while retaining the `mpush*` content/delivery keys. All other channel identifiers pass through unchanged.
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; paneKey is the TemplatesV2 defaultPanes key used by channelsToHide, and channelProp is the CreativesContainer channel prop that must match pane.key for the tab to be active.
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; name (required for CCS create),
84
- // referenceId (optional, auto-generated from name if absent), description (optional).
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 - called when user saves; data.ccsCommDefinition
89
- // ({id, referenceId, version, status}) is present when the CCS create succeeded
90
- onCancel: PropTypes.func.isRequired, // () => void - called when user cancels
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="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