@capillarytech/creatives-library 9.0.55 → 9.0.56-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 (30) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/services/api.js +12 -0
  4. package/utils/common.js +0 -4
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
  6. package/v2Components/CommonTestAndPreview/constants.js +2 -0
  7. package/v2Components/CommonTestAndPreview/index.js +1 -1
  8. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
  9. package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
  10. package/v2Containers/CommunicationFlow/CommunicationFlow.js +109 -56
  11. package/v2Containers/CommunicationFlow/CommunicationFlow.scss +1 -1
  12. package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +60 -27
  13. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +441 -51
  14. package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +40 -4
  15. package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
  16. package/v2Containers/CommunicationFlow/constants.js +63 -4
  17. package/v2Containers/CommunicationFlow/index.js +15 -20
  18. package/v2Containers/CommunicationFlow/messages.js +7 -3
  19. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +0 -4
  20. package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +2 -2
  21. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +6 -0
  22. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +25 -0
  23. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
  24. package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
  25. package/v2Containers/CreativesContainer/index.js +1 -1
  26. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
  27. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -12
  28. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +3 -3
  29. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -34
  30. package/.vscode/settings.json +0 -3
@@ -63,7 +63,6 @@ export const ENABLE_AI_SUGGESTIONS = 'ENABLE_AI_SUGGESTIONS';
63
63
  export const AI_CONTENT_BOT_DISABLED = 'AI_CONTENT_BOT_DISABLED';
64
64
  export const AI_DOCUMENTATION_BOT_ENABLED = 'AI_DOCUMENTATION_BOT_ENABLED';
65
65
  export const ENABLE_PRODUCT_SUPPORT_VIDEOS = 'ENABLE_PRODUCT_SUPPORT_VIDEOS';
66
- export const SUPPORT_ENGAGEMENT_MODULE = 'SUPPORT_ENGAGEMENT_MODULE';
67
66
  export const EMBEDDED = 'embedded';
68
67
  // --- Tag/Validation Constants ---
69
68
  export const CARD_RELATED_TAGS = [
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.55",
4
+ "version": "9.0.56-alpha.0",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/services/api.js CHANGED
@@ -701,6 +701,18 @@ export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
701
701
  return request(url, getAPICallObject('POST', payload, false, false, false, true));
702
702
  };
703
703
 
704
+ // CCS CommDefinition API (new Veyron /api/v1/commdefinition surface, proxied through cap-creatives-api /comm-definitions); used by CommunicationFlow's Save, while legacy messageMeta functions remain for CreativesContainer's save flow (Cap/sagas.js).
705
+ export const createCommDefinition = (payload) => {
706
+ const url = `${API_ENDPOINT}/comm-definitions`;
707
+ return request(url, getAPICallObject('POST', payload, false, false, false, true));
708
+ };
709
+
710
+ // Opens a new DRAFT version with the supplied content on an existing CommDefinition (same id/referenceId); used by CommunicationFlow edit-mode saves to preserve the alert's identity instead of creating a new CommDefinition. Route verified against Veyron's /commdefinition/{id}/edit endpoint.
711
+ export const editCommDefinition = (commDefinitionId, payload) => {
712
+ const url = `${API_ENDPOINT}/comm-definitions/${commDefinitionId}/edit`;
713
+ return request(url, getAPICallObject('POST', payload, false, false, false, true));
714
+ };
715
+
704
716
  export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
705
717
  const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
706
718
  return request(url, getAPICallObject('GET', null, false, false, false, true));
package/utils/common.js CHANGED
@@ -99,10 +99,6 @@ export const hasSupportCKEditor = Auth.hasFeatureAccess.bind(
99
99
  null,
100
100
  SUPPORT_CK_EDITOR,
101
101
  );
102
- export const hasSupportEngagementModule = Auth.hasFeatureAccess.bind(
103
- null,
104
- SUPPORT_ENGAGEMENT_MODULE,
105
- );
106
102
 
