@capillarytech/creatives-library 9.0.56-alpha.0 → 9.0.56-alpha.2
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/package.json +1 -1
- package/services/api.js +6 -0
- package/v2Components/CommonTestAndPreview/index.js +54 -1
- package/v2Components/TestAndPreviewSlidebox/index.js +6 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +11 -14
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +6 -6
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +25 -2
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -713,6 +713,12 @@ export const editCommDefinition = (commDefinitionId, payload) => {
|
|
|
713
713
|
return request(url, getAPICallObject('POST', payload, false, false, false, true));
|
|
714
714
|
};
|
|
715
715
|
|
|
716
|
+
// Sends a real transactional comm via CCS (notify/ui's "Send test" action uses this instead of the legacy createMessageMeta/sendTestMessage flow — see CommonTestAndPreview's handleSendTestMessage).
|
|
717
|
+
export const sendTransactionalComm = (payload) => {
|
|
718
|
+
const url = `${API_ENDPOINT}/comm-definitions/send/transaction`;
|
|
719
|
+
return request(url, getAPICallObject('POST', payload, false, false, false, true));
|
|
720
|
+
};
|
|
721
|
+
|
|
716
722
|
export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
|
|
717
723
|
const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
|
|
718
724
|
return request(url, getAPICallObject('GET', null, false, false, false, true));
|
|
@@ -208,6 +208,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
208
208
|
wecrmAccounts = [],
|
|
209
209
|
isLoadingSenderDetails = false,
|
|
210
210
|
orgUnitId = -1,
|
|
211
|
+
// notify/ui only: { commDefinitionId, referenceId } — when present, "Send test" calls CCS's send/transaction endpoint directly instead of the legacy createMessageMeta/sendTestMessage flow.
|
|
212
|
+
ccsSendTransaction,
|
|
211
213
|
// Email-specific props
|
|
212
214
|
beeInstance,
|
|
213
215
|
currentTab = 1,
|
|
@@ -231,6 +233,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
231
233
|
const [smsFallbackOptionalTags, setSmsFallbackOptionalTags] = useState([]);
|
|
232
234
|
const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
|
|
233
235
|
const [customValues, setCustomValues] = useState({});
|
|
236
|
+
const [isSendingCcsTest, setIsSendingCcsTest] = useState(false);
|
|
234
237
|
const previewCustomValuesRef = useRef({});
|
|
235
238
|
const [showJSON, setShowJSON] = useState(false);
|
|
236
239
|
const [tagsExtracted, setTagsExtracted] = useState(false);
|
|
@@ -3625,7 +3628,52 @@ const CommonTestAndPreview = (props) => {
|
|
|
3625
3628
|
/**
|
|
3626
3629
|
* Handle send test message
|
|
3627
3630
|
*/
|
|
3631
|
+
// notify/ui: send the test through CCS's own send/transaction endpoint against the alert's
|
|
3632
|
+
// real CommDefinition, instead of the legacy createMessageMeta/sendTestMessage flow below.
|
|
3633
|
+
const handleCcsSendTestMessage = async () => {
|
|
3634
|
+
const allUserIds = [];
|
|
3635
|
+
selectedTestEntities.forEach((entityId) => {
|
|
3636
|
+
const group = testGroups.find((testGroup) => testEntityIdsEqual(testGroup.groupId, entityId));
|
|
3637
|
+
if (group) {
|
|
3638
|
+
allUserIds.push(...group.userIds);
|
|
3639
|
+
} else {
|
|
3640
|
+
allUserIds.push(entityId);
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
const uniqueUserIds = [...new Set(allUserIds)];
|
|
3644
|
+
const { commDefinitionId, referenceId } = ccsSendTransaction;
|
|
3645
|
+
|
|
3646
|
+
setIsSendingCcsTest(true);
|
|
3647
|
+
try {
|
|
3648
|
+
const res = await Api.sendTransactionalComm({
|
|
3649
|
+
...(commDefinitionId ? { commDefinitionId } : { referenceId }),
|
|
3650
|
+
uniqueKey: `test_${Date.now()}`,
|
|
3651
|
+
isTest: true,
|
|
3652
|
+
recipient: {
|
|
3653
|
+
identifiers: uniqueUserIds.map((userId) => ({ type: 'USER_ID', value: String(userId) })),
|
|
3654
|
+
tagValues: customValues,
|
|
3655
|
+
},
|
|
3656
|
+
});
|
|
3657
|
+
if (res?.response?.success === false) {
|
|
3658
|
+
throw new Error(res?.response?.errors?.[0]?.message || 'Failed to send test message');
|
|
3659
|
+
}
|
|
3660
|
+
CapNotification.success({
|
|
3661
|
+
message: formatMessage(messages.testMessageSent),
|
|
3662
|
+
});
|
|
3663
|
+
} catch (error) {
|
|
3664
|
+
CapNotification.error({
|
|
3665
|
+
message: formatMessage(messages.testMessageFailed),
|
|
3666
|
+
});
|
|
3667
|
+
} finally {
|
|
3668
|
+
setIsSendingCcsTest(false);
|
|
3669
|
+
}
|
|
3670
|
+
};
|
|
3671
|
+
|
|
3628
3672
|
const handleSendTestMessage = () => {
|
|
3673
|
+
if (ccsSendTransaction) {
|
|
3674
|
+
handleCcsSendTestMessage();
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3629
3677
|
const allUserIds = [];
|
|
3630
3678
|
selectedTestEntities.forEach((entityId) => {
|
|
3631
3679
|
const group = testGroups.find((testGroup) => testEntityIdsEqual(testGroup.groupId, entityId));
|
|
@@ -3763,7 +3811,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
3763
3811
|
formData={formDataForSendTest}
|
|
3764
3812
|
content={getCurrentContent}
|
|
3765
3813
|
channel={channel}
|
|
3766
|
-
isSendingTestMessage={isSendingTestMessage}
|
|
3814
|
+
isSendingTestMessage={isSendingTestMessage || isSendingCcsTest}
|
|
3767
3815
|
renderAddTestCustomerButton={renderAddTestCustomerButton}
|
|
3768
3816
|
formatMessage={formatMessage}
|
|
3769
3817
|
deliverySettings={testPreviewDeliverySettings[channel]}
|
|
@@ -3918,6 +3966,10 @@ CommonTestAndPreview.propTypes = {
|
|
|
3918
3966
|
wecrmAccounts: PropTypes.array,
|
|
3919
3967
|
isLoadingSenderDetails: PropTypes.bool,
|
|
3920
3968
|
orgUnitId: PropTypes.number,
|
|
3969
|
+
ccsSendTransaction: PropTypes.shape({
|
|
3970
|
+
commDefinitionId: PropTypes.string,
|
|
3971
|
+
referenceId: PropTypes.string,
|
|
3972
|
+
}),
|
|
3921
3973
|
|
|
3922
3974
|
// Email-specific props
|
|
3923
3975
|
beeInstance: PropTypes.object,
|
|
@@ -3957,6 +4009,7 @@ CommonTestAndPreview.defaultProps = {
|
|
|
3957
4009
|
wecrmAccounts: [],
|
|
3958
4010
|
isLoadingSenderDetails: false,
|
|
3959
4011
|
orgUnitId: -1,
|
|
4012
|
+
ccsSendTransaction: null,
|
|
3960
4013
|
};
|
|
3961
4014
|
|
|
3962
4015
|
// ============================================
|
|
@@ -108,6 +108,11 @@ TestAndPreviewSlidebox.propTypes = {
|
|
|
108
108
|
wecrmAccounts: PropTypes.array,
|
|
109
109
|
isLoadingSenderDetails: PropTypes.bool,
|
|
110
110
|
orgUnitId: PropTypes.number,
|
|
111
|
+
/** notify/ui only — see CommonTestAndPreview's handleSendTestMessage. */
|
|
112
|
+
ccsSendTransaction: PropTypes.shape({
|
|
113
|
+
commDefinitionId: PropTypes.string,
|
|
114
|
+
referenceId: PropTypes.string,
|
|
115
|
+
}),
|
|
111
116
|
};
|
|
112
117
|
|
|
113
118
|
TestAndPreviewSlidebox.defaultProps = {
|
|
@@ -125,6 +130,7 @@ TestAndPreviewSlidebox.defaultProps = {
|
|
|
125
130
|
isLoadingSenderDetails: false,
|
|
126
131
|
orgUnitId: -1,
|
|
127
132
|
smsFallbackContent: null,
|
|
133
|
+
ccsSendTransaction: null,
|
|
128
134
|
};
|
|
129
135
|
|
|
130
136
|
const mapStateToProps = createStructuredSelector({
|
|
@@ -70,18 +70,6 @@ const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
|
70
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
71
|
const isDuplicateReferenceIdError = (res) => res?.success === false && res?.status?.message === 'REFERENCE_ID_EXISTS';
|
|
72
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
|
-
|
|
85
73
|
const CommunicationFlow = ({
|
|
86
74
|
config,
|
|
87
75
|
initialData,
|
|
@@ -192,12 +180,15 @@ const CommunicationFlow = ({
|
|
|
192
180
|
const contentPayload = contentTransform ? contentTransform(contentItem.templateData) : contentItem.templateData;
|
|
193
181
|
const { dynamicControls = {} } = aggregatedData;
|
|
194
182
|
const channelSettings = aggregatedData.deliverySetting?.channelSetting?.[channel] || {};
|
|
195
|
-
|
|
183
|
+
// referenceId/description are user-editable, optional fields (up until Send for
|
|
184
|
+
// approval) — sent as '' rather than auto-generated/omitted when left blank.
|
|
185
|
+
const referenceId = config?.context?.referenceId || '';
|
|
186
|
+
const description = config?.context?.description || '';
|
|
196
187
|
|
|
197
188
|
const payload = {
|
|
198
189
|
referenceId,
|
|
199
190
|
name,
|
|
200
|
-
description
|
|
191
|
+
description,
|
|
201
192
|
strategyType: CCS_STRATEGY_TYPE_SINGLE,
|
|
202
193
|
settings: {
|
|
203
194
|
additionalSettings: {
|
|
@@ -224,6 +215,8 @@ const CommunicationFlow = ({
|
|
|
224
215
|
try {
|
|
225
216
|
const res = existingCommDefinitionId
|
|
226
217
|
? await editCommDefinition(existingCommDefinitionId, {
|
|
218
|
+
referenceId: payload.referenceId,
|
|
219
|
+
description: payload.description,
|
|
227
220
|
strategyType: payload.strategyType,
|
|
228
221
|
settings: payload.settings,
|
|
229
222
|
singleChannelStrategy: payload.singleChannelStrategy,
|
|
@@ -328,6 +321,8 @@ const CommunicationFlow = ({
|
|
|
328
321
|
onChange={(data) => handleStepChange(step, data)}
|
|
329
322
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
330
323
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
324
|
+
disablePreviewAndTest={!!contentTemplateData.disablePreviewAndTest}
|
|
325
|
+
autoOpenExistingContent={!!contentTemplateData.autoOpenExistingContent}
|
|
331
326
|
creativesMode={config.mode || 'create'}
|
|
332
327
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
333
328
|
incentivesData={{
|
|
@@ -415,6 +410,8 @@ CommunicationFlow.propTypes = {
|
|
|
415
410
|
contentTemplateData: PropTypes.shape({
|
|
416
411
|
required: PropTypes.bool,
|
|
417
412
|
channels: PropTypes.object,
|
|
413
|
+
disablePreviewAndTest: PropTypes.bool,
|
|
414
|
+
autoOpenExistingContent: PropTypes.bool,
|
|
418
415
|
}),
|
|
419
416
|
enableIncentives: PropTypes.bool,
|
|
420
417
|
enableDeliverySettings: PropTypes.bool,
|
|
@@ -613,10 +613,7 @@ describe('handleSave — CCS flow', () => {
|
|
|
613
613
|
);
|
|
614
614
|
});
|
|
615
615
|
|
|
616
|
-
it('
|
|
617
|
-
// moduleMocks.js spies on Date.now globally, but this config's resetMocks:true
|
|
618
|
-
// clears that return value before every test — set it explicitly here.
|
|
619
|
-
jest.spyOn(Date, 'now').mockReturnValue(1612267539410);
|
|
616
|
+
it('sends an empty referenceId/description when config.context does not supply them', async () => {
|
|
620
617
|
renderWithFlow({
|
|
621
618
|
features: {},
|
|
622
619
|
config: ccsConfig,
|
|
@@ -627,7 +624,8 @@ describe('handleSave — CCS flow', () => {
|
|
|
627
624
|
|
|
628
625
|
await waitFor(() => expect(createCommDefinition).toHaveBeenCalled());
|
|
629
626
|
const payload = createCommDefinition.mock.calls[0][0];
|
|
630
|
-
expect(payload.referenceId).toBe('
|
|
627
|
+
expect(payload.referenceId).toBe('');
|
|
628
|
+
expect(payload.description).toBe('');
|
|
631
629
|
});
|
|
632
630
|
|
|
633
631
|
it('uses config.context.referenceId and description when provided', async () => {
|
|
@@ -820,7 +818,7 @@ describe('handleSave — CCS flow', () => {
|
|
|
820
818
|
features: {},
|
|
821
819
|
config: {
|
|
822
820
|
...ccsConfig,
|
|
823
|
-
context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', existingCommDefinitionId: 'cd_existing_1' },
|
|
821
|
+
context: { ...ccsConfig.context, referenceId: 'ORDER_PLACED', description: 'desc', existingCommDefinitionId: 'cd_existing_1' },
|
|
824
822
|
},
|
|
825
823
|
initialData: { contentItems: [{ channel: 'SMS', templateData: {} }] },
|
|
826
824
|
onSave,
|
|
@@ -832,6 +830,8 @@ describe('handleSave — CCS flow', () => {
|
|
|
832
830
|
expect(editCommDefinition).toHaveBeenCalledWith(
|
|
833
831
|
'cd_existing_1',
|
|
834
832
|
expect.objectContaining({
|
|
833
|
+
referenceId: 'ORDER_PLACED',
|
|
834
|
+
description: 'desc',
|
|
835
835
|
strategyType: 'SINGLE',
|
|
836
836
|
settings: expect.anything(),
|
|
837
837
|
singleChannelStrategy: expect.anything(),
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* Content template selection with channel dropdown
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import React, {
|
|
7
|
+
import React, {
|
|
8
|
+
useState, useCallback, useMemo, useEffect, useRef,
|
|
9
|
+
} from 'react';
|
|
8
10
|
import PropTypes from 'prop-types';
|
|
9
11
|
import { injectIntl } from 'react-intl';
|
|
10
12
|
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
@@ -45,6 +47,8 @@ const ChannelSelectionStep = ({
|
|
|
45
47
|
onChange,
|
|
46
48
|
channelsToHide = [],
|
|
47
49
|
channelsToDisable = [],
|
|
50
|
+
disablePreviewAndTest = false,
|
|
51
|
+
autoOpenExistingContent = false,
|
|
48
52
|
creativesMode = 'create',
|
|
49
53
|
selectedOfferDetails = [],
|
|
50
54
|
incentivesData,
|
|
@@ -117,6 +121,21 @@ const ChannelSelectionStep = ({
|
|
|
117
121
|
}
|
|
118
122
|
}, [contentItems]);
|
|
119
123
|
|
|
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
|
+
|
|
120
139
|
const handleDeleteContent = useCallback((contentId) => {
|
|
121
140
|
onChange({ contentItems: contentItems.filter((c) => c.contentId !== contentId) });
|
|
122
141
|
}, [contentItems, onChange]);
|
|
@@ -358,7 +377,7 @@ const ChannelSelectionStep = ({
|
|
|
358
377
|
>
|
|
359
378
|
{formatMessage(messages.edit)}
|
|
360
379
|
</CapMenu.Item>
|
|
361
|
-
{!PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
|
|
380
|
+
{!disablePreviewAndTest && !PREVIEW_TEST_UNSUPPORTED_CHANNELS.includes(item?.channel?.toUpperCase()) && (
|
|
362
381
|
<CapMenu.Item
|
|
363
382
|
id="preview-menu-item"
|
|
364
383
|
className="ant-dropdown-menu-item"
|
|
@@ -551,6 +570,8 @@ ChannelSelectionStep.propTypes = {
|
|
|
551
570
|
onChange: PropTypes.func.isRequired,
|
|
552
571
|
channelsToHide: PropTypes.array,
|
|
553
572
|
channelsToDisable: PropTypes.array,
|
|
573
|
+
disablePreviewAndTest: PropTypes.bool,
|
|
574
|
+
autoOpenExistingContent: PropTypes.bool,
|
|
554
575
|
creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
|
|
555
576
|
selectedOfferDetails: PropTypes.array,
|
|
556
577
|
incentivesData: PropTypes.shape({
|
|
@@ -572,6 +593,8 @@ ChannelSelectionStep.defaultProps = {
|
|
|
572
593
|
error: null,
|
|
573
594
|
channelsToHide: [],
|
|
574
595
|
channelsToDisable: [],
|
|
596
|
+
disablePreviewAndTest: false,
|
|
597
|
+
autoOpenExistingContent: false,
|
|
575
598
|
creativesMode: 'create',
|
|
576
599
|
selectedOfferDetails: [],
|
|
577
600
|
incentivesData: null,
|