@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.
- package/constants/unified.js +0 -1
- package/package.json +1 -1
- package/services/api.js +12 -0
- package/utils/common.js +0 -4
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +13 -0
- package/v2Components/CommonTestAndPreview/constants.js +2 -0
- package/v2Components/CommonTestAndPreview/index.js +1 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +106 -0
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +9 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +109 -56
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +1 -1
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +60 -27
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +441 -51
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +40 -4
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +85 -0
- package/v2Containers/CommunicationFlow/constants.js +63 -4
- package/v2Containers/CommunicationFlow/index.js +15 -20
- package/v2Containers/CommunicationFlow/messages.js +7 -3
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +0 -4
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +2 -2
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +6 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +25 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +11 -4
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +19 -5
- package/v2Containers/CreativesContainer/index.js +1 -1
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +5 -5
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -12
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +3 -3
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -34
- package/.vscode/settings.json +0 -3
package/constants/unified.js
CHANGED
|
@@ -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
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. */
|
|
@@ -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 {
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
158
|
-
const
|
|
159
|
-
|
|
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
|
-
|
|
162
|
-
|
|
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
|
-
|
|
165
|
-
|
|
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
|
-
|
|
168
|
-
|
|
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
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
|
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>
|
|
@@ -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?.
|
|
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">
|
|
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">
|
|
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
|
-
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
186
|
-
{
|
|
187
|
-
|
|
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-
|
|
228
|
+
size="size-l"
|
|
200
229
|
header={(
|
|
201
230
|
<CapHeader
|
|
202
|
-
title={formatMessage(messages.
|
|
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);
|