@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.
Files changed (91) hide show
  1. package/constants/unified.js +7 -1
  2. package/package.json +1 -1
  3. package/services/api.js +25 -4
  4. package/utils/common.js +0 -4
  5. package/utils/templateVarUtils.js +16 -0
  6. package/utils/tests/templateVarUtils.test.js +44 -0
  7. package/v2Components/CapActionButton/index.js +73 -60
  8. package/v2Components/CapActionButton/index.scss +44 -28
  9. package/v2Components/CapActionButton/messages.js +7 -3
  10. package/v2Components/CapActionButton/tests/index.test.js +17 -1
  11. package/v2Components/CapWhatsappCTA/messages.js +4 -0
  12. package/v2Components/CapWhatsappCarouselButton/index.js +42 -33
  13. package/v2Components/CapWhatsappCarouselButton/index.scss +44 -2
  14. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  15. package/v2Components/CommonTestAndPreview/actions.js +13 -0
  16. package/v2Components/CommonTestAndPreview/constants.js +8 -0
  17. package/v2Components/CommonTestAndPreview/index.js +55 -3
  18. package/v2Components/CommonTestAndPreview/reducer.js +20 -0
  19. package/v2Components/CommonTestAndPreview/sagas.js +28 -0
  20. package/v2Components/CommonTestAndPreview/selectors.js +6 -0
  21. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  22. package/v2Components/CommonTestAndPreview/tests/actions.test.js +15 -0
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +101 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +41 -0
  26. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +84 -0
  27. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +12 -0
  28. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +36 -22
  29. package/v2Components/FormBuilder/Functional/channels/mobilepush/config.js +5 -13
  30. package/v2Components/FormBuilder/Functional/channels/mobilepush/modals.js +6 -4
  31. package/v2Components/FormBuilder/Functional/channels/mobilepush/validate.js +13 -14
  32. package/v2Components/FormBuilder/Functional/renderers/mpushRenderers.js +15 -8
  33. package/v2Components/FormBuilder/Functional/tests/mpush.ctaFlows.parity.test.js +31 -0
  34. package/v2Components/FormBuilder/Functional/tests/mpush.shellEvents.test.js +13 -0
  35. package/v2Components/FormBuilder/Functional/tests/mpushRenderers.test.js +26 -0
  36. package/v2Components/FormBuilder/_formBuilder.scss +8 -0
  37. package/v2Components/TestAndPreviewSlidebox/index.js +9 -0
  38. package/v2Containers/CommunicationFlow/CommunicationFlow.js +140 -75
  39. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +18 -2
  40. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +67 -28
  41. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +503 -51
  42. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +98 -4
  43. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  44. package/v2Containers/CommunicationFlow/constants.js +65 -4
  45. package/v2Containers/CommunicationFlow/index.js +16 -23
  46. package/v2Containers/CommunicationFlow/messages.js +11 -3
  47. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +27 -11
  48. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +4 -0
  49. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +176 -4
  50. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +22 -4
  51. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +14 -4
  52. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +11 -0
  53. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +51 -0
  54. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  55. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  56. package/v2Containers/CreativesContainer/SlideBoxContent.js +15 -0
  57. package/v2Containers/CreativesContainer/SlideBoxFooter.js +6 -2
  58. package/v2Containers/CreativesContainer/index.js +29 -3
  59. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +29 -0
  60. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +18 -5
  61. package/v2Containers/CreativesContainer/tests/index.test.js +12 -0
  62. package/v2Containers/MobilePush/Create/index.js +18 -1
  63. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +96 -0
  64. package/v2Containers/MobilePush/Edit/index.js +18 -1
  65. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +116 -0
  66. package/v2Containers/MobilePush/commonMethods.js +49 -1
  67. package/v2Containers/MobilePushNew/index.js +29 -4
  68. package/v2Containers/MobilePushNew/tests/index.test.js +119 -0
  69. package/v2Containers/MobilePushNew/tests/utils.test.js +82 -0
  70. package/v2Containers/MobilePushNew/utils.js +34 -1
  71. package/v2Containers/MobilepushWrapper/index.js +3 -1
  72. package/v2Containers/Rcs/index.js +34 -0
  73. package/v2Containers/Rcs/index.scss +15 -0
  74. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +115 -19
  75. package/v2Containers/Rcs/tests/index.test.js +67 -0
  76. package/v2Containers/Sms/Create/index.js +3 -1
  77. package/v2Containers/Sms/Edit/index.js +25 -0
  78. package/v2Containers/Sms/Edit/tests/index.test.js +85 -0
  79. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +27 -3
  80. package/v2Containers/Viber/index.js +24 -1
  81. package/v2Containers/Viber/tests/index.test.js +103 -0
  82. package/v2Containers/WebPush/Create/index.js +19 -0
  83. package/v2Containers/WebPush/Create/preview/WebPushPreview.js +17 -12
  84. package/v2Containers/WebPush/Create/preview/tests/WebPushPreview.test.js +5 -0
  85. package/v2Containers/WebPush/Create/tests/contentValidity.test.js +294 -0
  86. package/v2Containers/Whatsapp/index.js +25 -4
  87. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +306 -34
  88. package/v2Containers/Whatsapp/tests/index.test.js +115 -0
  89. package/v2Containers/Zalo/index.js +28 -1
  90. package/v2Containers/Zalo/tests/index.test.js +99 -0
  91. 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
- // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
21
- // import injectReducer from '../../utils/injectReducer';
19
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
20
+ import injectReducer from '../../utils/injectReducer';
22
21
  import { makeSelectAuthenticated } from '../Cap/selectors';
23
- import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
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
- CHANNEL_CONTENT_KEY_MAP,
39
- CHANNEL_DELIVERY_KEY_MAP,
40
- CAMPAIGNS,
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
- // Inject couponsCap reducer and saga - commented out (cap-coupons flows disabled)
47
- // const withCouponsReducer = injectReducer({
48
- // key: 'couponsCap',
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 ouId = config?.context?.ouId || -1;
158
- const module = config?.context?.module
159
- || (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
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
- const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
- const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
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
- const contentItems = aggregatedData.contentItems || [];
165
- const { dynamicControls = {} } = aggregatedData;
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
- const additionalSettings = {
168
- useTinyUrl: dynamicControls.useTinyUrl ?? false,
169
- encryptUrl: dynamicControls.sendToControlCustomers ?? false,
170
- linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
171
- userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
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 responses = await Promise.all(
177
- contentItems.map((item) => {
178
- const channel = (item.channel || '').toUpperCase();
179
- const contentKey = channelContentKeyMap[channel];
180
- const deliveryKey = channelDeliveryKeyMap[channel];
181
- const payload = {
182
- centralCommsPayload: {
183
- ouId,
184
- channel,
185
- module,
186
- executionParams: {},
187
- clientName: 'EMF',
188
- ...(contentKey && {
189
- [contentKey]: { channel, ...item.templateData },
190
- }),
191
- ...(deliveryKey && {
192
- [deliveryKey]: {
193
- additionalSettings,
194
- channelSettings: {
195
- channel,
196
- ...(aggregatedData.deliverySetting?.channelSetting?.[channel] || {}),
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
- return createCentralCommsMetaId(payload);
203
- }),
204
- );
205
-
206
- const metaIds = responses
207
- .map((res) => res?.response?.data?.id)
208
- .filter(Boolean)
209
- .join(',');
210
-
211
- if (metaIds) {
212
- const getResponse = await getCentralCommsMetaIds(metaIds);
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 createCentralCommsMetaId error:', error);
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 useLegacy key={step}>
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
- // withCouponsReducer, // cap-coupons flows disabled
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
- padding: $CAP_SPACE_16 0 $CAP_SPACE_08;
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?.senderEmail),
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">({messageTypeLabel})</CapLabel>
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">({senderLabel}: {senderIdValue})</CapLabel>
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
- <CapColumn span={12} className="card-body-right-col">
149
- <CapLabel type="label8">{formatMessage(messages.dynamicControlsTitle)}</CapLabel>
150
- {dynamicControlKeys.map((key) => {
151
- const controlConfig = controls.find((c) => c.key === key);
152
- if (!controlConfig) return null;
153
- return (
154
- <CapRow key={key} type="flex" justify="space-between" className="control-row">
155
- <CapLabel type="label2">{controlConfig.label}</CapLabel>
156
- <CapLabel type="label2">
157
- {savedData.dynamicControls[key]
158
- ? formatMessage(messages.yes)
159
- : formatMessage(messages.no)}
160
- </CapLabel>
161
- </CapRow>
162
- );
163
- })}
164
- </CapColumn>
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
- <CapButton type="secondary" onClick={handleOpen}>
186
- {formatMessage(messages.addCreatives)}
187
- </CapButton>
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-xl"
234
+ size="size-l"
200
235
  header={(
201
236
  <CapHeader
202
- title={formatMessage(messages.addMessage)}
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);