@capillarytech/creatives-library 9.0.13 → 9.0.14
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 +10 -0
- package/services/tests/api.test.js +83 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/WhatsAppPreviewContent.js +5 -3
- package/v2Components/CommonTestAndPreview/index.js +7 -0
- package/v2Components/NavigationBar/index.js +27 -0
- package/v2Components/NavigationBar/messages.js +4 -0
- package/v2Components/NavigationBar/tests/index.test.js +19 -0
- package/v2Components/NewCallTask/index.js +6 -1
- package/v2Components/TemplatePreview/index.js +4 -2
- package/v2Containers/Cap/index.js +3 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +130 -20
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +154 -0
- package/v2Containers/CommunicationFlow/CommunicationFlowCard.js +240 -0
- package/v2Containers/CommunicationFlow/DemoPage.js +47 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +369 -2
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlowCard.test.js +619 -0
- package/v2Containers/CommunicationFlow/Tests/DemoPage.test.js +77 -0
- package/v2Containers/CommunicationFlow/Tests/getContentBody.test.js +933 -0
- package/v2Containers/CommunicationFlow/constants.js +45 -10
- package/v2Containers/CommunicationFlow/index.js +5 -2
- package/v2Containers/CommunicationFlow/messages.js +20 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +94 -31
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +14 -11
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +1144 -32
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/extractContentForPreview.js +183 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +3 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +39 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +6 -2
- package/v2Containers/CommunicationFlow/utils/getContentBody.js +369 -0
- package/v2Containers/CommunicationFlow/utils/getContentBody.scss +19 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +1 -1
- package/v2Containers/CreativesContainer/constants.js +6 -0
- package/v2Containers/CreativesContainer/index.js +68 -1
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +2 -2
- package/v2Containers/Templates/index.js +2 -2
- package/v2Containers/TemplatesV2/index.js +9 -1
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +41 -34
package/package.json
CHANGED
package/services/api.js
CHANGED
|
@@ -693,6 +693,16 @@ export const createCentralCommsMetaId = (payload, metaType = TRANSACTION) => {
|
|
|
693
693
|
return request(url, getAPICallObject('POST', payload, false, false, false, true));
|
|
694
694
|
};
|
|
695
695
|
|
|
696
|
+
export const getCentralCommsMetaIds = (metaIds, metaType = TRANSACTION) => {
|
|
697
|
+
const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}?metaIds=${metaIds}`;
|
|
698
|
+
return request(url, getAPICallObject('GET', null, false, false, false, true));
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
export const bulkClaimAndApprove = (payload, metaType = TRANSACTION) => {
|
|
702
|
+
const url = `${API_ENDPOINT}/common/central-comms/bulk-claim-approve/${metaType}`;
|
|
703
|
+
return request(url, getAPICallObject('POST', payload, false, false, false, true));
|
|
704
|
+
};
|
|
705
|
+
|
|
696
706
|
export const updateMetaConfig = (payload, metaType = TRANSACTION, metaId) => {
|
|
697
707
|
const url = `${API_ENDPOINT}/common/central-comms/meta-id/${metaType}/${metaId}`;
|
|
698
708
|
return request(url, getAPICallObject('PATCH', payload, false, false, false, true));
|
|
@@ -1092,6 +1092,8 @@ import {
|
|
|
1092
1092
|
unarchiveTemplate,
|
|
1093
1093
|
bulkArchiveTemplates,
|
|
1094
1094
|
bulkUnarchiveTemplates,
|
|
1095
|
+
getCentralCommsMetaIds,
|
|
1096
|
+
bulkClaimAndApprove,
|
|
1095
1097
|
} from '../api';
|
|
1096
1098
|
|
|
1097
1099
|
describe('archiveTemplate', () => {
|
|
@@ -1145,3 +1147,84 @@ describe('bulkUnarchiveTemplates', () => {
|
|
|
1145
1147
|
expect(lastCall[1].method).toBe('PUT');
|
|
1146
1148
|
});
|
|
1147
1149
|
});
|
|
1150
|
+
|
|
1151
|
+
describe('getCentralCommsMetaIds', () => {
|
|
1152
|
+
beforeEach(() => {
|
|
1153
|
+
global.fetch = jest.fn().mockReturnValue(Promise.resolve({
|
|
1154
|
+
status: 200,
|
|
1155
|
+
json: () => Promise.resolve({ status: 200, response: [] }),
|
|
1156
|
+
}));
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1159
|
+
afterEach(() => {
|
|
1160
|
+
jest.restoreAllMocks();
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
it('should return a Promise', () => {
|
|
1164
|
+
const result = getCentralCommsMetaIds('1,2,3');
|
|
1165
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1166
|
+
});
|
|
1167
|
+
|
|
1168
|
+
it('should call GET on central-comms/meta-id endpoint with default TRANSACTION type', () => {
|
|
1169
|
+
getCentralCommsMetaIds('1,2,3');
|
|
1170
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1171
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1172
|
+
expect(lastCall[0]).toContain('central-comms/meta-id/TRANSACTION');
|
|
1173
|
+
expect(lastCall[0]).toContain('metaIds=1,2,3');
|
|
1174
|
+
expect(lastCall[1].method).toBe('GET');
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('should use provided metaType in URL', () => {
|
|
1178
|
+
getCentralCommsMetaIds('4,5', 'EVENT');
|
|
1179
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1180
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1181
|
+
expect(lastCall[0]).toContain('central-comms/meta-id/EVENT');
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
it('should handle fetch failure', async () => {
|
|
1185
|
+
global.fetch.mockRejectedValue({ error: 'Network error' });
|
|
1186
|
+
const result = await getCentralCommsMetaIds('1,2,3');
|
|
1187
|
+
expect(result).toEqual({ error: 'Network error' });
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
describe('bulkClaimAndApprove', () => {
|
|
1192
|
+
beforeEach(() => {
|
|
1193
|
+
global.fetch = jest.fn().mockReturnValue(Promise.resolve({
|
|
1194
|
+
status: 200,
|
|
1195
|
+
json: () => Promise.resolve({ status: 200, response: { success: true } }),
|
|
1196
|
+
}));
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
afterEach(() => {
|
|
1200
|
+
jest.restoreAllMocks();
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
it('should return a Promise', () => {
|
|
1204
|
+
const result = bulkClaimAndApprove({ ids: ['1', '2'] });
|
|
1205
|
+
expect(result).toBeInstanceOf(Promise);
|
|
1206
|
+
});
|
|
1207
|
+
|
|
1208
|
+
it('should call POST on central-comms/bulk-claim-approve endpoint with default TRANSACTION type', () => {
|
|
1209
|
+
const payload = { ids: ['1', '2'] };
|
|
1210
|
+
bulkClaimAndApprove(payload);
|
|
1211
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1212
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1213
|
+
expect(lastCall[0]).toContain('central-comms/bulk-claim-approve/TRANSACTION');
|
|
1214
|
+
expect(lastCall[1].method).toBe('POST');
|
|
1215
|
+
expect(lastCall[1].body).toBe(JSON.stringify(payload));
|
|
1216
|
+
});
|
|
1217
|
+
|
|
1218
|
+
it('should use provided metaType in URL', () => {
|
|
1219
|
+
bulkClaimAndApprove({ ids: ['3'] }, 'EVENT');
|
|
1220
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
1221
|
+
const lastCall = global.fetch.mock.calls[global.fetch.mock.calls.length - 1];
|
|
1222
|
+
expect(lastCall[0]).toContain('central-comms/bulk-claim-approve/EVENT');
|
|
1223
|
+
});
|
|
1224
|
+
|
|
1225
|
+
it('should handle fetch failure', async () => {
|
|
1226
|
+
global.fetch.mockRejectedValue({ error: 'Network error' });
|
|
1227
|
+
const result = await bulkClaimAndApprove({ ids: ['1'] });
|
|
1228
|
+
expect(result).toEqual({ error: 'Network error' });
|
|
1229
|
+
});
|
|
1230
|
+
});
|
|
@@ -17,7 +17,7 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
|
17
17
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
18
18
|
import { ANDROID, IOS } from '../constants';
|
|
19
19
|
import messages from '../messages';
|
|
20
|
-
import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../../v2Containers/Whatsapp/utils';
|
|
20
|
+
import { getWhatsappQuickReply, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../../../v2Containers/Whatsapp/utils';
|
|
21
21
|
import { QUICK_REPLY, PHONE_NUMBER, WHATSAPP_CATEGORIES, TEMPLATE_VARIABLE_REGEX } from '../../../v2Containers/Whatsapp/constants';
|
|
22
22
|
import videoPlay from '../../../assets/videoPlay.svg';
|
|
23
23
|
import whatsappImageEmptyPreview from '../../TemplatePreview/assets/images/empty_image_preview.svg';
|
|
@@ -258,8 +258,10 @@ const WhatsAppPreviewContent = ({
|
|
|
258
258
|
|
|
259
259
|
{/* Document Preview */}
|
|
260
260
|
{content?.docPreview && (
|
|
261
|
-
<CapRow
|
|
262
|
-
{content.docPreview
|
|
261
|
+
<CapRow className="whatsapp-image">
|
|
262
|
+
{React.isValidElement(content.docPreview)
|
|
263
|
+
? content.docPreview
|
|
264
|
+
: getWhatsappDocPreview(content.docPreview)}
|
|
263
265
|
</CapRow>
|
|
264
266
|
)}
|
|
265
267
|
|
|
@@ -394,6 +394,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
394
394
|
return '';
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
// WHATSAPP channel - return template message string (content is a complex object with JSX nodes,
|
|
398
|
+
// JSON.stringify would crash; the preview uses the content prop directly via WhatsAppPreviewContent)
|
|
399
|
+
if (channel === CHANNELS.WHATSAPP) {
|
|
400
|
+
if (typeof content === 'string') return content;
|
|
401
|
+
return content?.templateMsg || '';
|
|
402
|
+
}
|
|
403
|
+
|
|
397
404
|
// For SMS and other string-based channels, return content as-is if it's already a string
|
|
398
405
|
// Don't stringify strings as it adds unnecessary quotes
|
|
399
406
|
if (channel === CHANNELS.SMS && typeof content === 'string') {
|
|
@@ -67,11 +67,23 @@ export class NavigationBar extends React.Component {
|
|
|
67
67
|
return {
|
|
68
68
|
selectedProduct: formatMessage(messages.loyaltyProgram),
|
|
69
69
|
helpUrl: LOYALTY_HELP_URL,
|
|
70
|
+
settingsIcon: {
|
|
71
|
+
iconType: 'settings',
|
|
72
|
+
key: 'settings',
|
|
73
|
+
placement: 'bottomRight',
|
|
74
|
+
className: 'navigation-setting-icon',
|
|
75
|
+
toolTip: formatMessage(messages.loyaltyCreativeSettings),
|
|
76
|
+
},
|
|
70
77
|
};
|
|
71
78
|
default:
|
|
72
79
|
return {
|
|
73
80
|
selectedProduct: formatMessage(messages.selectedProductDefault),
|
|
74
81
|
helpUrl: HELP_URL,
|
|
82
|
+
settingsIcon: {
|
|
83
|
+
iconType: 'settings',
|
|
84
|
+
key: 'settings',
|
|
85
|
+
onClickHandler: this.onSettingsIconClick,
|
|
86
|
+
},
|
|
75
87
|
};
|
|
76
88
|
}
|
|
77
89
|
}
|
|
@@ -83,6 +95,16 @@ export class NavigationBar extends React.Component {
|
|
|
83
95
|
}
|
|
84
96
|
};
|
|
85
97
|
|
|
98
|
+
onSettingsIconClick = () => {
|
|
99
|
+
const { settingsUrl } = this.props;
|
|
100
|
+
if (settingsUrl) {
|
|
101
|
+
const stateObj = {
|
|
102
|
+
page: "settings",
|
|
103
|
+
};
|
|
104
|
+
window.history.pushState(stateObj, 'Settings', window.location.replace(settingsUrl));
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
86
108
|
onHelpIconClick = () => {
|
|
87
109
|
const { helpUrl } = this.state;
|
|
88
110
|
if (helpUrl) {
|
|
@@ -91,6 +113,7 @@ export class NavigationBar extends React.Component {
|
|
|
91
113
|
};
|
|
92
114
|
|
|
93
115
|
getTopbarIcons = (showDocumentationBot = false) => {
|
|
116
|
+
const { settingsIcon } = this.state;
|
|
94
117
|
const ICONS = [
|
|
95
118
|
{
|
|
96
119
|
iconType: 'help',
|
|
@@ -98,6 +121,9 @@ export class NavigationBar extends React.Component {
|
|
|
98
121
|
onClickHandler: this.onHelpIconClick,
|
|
99
122
|
},
|
|
100
123
|
];
|
|
124
|
+
if (settingsIcon) {
|
|
125
|
+
ICONS.push(settingsIcon);
|
|
126
|
+
}
|
|
101
127
|
return showDocumentationBot ? ICONS.slice(1) : ICONS; // If showDocumentationBot is true, help icon will be replaced by Aira icon on UI
|
|
102
128
|
};
|
|
103
129
|
|
|
@@ -196,6 +222,7 @@ NavigationBar.propTypes = {
|
|
|
196
222
|
topbarMenuData: PropTypes.array,
|
|
197
223
|
logout: PropTypes.func,
|
|
198
224
|
changeOrg: PropTypes.func,
|
|
225
|
+
settingsUrl: PropTypes.string,
|
|
199
226
|
children: PropTypes.node,
|
|
200
227
|
orgSettingsUrl: PropTypes.string,
|
|
201
228
|
intl: intlShape.isRequired,
|
|
@@ -65,5 +65,9 @@ export default defineMessages({
|
|
|
65
65
|
"selectOrganization": {
|
|
66
66
|
id: `${scope}.selectOrganization`,
|
|
67
67
|
defaultMessage: 'Select organization',
|
|
68
|
+
},
|
|
69
|
+
"loyaltyCreativeSettings": {
|
|
70
|
+
id: `${scope}.loyaltyCreativeSettings`,
|
|
71
|
+
defaultMessage: 'There are no settings for creatives within Loyalty+. Channel-related settings are available within organisation settings',
|
|
68
72
|
}
|
|
69
73
|
});
|
|
@@ -71,4 +71,23 @@ describe('NavigationBar', () => {
|
|
|
71
71
|
const loyaltyProduct = screen.getByText('Loyalty+');
|
|
72
72
|
expect(loyaltyProduct).toBeInTheDocument();
|
|
73
73
|
});
|
|
74
|
+
|
|
75
|
+
// Regression: AntD v3 -> v6 migration (CAP-183930) dropped the settings (gear)
|
|
76
|
+
// icon from the top nav. These tests ensure the gear keeps rendering.
|
|
77
|
+
it('Should render the settings (gear) icon in the top nav bar', () => {
|
|
78
|
+
const updatedProps = cloneDeep(props);
|
|
79
|
+
updatedProps.location.pathname = '/creatives/ui/v2';
|
|
80
|
+
updatedProps.settingsUrl = '/campaigns/ui/creatives/settings/message';
|
|
81
|
+
renderComponent(updatedProps);
|
|
82
|
+
const settingsIcon = document.querySelector('.cap-icon-v2-settings');
|
|
83
|
+
expect(settingsIcon).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('Should render the settings (gear) icon for the loyalty product', () => {
|
|
87
|
+
const updatedProps = cloneDeep(props);
|
|
88
|
+
updatedProps.location.pathname = '/creatives/ui/v2/loyalty';
|
|
89
|
+
renderComponent(updatedProps);
|
|
90
|
+
const settingsIcon = document.querySelector('.cap-icon-v2-settings.navigation-setting-icon');
|
|
91
|
+
expect(settingsIcon).toBeInTheDocument();
|
|
92
|
+
});
|
|
74
93
|
});
|
|
@@ -16,6 +16,7 @@ import CallTaskPreview from '../CallTaskPreview';
|
|
|
16
16
|
import messages from './messages';
|
|
17
17
|
import './_newCallTask.scss';
|
|
18
18
|
import { hasStore2DoorFeature } from '../../utils/common';
|
|
19
|
+
import { EDIT } from '../../constants/unified';
|
|
19
20
|
|
|
20
21
|
const CALL = 'CALL';
|
|
21
22
|
const STOREMAX_WHATSAPP = 'STOREMAX_WHATSAPP';
|
|
@@ -27,7 +28,11 @@ class NewCallTask extends React.Component { // eslint-disable-line react/prefer-
|
|
|
27
28
|
|
|
28
29
|
getInitialState = () => {
|
|
29
30
|
const { templateData, messageDetails, mode } = this.props;
|
|
30
|
-
const actionType = mode ===
|
|
31
|
+
const actionType = mode === EDIT
|
|
32
|
+
? (messageDetails?.messageContent?.message_content_id_1?.callTaskActionType
|
|
33
|
+
|| templateData?.callTaskActionType
|
|
34
|
+
|| CALL)
|
|
35
|
+
: CALL;
|
|
31
36
|
return {
|
|
32
37
|
subject: templateData && templateData.subject ? templateData.subject : '',
|
|
33
38
|
messageBody: templateData && templateData.messageBody ? templateData.messageBody : '',
|
|
@@ -47,7 +47,7 @@ import { handlePreviewInNewTab } from '../../utils/common';
|
|
|
47
47
|
import { TEMPLATE, IMAGE_CAROUSEL, IMAGE, STICKER, TEXT, IMAGE_MAP, VIDEO } from '../../v2Containers/Line/Container/constants';
|
|
48
48
|
import CapFacebookPreview from '../../v2Containers/CapFacebookPreview';
|
|
49
49
|
import WhatsappStatusContainer from '../WhatsappStatusContainer';
|
|
50
|
-
import { getWhatsappQuickReply, getWhatsappCarouselButtonView } from '../../v2Containers/Whatsapp/utils';
|
|
50
|
+
import { getWhatsappQuickReply, getWhatsappCarouselButtonView, getWhatsappDocPreview } from '../../v2Containers/Whatsapp/utils';
|
|
51
51
|
import { QUICK_REPLY, WHATSAPP_CATEGORIES, PHONE_NUMBER, TEMPLATE_VARIABLE_REGEX } from '../../v2Containers/Whatsapp/constants';
|
|
52
52
|
import { RCS_BUTTON_TYPES, LEFT, HORIZONTAL, VERTICAL, RIGHT} from '../../v2Containers/Rcs/constants';
|
|
53
53
|
import { ANDROID, INAPP_MESSAGE_LAYOUT_TYPES } from '../../v2Containers/InApp/constants';
|
|
@@ -1234,7 +1234,9 @@ export class TemplatePreview extends React.Component { // eslint-disable-line re
|
|
|
1234
1234
|
)}
|
|
1235
1235
|
{content?.docPreview && (
|
|
1236
1236
|
<div className="whatsapp-image">
|
|
1237
|
-
{content
|
|
1237
|
+
{React.isValidElement(content.docPreview)
|
|
1238
|
+
? content.docPreview
|
|
1239
|
+
: getWhatsappDocPreview(content.docPreview)}
|
|
1238
1240
|
</div>
|
|
1239
1241
|
)}
|
|
1240
1242
|
{content?.templateHeaderPreview || ""}
|
|
@@ -18,7 +18,7 @@ import * as locationActions from '../LanguageProvider/actions';
|
|
|
18
18
|
import * as appActions from '../App/actions';
|
|
19
19
|
import config from '../../config/app';
|
|
20
20
|
import NavigationBar from '../../v2Components/NavigationBar';
|
|
21
|
-
import { publicPath } from '../../config/path';
|
|
21
|
+
import { engagePlusPublicPath, publicPath } from '../../config/path';
|
|
22
22
|
import { GTM_TRACKING_ID, CREATIVES_UI_VIEW, FAILURE } from '../App/constants';
|
|
23
23
|
import { makeSelectLocale } from '../../v2Containers/LanguageProvider/selectors';
|
|
24
24
|
import CapSupportVideosWrapper from '@capillarytech/cap-ui-library/CapSupportVideosWrapper';
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
REQUEST,
|
|
31
31
|
DEFAULT,
|
|
32
32
|
ENABLE_PRODUCT_SUPPORT_VIDEOS,
|
|
33
|
+
CAMPAIGN_SETTINGS_URL,
|
|
33
34
|
} from './constants';
|
|
34
35
|
import './_cap.scss';
|
|
35
36
|
import { Switch } from 'react-router';
|
|
@@ -528,6 +529,7 @@ export class Cap extends React.Component { // eslint-disable-line react/prefer-s
|
|
|
528
529
|
changeOrg={this.changeOrg}
|
|
529
530
|
isCreativesAccessible={isCreativesAccessible}
|
|
530
531
|
logout={this.logout}
|
|
532
|
+
settingsUrl={`${engagePlusPublicPath}${CAMPAIGN_SETTINGS_URL}`}
|
|
531
533
|
orgSettingsUrl={ORG_SETTINGS_URL}
|
|
532
534
|
loggedIn={isLoggedIn}
|
|
533
535
|
topbarMenuData={topbarMenuDataOptions}
|
|
@@ -20,6 +20,7 @@ import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
|
20
20
|
// import injectSaga from '../../utils/injectSaga'; // cap-coupons flows disabled
|
|
21
21
|
// import injectReducer from '../../utils/injectReducer';
|
|
22
22
|
import { makeSelectAuthenticated } from '../Cap/selectors';
|
|
23
|
+
import { createCentralCommsMetaId, getCentralCommsMetaIds } from '../../services/api';
|
|
23
24
|
import DynamicControlsStep from './steps/DynamicControlsStep';
|
|
24
25
|
import MessageTypeStep from './steps/MessageTypeStep';
|
|
25
26
|
import CommunicationStrategyStep from './steps/CommunicationStrategyStep';
|
|
@@ -28,6 +29,15 @@ import {
|
|
|
28
29
|
STEPS,
|
|
29
30
|
CHANNEL_PRIORITY,
|
|
30
31
|
AB_TEST,
|
|
32
|
+
DEFAULT_COMMUNICATION_STRATEGY_OPTIONS,
|
|
33
|
+
CHANNELS_WITHOUT_DELIVERY,
|
|
34
|
+
MESSAGE_TYPES_OPTIONS,
|
|
35
|
+
CHANNELS,
|
|
36
|
+
INCENTIVE_TYPES,
|
|
37
|
+
DYNAMIC_CONTROLS_CONFIG,
|
|
38
|
+
CHANNEL_CONTENT_KEY_MAP,
|
|
39
|
+
CHANNEL_DELIVERY_KEY_MAP,
|
|
40
|
+
CAMPAIGNS,
|
|
31
41
|
} from './constants';
|
|
32
42
|
import { getEnabledSteps } from './utils/getEnabledSteps';
|
|
33
43
|
import messages from './messages';
|
|
@@ -43,6 +53,17 @@ import './CommunicationFlow.scss';
|
|
|
43
53
|
// saga: CouponsCapContainer.couponsCapSaga,
|
|
44
54
|
// });
|
|
45
55
|
|
|
56
|
+
const getDeliveryChannels = (contentItems) => {
|
|
57
|
+
const channels = contentItems.map((item) => (item.channel || '').toUpperCase()).filter(Boolean);
|
|
58
|
+
const unique = [...new Set(channels)];
|
|
59
|
+
return unique.filter((channel) => !CHANNELS_WITHOUT_DELIVERY.includes(channel));
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const hasDeliverySettingForChannel = (channelSetting, channel) => {
|
|
63
|
+
const settings = channelSetting[channel];
|
|
64
|
+
return !!settings && Object.values(settings).some((v) => v !== null && v !== '' && v !== undefined);
|
|
65
|
+
};
|
|
66
|
+
|
|
46
67
|
const CommunicationFlow = ({
|
|
47
68
|
config,
|
|
48
69
|
initialData,
|
|
@@ -50,13 +71,14 @@ const CommunicationFlow = ({
|
|
|
50
71
|
onCancel, // eslint-disable-line
|
|
51
72
|
onChange,
|
|
52
73
|
intl,
|
|
53
|
-
|
|
74
|
+
cap, // From parent consumer (e.g. campaigns passes campaignCap) — takes priority over Redux capData
|
|
75
|
+
capData, // From Redux - fallback when cap is not provided by consumer
|
|
54
76
|
}) => {
|
|
55
77
|
const { formatMessage } = intl || {};
|
|
56
78
|
const { messageTypeData = {}, communicationStrategyData = {}, contentTemplateData = {} } = config?.features || {};
|
|
57
79
|
// Initialize step data from initialData or defaults
|
|
58
80
|
const [stepData, setStepData] = useState(() => {
|
|
59
|
-
const defaultMessageType = messageTypeData.defaultOption?.value || null;
|
|
81
|
+
const defaultMessageType = messageTypeData.defaultOption?.value || MESSAGE_TYPES_OPTIONS?.[1]?.value || null;
|
|
60
82
|
return {
|
|
61
83
|
messageType: initialData?.messageType || defaultMessageType,
|
|
62
84
|
communicationStrategy: initialData?.communicationStrategy || null,
|
|
@@ -73,6 +95,27 @@ const CommunicationFlow = ({
|
|
|
73
95
|
// Memoize enabled steps
|
|
74
96
|
const enabledSteps = useMemo(() => getEnabledSteps(config), [config]);
|
|
75
97
|
|
|
98
|
+
const isSaveDisabled = useMemo(() => {
|
|
99
|
+
const strategyRequired = enabledSteps.includes(STEPS.COMMUNICATION_STRATEGY);
|
|
100
|
+
const templateRequired = enabledSteps.includes(STEPS.CHANNEL_SELECTION);
|
|
101
|
+
const isMultiChannel = [CHANNEL_PRIORITY, AB_TEST].includes(stepData.communicationStrategy);
|
|
102
|
+
|
|
103
|
+
if (strategyRequired && !stepData.communicationStrategy) return true;
|
|
104
|
+
if (templateRequired && !isMultiChannel && (!stepData.contentItems || stepData.contentItems.length === 0)) return true;
|
|
105
|
+
|
|
106
|
+
const deliverySettingsEnabled = !!config?.features?.deliverySettingsData;
|
|
107
|
+
if (deliverySettingsEnabled && templateRequired) {
|
|
108
|
+
const deliveryChannels = getDeliveryChannels(stepData.contentItems || []);
|
|
109
|
+
const channelSetting = stepData.deliverySetting?.channelSetting || {};
|
|
110
|
+
const anyChannelMissingSenderDetails = deliveryChannels.some(
|
|
111
|
+
(channel) => !hasDeliverySettingForChannel(channelSetting, channel),
|
|
112
|
+
);
|
|
113
|
+
if (anyChannelMissingSenderDetails) return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return false;
|
|
117
|
+
}, [enabledSteps, stepData.communicationStrategy, stepData.contentItems, stepData.deliverySetting, config?.features?.deliverySettingsData]);
|
|
118
|
+
|
|
76
119
|
/**
|
|
77
120
|
* Get aggregated data from all steps
|
|
78
121
|
*/
|
|
@@ -106,6 +149,77 @@ const CommunicationFlow = ({
|
|
|
106
149
|
}));
|
|
107
150
|
}, []);
|
|
108
151
|
|
|
152
|
+
const handleSave = useCallback(async () => {
|
|
153
|
+
const aggregatedData = getAggregatedData();
|
|
154
|
+
const shouldUseCCS = config?.useCCS !== false;
|
|
155
|
+
|
|
156
|
+
if (shouldUseCCS) {
|
|
157
|
+
const ouId = config?.context?.ouId || -1;
|
|
158
|
+
const module = config?.context?.module
|
|
159
|
+
|| (config?.consumer ? config.consumer.toUpperCase() : CAMPAIGNS);
|
|
160
|
+
|
|
161
|
+
const channelContentKeyMap = CHANNEL_CONTENT_KEY_MAP;
|
|
162
|
+
const channelDeliveryKeyMap = CHANNEL_DELIVERY_KEY_MAP;
|
|
163
|
+
|
|
164
|
+
const contentItems = aggregatedData.contentItems || [];
|
|
165
|
+
const { dynamicControls = {} } = aggregatedData;
|
|
166
|
+
|
|
167
|
+
const additionalSettings = {
|
|
168
|
+
useTinyUrl: dynamicControls.useTinyUrl ?? false,
|
|
169
|
+
encryptUrl: dynamicControls.sendToControlCustomers ?? false,
|
|
170
|
+
linkTrackingEnabled: dynamicControls.overrideDailyLimit ?? false,
|
|
171
|
+
userSubscriptionDisabled: dynamicControls.sendToBrandPocs ?? false,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
if (contentItems.length > 0) {
|
|
175
|
+
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
|
+
},
|
|
201
|
+
};
|
|
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);
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('[CommunicationFlow] CCS createCentralCommsMetaId error:', error);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
onSave(aggregatedData);
|
|
221
|
+
}, [getAggregatedData, config, onSave]);
|
|
222
|
+
|
|
109
223
|
// Call onChange callback when stepData changes
|
|
110
224
|
useEffect(() => {
|
|
111
225
|
if (onChange) {
|
|
@@ -131,8 +245,8 @@ const CommunicationFlow = ({
|
|
|
131
245
|
<MessageTypeStep
|
|
132
246
|
{...commonProps}
|
|
133
247
|
value={stepData.messageType}
|
|
134
|
-
options={messageTypeData.options}
|
|
135
|
-
defaultOption={messageTypeData.defaultOption}
|
|
248
|
+
options={messageTypeData.options || MESSAGE_TYPES_OPTIONS}
|
|
249
|
+
defaultOption={messageTypeData.defaultOption || MESSAGE_TYPES_OPTIONS?.[1]}
|
|
136
250
|
onChange={(messageType) => handleStepChange(step, { messageType })}
|
|
137
251
|
/>
|
|
138
252
|
<CapDivider />
|
|
@@ -145,7 +259,7 @@ const CommunicationFlow = ({
|
|
|
145
259
|
{...commonProps}
|
|
146
260
|
value={stepData.communicationStrategy}
|
|
147
261
|
// messageType={stepData.messageType}
|
|
148
|
-
options={communicationStrategyData.options}
|
|
262
|
+
options={communicationStrategyData.options || DEFAULT_COMMUNICATION_STRATEGY_OPTIONS}
|
|
149
263
|
disabled={communicationStrategyData.disabled}
|
|
150
264
|
onChange={(communicationStrategy) => handleStepChange(step, { communicationStrategy })}
|
|
151
265
|
/>
|
|
@@ -162,30 +276,23 @@ const CommunicationFlow = ({
|
|
|
162
276
|
<ChannelSelectionStep
|
|
163
277
|
{...commonProps}
|
|
164
278
|
value={stepData}
|
|
165
|
-
channels={contentTemplateData.channels}
|
|
279
|
+
channels={contentTemplateData.channels || CHANNELS}
|
|
166
280
|
onChange={(data) => handleStepChange(step, data)}
|
|
167
281
|
channelsToHide={contentTemplateData.channelsToHide}
|
|
168
282
|
channelsToDisable={contentTemplateData.channelsToDisable}
|
|
169
283
|
creativesMode={config.mode || 'create'}
|
|
170
284
|
selectedOfferDetails={stepData.selectedOfferDetails}
|
|
171
|
-
incentivesData={
|
|
285
|
+
incentivesData={{
|
|
286
|
+
...config.features?.incentivesData,
|
|
287
|
+
types: config.features?.incentivesData?.types || INCENTIVE_TYPES,
|
|
288
|
+
}}
|
|
172
289
|
deliverySettingsData={config.features?.deliverySettingsData}
|
|
173
290
|
config={config}
|
|
174
|
-
capData={capData}
|
|
291
|
+
capData={cap || capData}
|
|
175
292
|
/>
|
|
176
293
|
<CapDivider />
|
|
177
294
|
</CapRow>
|
|
178
295
|
);
|
|
179
|
-
// This will be added back in when coupons/points are integrated so keeping it commented out for now
|
|
180
|
-
// case STEPS.INCENTIVES:
|
|
181
|
-
// return (
|
|
182
|
-
// <IncentivesStep
|
|
183
|
-
// key={step}
|
|
184
|
-
// {...commonProps}
|
|
185
|
-
// value={stepData.selectedOfferDetails}
|
|
186
|
-
// onChange={(selectedOfferDetails) => handleStepChange(step, { selectedOfferDetails })}
|
|
187
|
-
// />
|
|
188
|
-
// );
|
|
189
296
|
case STEPS.DYNAMIC_CONTROLS:
|
|
190
297
|
// Only show DynamicControlsStep if communication strategy is selected
|
|
191
298
|
if (!stepData.communicationStrategy) {
|
|
@@ -196,7 +303,7 @@ const CommunicationFlow = ({
|
|
|
196
303
|
key={step}
|
|
197
304
|
{...commonProps}
|
|
198
305
|
value={{ dynamicControls: stepData.dynamicControls }}
|
|
199
|
-
controls={config.features?.dynamicControlsData?.controls ||
|
|
306
|
+
controls={config.features?.dynamicControlsData?.controls || DYNAMIC_CONTROLS_CONFIG}
|
|
200
307
|
onChange={(data) => handleStepChange(step, { dynamicControls: data.dynamicControls })}
|
|
201
308
|
/>
|
|
202
309
|
);
|
|
@@ -210,7 +317,7 @@ const CommunicationFlow = ({
|
|
|
210
317
|
{renderSteps()}
|
|
211
318
|
{onSave && (
|
|
212
319
|
<CapRow useLegacy className="communication-flow-container__footer">
|
|
213
|
-
<CapButton type="primary" onClick={
|
|
320
|
+
<CapButton type="primary" onClick={handleSave} disabled={isSaveDisabled}>
|
|
214
321
|
{formatMessage(messages.save)}
|
|
215
322
|
</CapButton>
|
|
216
323
|
</CapRow>
|
|
@@ -262,18 +369,21 @@ CommunicationFlow.propTypes = {
|
|
|
262
369
|
incentivesTypes: PropTypes.arrayOf(PropTypes.oneOf(['coupons', 'points', 'promotions', 'giftVouchers', 'badges'])),
|
|
263
370
|
}),
|
|
264
371
|
context: PropTypes.object,
|
|
372
|
+
useCCS: PropTypes.bool,
|
|
265
373
|
}).isRequired,
|
|
266
374
|
initialData: PropTypes.object,
|
|
267
375
|
onSave: PropTypes.func.isRequired,
|
|
268
376
|
onCancel: PropTypes.func.isRequired,
|
|
269
377
|
onChange: PropTypes.func,
|
|
270
378
|
intl: PropTypes.object.isRequired,
|
|
379
|
+
cap: PropTypes.object,
|
|
271
380
|
capData: PropTypes.object,
|
|
272
381
|
};
|
|
273
382
|
|
|
274
383
|
CommunicationFlow.defaultProps = {
|
|
275
384
|
initialData: null,
|
|
276
385
|
onChange: null,
|
|
386
|
+
cap: null,
|
|
277
387
|
capData: {},
|
|
278
388
|
};
|
|
279
389
|
|