107
103
  export const hasGiftVoucherFeature = Auth.hasFeatureAccess.bind(
108
104
  null,
@@ -139,6 +139,19 @@ const UnifiedPreview = ({
139
139
  );
140
140
  }
141
141
 
142
+ case CHANNELS.LINE:
143
+ // LINE currently carries a single text message; CCS stores lineMessageContent.messageBody as JSON for LINE's native messages[] (see testAndPreviewDataTransform.js), already reduced to plain text before this point, so reuse the SMS chat-bubble preview. Rich LINE types (image/video/sticker/imageMap/carousel/flex) are not specially rendered yet.
144
+ return (
145
+ <SmsPreviewContent
146
+ content={typeof content === 'string' ? content : (content?.resolvedBody || '')}
147
+ device={device}
148
+ isUpdating={isUpdating}
149
+ error={error}
150
+ formatMessage={formatMessage}
151
+ showHeader={showHeader}
152
+ />
153
+ );
154
+
142
155
  case CHANNELS.WHATSAPP: {
143
156
  // WhatsApp content is an object with templateMsg, media, CTA, etc.
144
157
  const whatsappContent = typeof content === 'object' ? content : {};
@@ -85,6 +85,7 @@ export const CHANNELS = {
85
85
  VIBER: 'VIBER',
86
86
  ZALO: 'ZALO',
87
87
  WEBPUSH: 'WEBPUSH',
88
+ LINE: 'LINE',
88
89
  };
89
90
 
90
91
  /** Unified preview tab keys when RCS shows RCS + SMS fallback panes. */
@@ -100,6 +101,7 @@ export const CHANNELS_USING_ANDROID_PREVIEW_DEVICE = [
100
101
  CHANNELS.INAPP,
101
102
  CHANNELS.MOBILEPUSH,
102
103
  CHANNELS.VIBER,
104
+ CHANNELS.LINE,
103
105
  ];
104
106
 
105
107
  /** RCS createMessageMeta test payload — rich card envelope literals. */
@@ -3816,7 +3816,7 @@ const CommonTestAndPreview = (props) => {
3816
3816
  header={slideboxHeader}
3817
3817
  handleClose={handleClose}
3818
3818
  show={show}
3819
- size="size-xl"
3819
+ size="size-l"
3820
3820
  content={(
3821
3821
  <CapSpin
3822
3822
  spinning={isCustomerDataLoading}
@@ -479,6 +479,112 @@ describe('UnifiedPreview', () => {
479
479
  });
480
480
  });
481
481
 
482
+ describe('Channel Routing - LINE', () => {
483
+ it('should render SmsPreviewContent for LINE channel with string content', () => {
484
+ const props = {
485
+ ...defaultProps,
486
+ channel: CHANNELS.LINE,
487
+ content: 'Hello world',
488
+ };
489
+
490
+ render(
491
+ <TestWrapper>
492
+ <ComponentToRender {...props} />
493
+ </TestWrapper>
494
+ );
495
+
496
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
497
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('Hello world');
498
+ });
499
+
500
+ it('should handle object content with resolvedBody for LINE channel', () => {
501
+ const props = {
502
+ ...defaultProps,
503
+ channel: CHANNELS.LINE,
504
+ content: {
505
+ resolvedBody: 'some text',
506
+ },
507
+ };
508
+
509
+ render(
510
+ <TestWrapper>
511
+ <ComponentToRender {...props} />
512
+ </TestWrapper>
513
+ );
514
+
515
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
516
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('some text');
517
+ });
518
+
519
+ it('should not crash and render empty content for LINE channel when content is undefined', () => {
520
+ const props = {
521
+ ...defaultProps,
522
+ channel: CHANNELS.LINE,
523
+ content: undefined,
524
+ };
525
+
526
+ render(
527
+ <TestWrapper>
528
+ <ComponentToRender {...props} />
529
+ </TestWrapper>
530
+ );
531
+
532
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
533
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('');
534
+ });
535
+
536
+ it('should not crash and render empty content for LINE channel when content is null', () => {
537
+ const props = {
538
+ ...defaultProps,
539
+ channel: CHANNELS.LINE,
540
+ content: null,
541
+ };
542
+
543
+ render(
544
+ <TestWrapper>
545
+ <ComponentToRender {...props} />
546
+ </TestWrapper>
547
+ );
548
+
549
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
550
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('');
551
+ });
552
+
553
+ it('should render empty content for LINE channel when content is not a string and has no resolvedBody', () => {
554
+ const props = {
555
+ ...defaultProps,
556
+ channel: CHANNELS.LINE,
557
+ content: { someOtherField: 'value' },
558
+ };
559
+
560
+ render(
561
+ <TestWrapper>
562
+ <ComponentToRender {...props} />
563
+ </TestWrapper>
564
+ );
565
+
566
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
567
+ expect(screen.getByTestId('sms-content')).toHaveTextContent('');
568
+ });
569
+
570
+ it('should not render the Unknown Channel fallback for LINE channel', () => {
571
+ const props = {
572
+ ...defaultProps,
573
+ channel: CHANNELS.LINE,
574
+ content: 'Hello world',
575
+ };
576
+
577
+ render(
578
+ <TestWrapper>
579
+ <ComponentToRender {...props} />
580
+ </TestWrapper>
581
+ );
582
+
583
+ expect(screen.queryByText('Unknown Channel')).toBeNull();
584
+ expect(screen.getByTestId('sms-preview')).toBeTruthy();
585
+ });
586
+ });
587
+
482
588
  describe('Channel Routing - WHATSAPP', () => {
483
589
  it('should render WhatsAppPreviewContent for WHATSAPP channel', () => {
484
590
  const props = {
@@ -42,6 +42,7 @@ import {
42
42
  CLEAR_PREVIEW_ERRORS,
43
43
  // Channel Constants
44
44
  CHANNELS,
45
+ CHANNELS_USING_ANDROID_PREVIEW_DEVICE,
45
46
  // Device Constants
46
47
  DESKTOP,
47
48
  MOBILE,
@@ -180,15 +181,22 @@ describe('CommonTestAndPreview Constants', () => {
180
181
  expect(CHANNELS.VIBER).toBe('VIBER');
181
182
  expect(CHANNELS.ZALO).toBe('ZALO');
182
183
  expect(CHANNELS.WEBPUSH).toBe('WEBPUSH');
184
+ expect(CHANNELS.LINE).toBe('LINE');
183
185
  });
184
186
 
185
187
  it('should have all required channel keys', () => {
186
- const expectedChannels = ['EMAIL', 'SMS', 'RCS', 'WHATSAPP', 'INAPP', 'MOBILEPUSH', 'VIBER', 'ZALO', 'WEBPUSH'];
188
+ const expectedChannels = ['EMAIL', 'SMS', 'RCS', 'WHATSAPP', 'INAPP', 'MOBILEPUSH', 'VIBER', 'ZALO', 'WEBPUSH', 'LINE'];
187
189
  expect(Object.keys(CHANNELS)).toEqual(expect.arrayContaining(expectedChannels));
188
190
  expect(Object.keys(CHANNELS).length).toBe(expectedChannels.length);
189
191
  });
190
192
  });
191
193
 
194
+ describe('CHANNELS_USING_ANDROID_PREVIEW_DEVICE', () => {
195
+ it('should include CHANNELS.LINE', () => {
196
+ expect(CHANNELS_USING_ANDROID_PREVIEW_DEVICE).toContain(CHANNELS.LINE);
197
+ });
198
+ });
199
+
192
200
  describe('Device Constants', () => {
193
201
  it('should export all device constants with correct values', () => {
194
202
  expect(DESKTOP).toBe('desktop');
@@ -17,10 +17,11 @@ import { createStructuredSelector } from 'reselect';
17
17
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
18
18
  import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
19
19
  import CapButton from '@capillarytech/cap-ui-library/CapButton';
20
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
20
21
  // import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
21
22
  // import injectReducer from '../../utils/injectReducer';
22
23
  import { makeSelectAuthenticated } from '../Cap/selectors';
23
- import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
24
+ import { createCommDefinition, editCommDefinition } from '../../services/api';
24
25
  import DynamicControlsStep from './steps/DynamicControlsStep';
25
26
  import MessageTypeStep from './steps/MessageTypeStep';
26
27
  import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
@@ -35,9 +36,11 @@ import {
35
36
  CHANNELS,
36
37
  INCENTIVE_TYPES,
37
38
  DYNAMIC_CONTROLS_CONFIG,
38
- CHANNEL_CONTENT_KEY_MAP,
39
- CHANNEL_DELIVERY_KEY_MAP,
40
- CAMPAIGNS,
39
+ CCS_STRATEGY_TYPE_SINGLE,
40
+ CCS_CHANNEL_CONTENT_KEY_MAP,
41
+ CCS_CHANNEL_DELIVERY_KEY_MAP,
42
+ CCS_CHANNEL_NAME_MAP,
43
+ CCS_CONTENT_TRANSFORMS,
41
44
  } from './constants';
42
45
  import { getEnabledSteps } from './utils/getEnabledSteps';
43
46
  import messages from './messages';
@@ -64,6 +67,21 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
64
67
  return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
65
68
  };
66
69
 
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';
72
+
73
+ /**
74
+ * CCS requires referenceId on create, while consumers require only the Alert/comm name; generate a stable fallback from the name when referenceId is omitted instead of failing the save.
75
+ */
76
+ const buildCcsReferenceId = (name) => {
77
+ const slug = (name || 'COMM')
78
+ .trim()
79
+ .toUpperCase()
80
+ .replace(/[^A-Z0-9]+/g, '_')
81
+ .replace(/^_+|_+$/g, '') || 'COMM';
82
+ return `${slug}_${Date.now()}`;
83
+ };
84
+
67
85
  const CommunicationFlow = ({
68
86
  config,
69
87
  initialData,
@@ -91,6 +109,7 @@ const CommunicationFlow = ({
91
109
  };
92
110
  });
93
111
  const [validationErrors, setValidationErrors] = useState({});
112
+ const [saveError, setSaveError] = useState(null);
94
113
 
95
114
  // Memoize enabled steps
96
115
  const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
@@ -150,74 +169,103 @@ const CommunicationFlow = ({
150
169
  }, []);
151
170
 
152
171
  const handleSave = useCallback(async () => {
172
+ setSaveError(null);
153
173
  const aggregatedData = getAggregatedData();
154
174
  const shouldUseCCS = config?.useCCS !== false;
175
+ let ccsCommDefinition = null;
155
176
 
156
177
  if (shouldUseCCS) {
157
- const ouId = config?.context?.ouId || -1;
158
- const module = config?.context?.module
159
- || (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
178
+ const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(aggregatedData.communicationStrategy);
179
+ const contentItem = (aggregatedData.contentItems || [])[0];
180
+ // Consumer-supplied name (e.g. CapNotify's Alert Name field) CommunicationFlow
181
+ // has no name input of its own, so this is mandatory input from config.context.
182
+ const name = config?.context?.name;
160
183
 
161
- const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
162
- const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
184
+ // CCS create is SINGLE-strategy only this phase (D1); CHANNEL_PRIORITY/AB_TEST
185
+ // carry multiple content items with no CCS equivalent yet.
186
+ if (!isMultiChannel && contentItem && name) {
187
+ const rawChannel = (contentItem.channel || '').toUpperCase();
188
+ const channel = CCS_CHANNEL_NAME_MAP[rawChannel] || rawChannel;
189
+ const contentKey = CCS_CHANNEL_CONTENT_KEY_MAP[channel];
190
+ const deliveryKey = CCS_CHANNEL_DELIVERY_KEY_MAP[channel];
191
+ const contentTransform = CCS_CONTENT_TRANSFORMS[channel];
192
+ const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
193
+ const { dynamicControls = {} } = aggregatedData;
194
+ const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
195
+ const referenceId = config?.context?.referenceId || buildCcsReferenceId(name);
163
196
 
164
- const contentItems = aggregatedData.contentItems || [];
165
- const { dynamicControls = {} } = aggregatedData;
197
+ const payload = {
198
+ referenceId,
199
+ name,
200
+ description: config?.context?.description || undefined,
201
+ strategyType: CCS_STRATEGY_TYPE_SINGLE,
202
+ settings: {
203
+ additionalSettings: {
204
+ useTinyUrl: dynamicControls.useTinyUrl ?? false,
205
+ encryptUrl: dynamicControls.sendToControlCustomers ?? false,
206
+ linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
207
+ userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
208
+ },
209
+ executionParams: {},
210
+ },
211
+ singleChannelStrategy: {
212
+ variant: {
213
+ channel,
214
+ // `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.
215
+ ...(contentKey && { [contentKey]: { ...contentPayload, channel } }),
216
+ ...(deliveryKey && { [deliveryKey]: { channelSettings: { ...channelSettings, channel } } }),
217
+ },
218
+ },
219
+ };
166
220
 
167
- const additionalSettings = {
168
- useTinyUrl: dynamicControls.useTinyUrl ?? false,
169
- encryptUrl: dynamicControls.sendToControlCustomers ?? false,
170
- linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
171
- userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
172
- };
221
+ // 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.
222
+ const { existingCommDefinitionId } = config?.context || {};
173
223
 
174
- if (contentItems.length > 0) {
175
224
  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
- },
225
+ const res = existingCommDefinitionId
226
+ ? await editCommDefinition(existingCommDefinitionId, {
227
+ strategyType: payload.strategyType,
228
+ settings: payload.settings,
229
+ singleChannelStrategy: payload.singleChannelStrategy,
230
+ })
231
+ : await createCommDefinition(payload);
232
+ if (isDuplicateReferenceIdError(res)) {
233
+ // Duplicate referenceId in this org — block the save so the user can
234
+ // change it, rather than silently proceeding without a CCS comm.
235
+ setSaveError(formatMessage(messages.duplicateReferenceIdError));
236
+ return;
237
+ }
238
+ const data = res?.response?.data;
239
+ if (existingCommDefinitionId) {
240
+ if (data) {
241
+ ccsCommDefinition = {
242
+ id: existingCommDefinitionId,
243
+ referenceId: referenceId || data.referenceId,
244
+ version: data.version?.version ?? data.version ?? 1,
245
+ status: data.status,
201
246
  };
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);
247
+ }
248
+ } else if (data?.id) {
249
+ ccsCommDefinition = {
250
+ id: data.id,
251
+ referenceId: data.referenceId,
252
+ version: data.version?.version ?? 1,
253
+ status: data.status,
254
+ };
213
255
  }
214
256
  } catch (error) {
215
- console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
257
+ console.error('[CommunicationFlow] CCS createCommDefinition error:', error);
216
258
  }
259
+ } else if (!name) {
260
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — config.context.name is required');
261
+ } else if (isMultiChannel) {
262
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — CHANNEL_PRIORITY/AB_TEST strategies are not supported by CCS create yet (SINGLE only)');
263
+ } else if (!contentItem) {
264
+ console.warn('[CommunicationFlow] Skipping CCS createCommDefinition — no content item found (contentItems is empty)');
217
265
  }
218
266
  }
219
267
 
220
- onSave(aggregatedData);
268
+ onSave(ccsCommDefinition ? { ...aggregatedData, ccsCommDefinition } : aggregatedData);
221
269
  }, [getAggregatedData, config, onSave]);
222
270
 
223
271
  // Call onChange callback when stepData changes
@@ -317,6 +365,11 @@ const CommunicationFlow = ({
317
365
  {renderSteps()}
318
366
  {onSave && (
319
367
  <CapRow useLegacy className="communication-flow-container__footer">
368
+ {saveError && (
369
+ <CapLabel type="label2" className="communication-flow-container__save-error">
370
+ {saveError}
371
+ </CapLabel>
372
+ )}
320
373
  <CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
321
374
  {formatMessage(messages.save)}
322
375
  </CapButton>
@@ -137,7 +137,7 @@
137
137
  }
138
138
 
139
139
  &__footer {
140
- padding: $CAP_SPACE_16 0 $CAP_SPACE_08;
140
+ padding: 3.142rem 0 $CAP_SPACE_08;
141
141
  }
142
142
  }
143
143
 
@@ -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,6 +75,8 @@ 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);
@@ -91,8 +94,9 @@ const CommunicationFlowCard = ({
91
94
  }, [onCancel]);
92
95
 
93
96
  const handleOpen = useCallback(() => {
97
+ if (disabled) return;
94
98
  setShowSlideBox(true);
95
- }, []);
99
+ }, [disabled]);
96
100
 
97
101
  const firstItem = savedData?.contentItems?.[0];
98
102
  const channelConfig = firstItem
@@ -107,6 +111,11 @@ const CommunicationFlowCard = ({
107
111
  const senderIdValue = getSenderIdValue(firstItem, savedData);
108
112
  const controls = config?.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG;
109
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.
118
+ const showDynamicControls = config?.features?.showDynamicControls !== false;
110
119
  const channel = firstItem.channel?.toUpperCase();
111
120
  const senderLabel = [WHATSAPP, RCS].includes(channel)
112
121
  ? formatMessage(messages.senderNumberLabel)
@@ -120,7 +129,11 @@ const CommunicationFlowCard = ({
120
129
  <CapLabel type="label8">Message:</CapLabel>
121
130
  <CapLabel type="label2">{strategyLabel}</CapLabel>
122
131
  {messageTypeLabel && (
123
- <CapLabel type="label1">({messageTypeLabel})</CapLabel>
132
+ <CapLabel type="label1">
133
+ (
134
+ {messageTypeLabel}
135
+ )
136
+ </CapLabel>
124
137
  )}
125
138
  </CapRow>
126
139
  )}
@@ -135,33 +148,41 @@ const CommunicationFlowCard = ({
135
148
  bodyStyle={{ padding: 0 }}
136
149
  >
137
150
  <CapRow gutter={0} className="card-body-row">
138
- <CapColumn span={12} className="card-body-left-col">
151
+ <CapColumn span={showDynamicControls ? 12 : 24} className="card-body-left-col">
139
152
  <CapRow type="flex" align="middle" className="channel-header-row">
140
153
  <CapIcon type={channelConfig?.iconType || 'sms'} size="s" />
141
154
  <CapLabel type="label8">{channelConfig?.label || firstItem.channel}</CapLabel>
142
155
  {senderIdValue && (
143
- <CapLabel type="label1">({senderLabel}: {senderIdValue})</CapLabel>
156
+ <CapLabel type="label1">
157
+ (
158
+ {senderLabel}
159
+ :
160
+ {senderIdValue}
161
+ )
162
+ </CapLabel>
144
163
  )}
145
164
  </CapRow>
146
165
  {getContentBody(firstItem)}
147
166
  </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>
167
+ {showDynamicControls && (
168
+ <CapColumn span={12} className="card-body-right-col">
169
+ <CapLabel type="label8">{formatMessage(messages.dynamicControlsTitle)}</CapLabel>
170
+ {dynamicControlKeys.map((key) => {
171
+ const controlConfig = controls.find((c) => c.key === key);
172
+ if (!controlConfig) return null;
173
+ return (
174
+ <CapRow key={key} type="flex" justify="space-between" className="control-row">
175
+ <CapLabel type="label2">{controlConfig.label}</CapLabel>
176
+ <CapLabel type="label2">
177
+ {savedData.dynamicControls[key]
178
+ ? formatMessage(messages.yes)
179
+ : formatMessage(messages.no)}
180
+ </CapLabel>
181
+ </CapRow>
182
+ );
183
+ })}
184
+ </CapColumn>
185
+ )}
165
186
  </CapRow>
166
187
  </CapCard>
167
188
  );
@@ -182,9 +203,17 @@ const CommunicationFlowCard = ({
182
203
  <CapImage src={addCreativesIllustration} />
183
204
  </CapColumn>
184
205
  <CapColumn span={14} className="empty-card-action-col">
185
- <CapButton type="secondary" onClick={handleOpen}>
186
- {formatMessage(messages.addCreatives)}
187
- </CapButton>
206
+ {disabled && disabledTooltip ? (
207
+ <CapTooltip title={disabledTooltip}>
208
+ <CapButton type="secondary" disabled>
209
+ {formatMessage(messages.addCreatives)}
210
+ </CapButton>
211
+ </CapTooltip>
212
+ ) : (
213
+ <CapButton type="secondary" onClick={handleOpen} disabled={disabled}>
214
+ {formatMessage(messages.addCreatives)}
215
+ </CapButton>
216
+ )}
188
217
  </CapColumn>
189
218
  </CapRow>
190
219
  </CapCard>
@@ -196,10 +225,10 @@ const CommunicationFlowCard = ({
196
225
  <CapSlideBox
197
226
  show={showSlideBox}
198
227
  handleClose={handleClose}
199
- size="size-xl"
228
+ size="size-l"
200
229
  header={(
201
230
  <CapHeader
202
- title={formatMessage(messages.addMessage)}
231
+ title={formatMessage(messages.addContent)}
203
232
  handleClose={handleClose}
204
233
  />
205
234
  )}
@@ -227,6 +256,8 @@ CommunicationFlowCard.propTypes = {
227
256
  onChange: PropTypes.func,
228
257
  cap: PropTypes.object,
229
258
  intl: PropTypes.object.isRequired,
259
+ disabled: PropTypes.bool, // Disables the "Add creatives" trigger (e.g. until a consumer-required field is filled)
260
+ disabledTooltip: PropTypes.node, // Shown on hover when disabled is true
230
261
  };
231
262
 
232
263
  CommunicationFlowCard.defaultProps = {
@@ -235,6 +266,8 @@ CommunicationFlowCard.defaultProps = {
235
266
  onCancel: null,
236
267
  onChange: null,
237
268
  cap: null,
269
+ disabled: false,
270
+ disabledTooltip: null,
238
271
  };
239
272
 
240
273
  export default injectIntl(CommunicationFlowCard);