@capillarytech/creatives-library 8.0.317 → 8.0.319
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 +1 -0
- package/package.json +1 -1
- package/services/api.js +6 -0
- package/services/tests/api.test.js +7 -0
- package/utils/common.js +6 -1
- package/v2Containers/CommunicationFlow/CommunicationFlow.js +291 -0
- package/v2Containers/CommunicationFlow/CommunicationFlow.scss +25 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +255 -0
- package/v2Containers/CommunicationFlow/constants.js +200 -0
- package/v2Containers/CommunicationFlow/index.js +102 -0
- package/v2Containers/CommunicationFlow/messages.js +346 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.js +522 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/ChannelSelectionStep.scss +170 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/Tests/ChannelSelectionStep.test.js +796 -0
- package/v2Containers/CommunicationFlow/steps/ChannelSelectionStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/CommunicationStrategyStep.js +95 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/Tests/CommunicationStrategyStep.test.js +133 -0
- package/v2Containers/CommunicationFlow/steps/CommunicationStrategyStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.js +289 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/DeliverySettingsSection.scss +70 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.js +319 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/SenderDetails.scss +69 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/DeliverySettingsSection.test.js +616 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/SenderDetails.test.js +577 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/Tests/deliverySettingsConfig.test.js +1111 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/deliverySettingsConfig.js +696 -0
- package/v2Containers/CommunicationFlow/steps/DeliverySettingsStep/index.js +7 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.js +102 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/DynamicControlsStep.scss +36 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/Tests/DynamicControlsStep.test.js +91 -0
- package/v2Containers/CommunicationFlow/steps/DynamicControlsStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/MessageTypeStep.js +86 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/Tests/MessageTypeStep.test.js +100 -0
- package/v2Containers/CommunicationFlow/steps/MessageTypeStep/index.js +5 -0
- package/v2Containers/CommunicationFlow/utils/getEnabledSteps.js +30 -0
- package/v2Containers/CreativesContainer/constants.js +3 -0
- package/v2Containers/CreativesContainer/index.js +1 -1
- package/v2Containers/Rcs/index.js +4 -2
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChannelSelectionStep Component
|
|
3
|
+
*
|
|
4
|
+
* Content template selection with channel dropdown
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useState, useCallback, useMemo } from 'react';
|
|
8
|
+
import PropTypes from 'prop-types';
|
|
9
|
+
import { injectIntl } from 'react-intl';
|
|
10
|
+
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
11
|
+
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
12
|
+
import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
|
|
13
|
+
import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
|
|
14
|
+
import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
|
|
15
|
+
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
16
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
17
|
+
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
18
|
+
import CapCustomCard from '@capillarytech/cap-ui-library/CapCustomCard';
|
|
19
|
+
import CreativesContainer from '../../../CreativesContainer';
|
|
20
|
+
import { DeliverySettingsSection } from '../DeliverySettingsStep';
|
|
21
|
+
import { CHANNELS } from '../../constants';
|
|
22
|
+
import { VIBER, MOBILE_PUSH, MPUSH, SMS, EMAIL } from '../../../CreativesContainer/constants';
|
|
23
|
+
// TemplatesV2 pane keys (for channelsToHide) - derived from CHANNELS + FTP
|
|
24
|
+
const CREATIVES_PANE_KEYS = [...CHANNELS?.map((channel) => channel?.paneKey), 'FTP'];
|
|
25
|
+
// Channels that TemplatesV2 shows only when enableNewChannels includes them (not in commonChannels)
|
|
26
|
+
const ENABLE_NEW_CHANNELS = CHANNELS.filter((channel) => channel.requiresEnableNewChannels).map((channel) => channel?.value);
|
|
27
|
+
import messages from '../../messages';
|
|
28
|
+
import './ChannelSelectionStep.scss';
|
|
29
|
+
|
|
30
|
+
// cap-coupons flows commented out
|
|
31
|
+
// import { CouponsWrapper, CouponsPointsStrategy } from '@capillarytech/cap-coupons';
|
|
32
|
+
|
|
33
|
+
const generateContentId = () => `content_${Date.now()}`;
|
|
34
|
+
|
|
35
|
+
const ChannelSelectionStep = ({
|
|
36
|
+
value,
|
|
37
|
+
channels, // eslint-disable-line
|
|
38
|
+
communicationStrategy, // eslint-disable-line
|
|
39
|
+
onChange,
|
|
40
|
+
channelsToHide = [],
|
|
41
|
+
channelsToDisable = [],
|
|
42
|
+
creativesMode = 'create',
|
|
43
|
+
selectedOfferDetails = [],
|
|
44
|
+
incentivesData,
|
|
45
|
+
deliverySettingsData,
|
|
46
|
+
error,
|
|
47
|
+
intl,
|
|
48
|
+
capData, // From Redux - contains user/org info needed by CouponsWrapper
|
|
49
|
+
}) => {
|
|
50
|
+
const contentItems = value?.contentItems || [];
|
|
51
|
+
const [showCreativesContainer, setShowCreativesContainer] = useState(false);
|
|
52
|
+
const [selectedChannel, setSelectedChannel] = useState(null);
|
|
53
|
+
const [editingContentId, setEditingContentId] = useState(null);
|
|
54
|
+
const [showChannelsMenu, setShowChannelsMenu] = useState(false);
|
|
55
|
+
// Keyed by contentId (or 'standalone' for the empty-state button) so each dropdown controls its own open state
|
|
56
|
+
const [showIncentivesMenuMap, setShowIncentivesMenuMap] = useState({});
|
|
57
|
+
const { formatMessage } = intl || {};
|
|
58
|
+
|
|
59
|
+
// Available channels (filter out hidden ones)
|
|
60
|
+
// Fallback to CHANNELS constant if channels prop is not provided
|
|
61
|
+
const channelsList = channels || CHANNELS;
|
|
62
|
+
const availableChannels = useMemo(() => channelsList?.filter((channel) => channel?.isActive), [channelsList]);
|
|
63
|
+
const availableIncentiveTypes = useMemo(() => incentivesData?.types?.filter((incentive) => incentive?.isActive), [incentivesData]);
|
|
64
|
+
// Derive channel config from CHANNELS for CreativesContainer
|
|
65
|
+
const selectedChannelConfig = useMemo(() => {
|
|
66
|
+
if (!selectedChannel) return null;
|
|
67
|
+
const selectedChannelLowerCase = selectedChannel?.toLowerCase();
|
|
68
|
+
return CHANNELS.find((channel) => channel?.value?.toLowerCase() === selectedChannelLowerCase || channel?.channelProp === selectedChannelLowerCase) || null;
|
|
69
|
+
}, [selectedChannel]);
|
|
70
|
+
/**
|
|
71
|
+
* Handle CreativesContainer close
|
|
72
|
+
*/
|
|
73
|
+
const handleCloseCreatives = useCallback(() => {
|
|
74
|
+
setShowCreativesContainer(false);
|
|
75
|
+
setSelectedChannel(null);
|
|
76
|
+
setEditingContentId(null);
|
|
77
|
+
setShowChannelsMenu(false);
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Handle CreativesContainer data callback - persist content for create/edit
|
|
82
|
+
*/
|
|
83
|
+
const handleCreativesData = useCallback((data) => {
|
|
84
|
+
if (!selectedChannel || !data) return;
|
|
85
|
+
const channelValue = CHANNELS.find((channel) => channel?.channelProp === selectedChannel)?.value || selectedChannel.toUpperCase();
|
|
86
|
+
const newItem = {
|
|
87
|
+
contentId: editingContentId || generateContentId(),
|
|
88
|
+
channel: channelValue,
|
|
89
|
+
templateData: data,
|
|
90
|
+
};
|
|
91
|
+
const updatedItems = editingContentId
|
|
92
|
+
? contentItems.map((item) => (item.contentId === editingContentId ? { ...item, ...newItem, templateData: data } : item))
|
|
93
|
+
: [...contentItems, newItem];
|
|
94
|
+
onChange({ contentItems: updatedItems });
|
|
95
|
+
setShowCreativesContainer(false);
|
|
96
|
+
setSelectedChannel(null);
|
|
97
|
+
setEditingContentId(null);
|
|
98
|
+
}, [selectedChannel, editingContentId, contentItems, onChange]);
|
|
99
|
+
|
|
100
|
+
const handleEditContent = useCallback((contentId) => {
|
|
101
|
+
const item = contentItems.find((c) => c.contentId === contentId);
|
|
102
|
+
if (!item) return;
|
|
103
|
+
const channelConfig = CHANNELS.find((channel) => channel?.value === item?.channel || channel?.channelProp === item?.channel?.toLowerCase());
|
|
104
|
+
if (channelConfig) {
|
|
105
|
+
setSelectedChannel(channelConfig.channelProp);
|
|
106
|
+
setEditingContentId(contentId);
|
|
107
|
+
setShowCreativesContainer(true);
|
|
108
|
+
}
|
|
109
|
+
}, [contentItems]);
|
|
110
|
+
|
|
111
|
+
const handleDeleteContent = useCallback((contentId) => {
|
|
112
|
+
onChange({ contentItems: contentItems.filter((c) => c.contentId !== contentId) });
|
|
113
|
+
}, [contentItems, onChange]);
|
|
114
|
+
|
|
115
|
+
/** Get short preview text for content card */
|
|
116
|
+
const getContentPreview = (item) => {
|
|
117
|
+
const templateData = item?.templateData || {};
|
|
118
|
+
const channel = item?.channel?.toUpperCase();
|
|
119
|
+
if (templateData?.messageBody) return templateData.messageBody?.substring?.(0, 100) || templateData.messageBody;
|
|
120
|
+
if (templateData?.emailSubject) return templateData.emailSubject;
|
|
121
|
+
if (templateData?.smsBody) return templateData.smsBody?.substring?.(0, 100) || templateData.smsBody;
|
|
122
|
+
if (templateData?.message) return templateData.message?.substring?.(0, 100) || templateData.message;
|
|
123
|
+
// Mobile push: notificationTitle, notificationBody, or android/ios content
|
|
124
|
+
if (channel === MOBILE_PUSH || channel === MPUSH) {
|
|
125
|
+
const title = templateData?.notificationTitle || templateData?.androidContent?.notificationTitle || templateData?.iosContent?.notificationTitle;
|
|
126
|
+
const body = templateData?.notificationBody || templateData?.androidContent?.body || templateData?.iosContent?.body || templateData?.androidContent?.notificationBody || templateData?.iosContent?.notificationBody;
|
|
127
|
+
if (title) return title;
|
|
128
|
+
if (body) return body?.substring?.(0, 100) || body;
|
|
129
|
+
}
|
|
130
|
+
// Viber: messageBody is JSON string, extract content.text (campaigns pattern)
|
|
131
|
+
if (channel === VIBER) {
|
|
132
|
+
try {
|
|
133
|
+
const parsed = typeof templateData?.messageBody === 'string' ? JSON.parse(templateData?.messageBody || '{}') : templateData?.messageBody;
|
|
134
|
+
const text = parsed?.content?.text || '';
|
|
135
|
+
return text ? text?.substring?.(0, 100) : item?.channel || '';
|
|
136
|
+
} catch {
|
|
137
|
+
return templateData?.messageBody?.substring?.(0, 100) || item?.channel || '';
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return item?.channel || '';
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** Get sender details display for card footer (e.g. SMS sender ID) */
|
|
144
|
+
const getSenderDetailsDisplay = (item) => {
|
|
145
|
+
const templateData = item?.templateData || {};
|
|
146
|
+
const channel = item?.channel?.toUpperCase();
|
|
147
|
+
if (channel === SMS) {
|
|
148
|
+
const senderId = templateData?.templateConfigs?.registeredSenderIds || templateData?.header;
|
|
149
|
+
return senderId ? `SMS sender ID ${senderId}` : null;
|
|
150
|
+
}
|
|
151
|
+
if (channel === EMAIL) return templateData?.emailFrom || null;
|
|
152
|
+
// Viber: sender from templateConfigs (campaigns/adiona pattern)
|
|
153
|
+
if (channel === VIBER) {
|
|
154
|
+
const senderId = templateData?.templateConfigs?.viberSenderId || templateData?.viberSenderId;
|
|
155
|
+
return senderId ? `Viber sender ID ${senderId}` : null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
/** Map channel to CapCustomCard type for styling (like adiona getEngagementCardType) */
|
|
161
|
+
const getCardType = (channel) => {
|
|
162
|
+
const upperCaseChannel = channel?.toUpperCase();
|
|
163
|
+
const mapping = { MOBILEPUSH: MPUSH, MPUSH: MPUSH };
|
|
164
|
+
return mapping[upperCaseChannel] || channel;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const handlePreviewContent = useCallback((item) => {
|
|
168
|
+
// Placeholder for preview - can be wired to Preview slidebox when available
|
|
169
|
+
// eslint-disable-next-line no-console
|
|
170
|
+
console.log('Preview content', item?.contentId);
|
|
171
|
+
}, []);
|
|
172
|
+
|
|
173
|
+
const handleChannelSelect = (channelValue) => {
|
|
174
|
+
// Check if channel is supported (not in channelsToHide or channelsToDisable)
|
|
175
|
+
if (channelsToHide.includes(channelValue) || channelsToDisable.includes(channelValue)) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Find the channel object to verify it's active
|
|
180
|
+
const selectedChannelObj = availableChannels?.find((channel) => channel?.value === channelValue);
|
|
181
|
+
if (!selectedChannelObj || !selectedChannelObj?.isActive) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Update parent component with selected channel
|
|
186
|
+
onChange({ channel: channelValue, channels: [] });
|
|
187
|
+
|
|
188
|
+
// Open CreativesContainer - derive channelProp from config
|
|
189
|
+
const lowerCaseChannelValue = channelValue?.toLowerCase();
|
|
190
|
+
const channelConfig = lowerCaseChannelValue && CHANNELS.find((channel) => channel?.value?.toLowerCase() === lowerCaseChannelValue || channel?.channelProp === lowerCaseChannelValue);
|
|
191
|
+
if (!channelConfig?.channelProp || channelConfig.channelProp === 'ftp') {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
setSelectedChannel(channelConfig.channelProp);
|
|
196
|
+
setShowCreativesContainer(true);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const handleChannelMenuClick = ({ key }) => {
|
|
200
|
+
// key should be the channel value (e.g., 'SMS', 'EMAIL', etc.)
|
|
201
|
+
handleChannelSelect(key);
|
|
202
|
+
setShowChannelsMenu(false);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handleIncentiveSelect = (incentiveType) => {
|
|
206
|
+
// Update selectedOfferDetails with the selected incentive
|
|
207
|
+
const updatedOfferDetails = [...(selectedOfferDetails || []), { type: incentiveType }];
|
|
208
|
+
onChange({ selectedOfferDetails: updatedOfferDetails });
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const handleIncentiveMenuClick = ({ key }, cardId) => {
|
|
212
|
+
setShowIncentivesMenuMap((prev) => ({ ...prev, [cardId]: false }));
|
|
213
|
+
// cap-coupons flows disabled - skip coupons/points
|
|
214
|
+
if (key === 'coupons' || key === 'points') return;
|
|
215
|
+
handleIncentiveSelect(key);
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const handleChannelsVisibleChange = (visible) => {
|
|
219
|
+
setShowChannelsMenu(visible);
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleIncentivesVisibleChange = (visible, cardId) => {
|
|
223
|
+
setShowIncentivesMenuMap((prev) => ({ ...prev, [cardId]: visible }));
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/* cap-coupons flows disabled - handlers commented out
|
|
227
|
+
const handleClaimCoupon = useCallback((claimedCouponData) => {...}, [selectedOfferDetails, onChange]);
|
|
228
|
+
const handleEditCoupon = useCallback(() => {}, []);
|
|
229
|
+
const handleCreateCoupon = useCallback(() => {}, []);
|
|
230
|
+
const handleViewCoupon = useCallback(() => {}, []);
|
|
231
|
+
const handleEditCouponAction = useCallback(() => {}, []);
|
|
232
|
+
const handleTogglePreview = useCallback(() => {}, []);
|
|
233
|
+
const handleToggleAdvanceView = useCallback(() => {}, []);
|
|
234
|
+
const handleCancelCreateCoupon = useCallback(() => {}, []);
|
|
235
|
+
const handleClaimPoints = useCallback((claimedPointsData) => {...}, [selectedOfferDetails, onChange]);
|
|
236
|
+
const handleCloseCouponsSlidebox = useCallback(() => {}, []);
|
|
237
|
+
const handleClosePointsSlidebox = useCallback(() => {}, []);
|
|
238
|
+
*/
|
|
239
|
+
|
|
240
|
+
const renderChannelDropdownOverlay = () => {
|
|
241
|
+
if (!availableChannels || availableChannels.length === 0) {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Filter out FTP and ensure only active channels are shown
|
|
246
|
+
const filteredChannels = availableChannels.filter((channel) => {
|
|
247
|
+
const isFTP = channel.value === 'FTP' || channel.value === 'ftp';
|
|
248
|
+
const isHidden = channelsToHide.includes(channel.value);
|
|
249
|
+
const isDisabled = channelsToDisable.includes(channel.value);
|
|
250
|
+
return !isFTP && !isHidden && channel.isActive;
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
if (filteredChannels.length === 0) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<CapMenu onClick={handleChannelMenuClick} className="channel-dropdown-content">
|
|
259
|
+
{filteredChannels.map((channel) => {
|
|
260
|
+
const isDisabled = channelsToDisable.includes(channel.value);
|
|
261
|
+
const iconType = channel.iconType;
|
|
262
|
+
const displayLabel = channel.label || channel.value;
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<CapMenu.Item key={channel.value} disabled={isDisabled}>
|
|
266
|
+
<CapRow align="middle" type="flex" className="channel-row">
|
|
267
|
+
<CapIcon type={iconType} className="channel-icon" size="s" />
|
|
268
|
+
<CapHeading type="h5">{displayLabel}</CapHeading>
|
|
269
|
+
</CapRow>
|
|
270
|
+
</CapMenu.Item>
|
|
271
|
+
);
|
|
272
|
+
})}
|
|
273
|
+
</CapMenu>
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const renderIncentiveDropdownOverlay = (onMenuClick) => {
|
|
278
|
+
if (availableIncentiveTypes?.length === 0) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const filteredIncentiveTypes = availableIncentiveTypes?.filter(
|
|
282
|
+
(incentive) => incentive?.value !== 'coupons' && incentive?.value !== 'points',
|
|
283
|
+
);
|
|
284
|
+
if (filteredIncentiveTypes?.length === 0) return null;
|
|
285
|
+
|
|
286
|
+
return (
|
|
287
|
+
<CapMenu onClick={onMenuClick} className="incentive-dropdown-content">
|
|
288
|
+
{filteredIncentiveTypes.map((incentive) => {
|
|
289
|
+
const isDisabled = incentive?.disabled || false;
|
|
290
|
+
return (
|
|
291
|
+
<CapMenu.Item key={incentive.value} disabled={isDisabled}>
|
|
292
|
+
{incentive.label}
|
|
293
|
+
</CapMenu.Item>
|
|
294
|
+
);
|
|
295
|
+
})}
|
|
296
|
+
</CapMenu>
|
|
297
|
+
);
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/** Configured state: CapCustomCard per content item - matches campaigns-ui structure */
|
|
301
|
+
const renderConfiguredCard = (item) => {
|
|
302
|
+
const channelConfig = CHANNELS.find((c) => c.value === item.channel || c.channelProp === item.channel?.toLowerCase());
|
|
303
|
+
const iconType = channelConfig?.iconType || 'sms';
|
|
304
|
+
const channelLabel = channelConfig?.label || item.channel;
|
|
305
|
+
const senderDetailsText = getSenderDetailsDisplay(item);
|
|
306
|
+
const hasIncentiveOptions = incentivesData && availableIncentiveTypes?.some((i) => i.value !== 'coupons' && i.value !== 'points');
|
|
307
|
+
|
|
308
|
+
const card = {
|
|
309
|
+
title: (
|
|
310
|
+
<CapRow align="middle" type="flex" className="card-title-row">
|
|
311
|
+
<CapIcon type={iconType} className="channel-icon" size="s" />
|
|
312
|
+
<CapHeader size="label1" titleClass="truncate-text" title={<span title={channelLabel}>{channelLabel}</span>} />
|
|
313
|
+
</CapRow>
|
|
314
|
+
),
|
|
315
|
+
content: (
|
|
316
|
+
<CapRow className="content-card-body">
|
|
317
|
+
<CapLabel type="label2" className="content-preview-text">
|
|
318
|
+
{getContentPreview(item)}
|
|
319
|
+
</CapLabel>
|
|
320
|
+
{senderDetailsText && (
|
|
321
|
+
<CapRow type="flex" align="middle" justify="space-between" className="sender-details-row">
|
|
322
|
+
<CapLabel type="label2">{senderDetailsText}</CapLabel>
|
|
323
|
+
<CapIcon type="arrow-right" size="s" className="sender-details-arrow" />
|
|
324
|
+
</CapRow>
|
|
325
|
+
)}
|
|
326
|
+
</CapRow>
|
|
327
|
+
),
|
|
328
|
+
cardType: getCardType(item.channel),
|
|
329
|
+
extra: (
|
|
330
|
+
<CapDropdown
|
|
331
|
+
overlay={
|
|
332
|
+
<CapMenu>
|
|
333
|
+
<CapMenu.Item
|
|
334
|
+
id="edit-menu-item"
|
|
335
|
+
className="ant-dropdown-menu-item"
|
|
336
|
+
onClick={() => handleEditContent(item.contentId)}
|
|
337
|
+
>
|
|
338
|
+
{formatMessage(messages.edit)}
|
|
339
|
+
</CapMenu.Item>
|
|
340
|
+
<CapMenu.Item
|
|
341
|
+
id="preview-menu-item"
|
|
342
|
+
className="ant-dropdown-menu-item"
|
|
343
|
+
onClick={() => handlePreviewContent(item)}
|
|
344
|
+
>
|
|
345
|
+
{formatMessage(messages.previewAndTest)}
|
|
346
|
+
</CapMenu.Item>
|
|
347
|
+
<CapMenu.Item
|
|
348
|
+
id="delete-menu-item"
|
|
349
|
+
onClick={() => handleDeleteContent(item.contentId)}
|
|
350
|
+
>
|
|
351
|
+
{formatMessage(messages.remove)}
|
|
352
|
+
</CapMenu.Item>
|
|
353
|
+
</CapMenu>
|
|
354
|
+
}
|
|
355
|
+
>
|
|
356
|
+
<CapIcon type="more" aria-label="Show more content options icon" />
|
|
357
|
+
</CapDropdown>
|
|
358
|
+
),
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// Add incentive in actions (campaigns-ui style: cap-card-action-container)
|
|
362
|
+
if (hasIncentiveOptions) {
|
|
363
|
+
card.actions = [
|
|
364
|
+
<CapRow className="cap-card-action-container add-incentive-action-row" key="add-incentive">
|
|
365
|
+
<CapDropdown
|
|
366
|
+
overlay={renderIncentiveDropdownOverlay((menuClickEvent) => handleIncentiveMenuClick(menuClickEvent, item.contentId))}
|
|
367
|
+
onVisibleChange={(visible) => handleIncentivesVisibleChange(visible, item.contentId)}
|
|
368
|
+
visible={!!showIncentivesMenuMap[item.contentId]}
|
|
369
|
+
>
|
|
370
|
+
<CapButton isAddBtn type="flat" className="add-incentives-button add-incentive-link">
|
|
371
|
+
{formatMessage(messages.addIncentive)}
|
|
372
|
+
</CapButton>
|
|
373
|
+
</CapDropdown>
|
|
374
|
+
<CapLabel type="label1" className="optional-incentive-label">
|
|
375
|
+
{formatMessage(messages.optional)}
|
|
376
|
+
</CapLabel>
|
|
377
|
+
</CapRow>,
|
|
378
|
+
];
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return card;
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
return (
|
|
385
|
+
<>
|
|
386
|
+
<CapRow className="channel-selection-step">
|
|
387
|
+
<CapHeading type="h3" className="margin-b-8">
|
|
388
|
+
{formatMessage(messages.contentTemplate)}
|
|
389
|
+
</CapHeading>
|
|
390
|
+
|
|
391
|
+
<CapRow
|
|
392
|
+
className={`content-template-container ${contentItems?.length > 0 ? 'content-template-container--has-content' : ''}`}
|
|
393
|
+
>
|
|
394
|
+
{contentItems?.length === 0 ? (
|
|
395
|
+
<CapRow className="content-template-section">
|
|
396
|
+
<CapHeading type="h5">
|
|
397
|
+
{formatMessage(messages.addMessageContentAndIncentive)}
|
|
398
|
+
</CapHeading>
|
|
399
|
+
|
|
400
|
+
<CapDropdown
|
|
401
|
+
overlay={renderChannelDropdownOverlay()}
|
|
402
|
+
onVisibleChange={handleChannelsVisibleChange}
|
|
403
|
+
visible={showChannelsMenu}
|
|
404
|
+
>
|
|
405
|
+
<CapButton
|
|
406
|
+
isAddBtn
|
|
407
|
+
type="primary"
|
|
408
|
+
className="add-content-template-button"
|
|
409
|
+
>
|
|
410
|
+
{formatMessage(messages.addContentTemplate)}
|
|
411
|
+
</CapButton>
|
|
412
|
+
</CapDropdown>
|
|
413
|
+
</CapRow>
|
|
414
|
+
) : (
|
|
415
|
+
<CapCustomCard.CapCustomCardList
|
|
416
|
+
cardList={contentItems.map(renderConfiguredCard)}
|
|
417
|
+
className="content-items-list"
|
|
418
|
+
/>
|
|
419
|
+
)}
|
|
420
|
+
|
|
421
|
+
{contentItems?.length === 0 &&
|
|
422
|
+
incentivesData &&
|
|
423
|
+
availableIncentiveTypes?.some((i) => i.value !== 'coupons' && i.value !== 'points') && (
|
|
424
|
+
<CapRow type="flex" justify="center" align="middle" className="incentive-section">
|
|
425
|
+
<CapDropdown
|
|
426
|
+
overlay={renderIncentiveDropdownOverlay((menuClickEvent) => handleIncentiveMenuClick(menuClickEvent, 'standalone'))}
|
|
427
|
+
onVisibleChange={(visible) => handleIncentivesVisibleChange(visible, 'standalone')}
|
|
428
|
+
visible={!!showIncentivesMenuMap.standalone}
|
|
429
|
+
>
|
|
430
|
+
<CapButton isAddBtn type="flat" className="add-incentive-button">
|
|
431
|
+
{formatMessage(messages.addIncentive)}
|
|
432
|
+
</CapButton>
|
|
433
|
+
</CapDropdown>
|
|
434
|
+
<CapHeading type="label4" className="optional-text">
|
|
435
|
+
{formatMessage(messages.optional)}
|
|
436
|
+
</CapHeading>
|
|
437
|
+
</CapRow>
|
|
438
|
+
)}
|
|
439
|
+
|
|
440
|
+
{contentItems?.length > 0 && (
|
|
441
|
+
<DeliverySettingsSection
|
|
442
|
+
contentItems={contentItems}
|
|
443
|
+
deliverySettingsData={deliverySettingsData}
|
|
444
|
+
deliverySetting={value?.deliverySetting}
|
|
445
|
+
onDeliverySettingChange={(deliverySetting) => onChange({ deliverySetting })}
|
|
446
|
+
intl={intl}
|
|
447
|
+
/>
|
|
448
|
+
)}
|
|
449
|
+
</CapRow>
|
|
450
|
+
|
|
451
|
+
{error && (
|
|
452
|
+
<CapRow className="validation-error">
|
|
453
|
+
{error}
|
|
454
|
+
</CapRow>
|
|
455
|
+
)}
|
|
456
|
+
</CapRow>
|
|
457
|
+
|
|
458
|
+
{showCreativesContainer && selectedChannelConfig && (
|
|
459
|
+
<CreativesContainer
|
|
460
|
+
key={`creatives-${selectedChannel}-${editingContentId || 'create'}`}
|
|
461
|
+
channel={selectedChannelConfig.channelProp}
|
|
462
|
+
creativesMode={editingContentId ? 'edit' : creativesMode}
|
|
463
|
+
getCreativesData={handleCreativesData}
|
|
464
|
+
handleCloseCreatives={handleCloseCreatives}
|
|
465
|
+
isFullMode={false}
|
|
466
|
+
messageDetails={{ type: 'default' }}
|
|
467
|
+
templateData={editingContentId ? contentItems.find((c) => c.contentId === editingContentId)?.templateData : null}
|
|
468
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
469
|
+
channelsToHide={[
|
|
470
|
+
...(channelsToHide || []),
|
|
471
|
+
...CREATIVES_PANE_KEYS.filter((key) => key !== selectedChannelConfig.paneKey),
|
|
472
|
+
]}
|
|
473
|
+
channelsToDisable={channelsToDisable}
|
|
474
|
+
enableNewChannels={ENABLE_NEW_CHANNELS}
|
|
475
|
+
/>
|
|
476
|
+
)}
|
|
477
|
+
|
|
478
|
+
{/* cap-coupons flows disabled - Coupons and Points slideboxes commented out */}
|
|
479
|
+
</>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
ChannelSelectionStep.propTypes = {
|
|
484
|
+
value: PropTypes.object, // stepData (channel, contentItems, etc.)
|
|
485
|
+
channels: PropTypes.arrayOf(PropTypes.shape({
|
|
486
|
+
value: PropTypes.string.isRequired,
|
|
487
|
+
label: PropTypes.string,
|
|
488
|
+
isActive: PropTypes.bool,
|
|
489
|
+
iconType: PropTypes.string,
|
|
490
|
+
channelProp: PropTypes.string,
|
|
491
|
+
paneKey: PropTypes.string,
|
|
492
|
+
})),
|
|
493
|
+
communicationStrategy: PropTypes.string,
|
|
494
|
+
onChange: PropTypes.func.isRequired,
|
|
495
|
+
channelsToHide: PropTypes.array,
|
|
496
|
+
channelsToDisable: PropTypes.array,
|
|
497
|
+
creativesMode: PropTypes.oneOf(['create', 'edit', 'preview']),
|
|
498
|
+
selectedOfferDetails: PropTypes.array,
|
|
499
|
+
incentivesData: PropTypes.shape({
|
|
500
|
+
types: PropTypes.array,
|
|
501
|
+
required: PropTypes.bool,
|
|
502
|
+
}),
|
|
503
|
+
deliverySettingsData: PropTypes.object,
|
|
504
|
+
error: PropTypes.string,
|
|
505
|
+
intl: PropTypes.object.isRequired,
|
|
506
|
+
capData: PropTypes.object, // Cap data from Redux (user/org info)
|
|
507
|
+
};
|
|
508
|
+
|
|
509
|
+
ChannelSelectionStep.defaultProps = {
|
|
510
|
+
value: null,
|
|
511
|
+
channels: [],
|
|
512
|
+
communicationStrategy: null,
|
|
513
|
+
error: null,
|
|
514
|
+
channelsToHide: [],
|
|
515
|
+
channelsToDisable: [],
|
|
516
|
+
creativesMode: 'create',
|
|
517
|
+
selectedOfferDetails: [],
|
|
518
|
+
incentivesData: null,
|
|
519
|
+
capData: {},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
export default injectIntl(ChannelSelectionStep);
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
@import '~@capillarytech/cap-ui-library/styles/_variables.scss';
|
|
2
|
+
|
|
3
|
+
.channel-selection-step {
|
|
4
|
+
.content-template-container {
|
|
5
|
+
border-radius: 0.25rem; // 4px
|
|
6
|
+
border: 1px solid $CAP_G07; // #DFE2E7
|
|
7
|
+
background: $CAP_WHITE; // #FFF
|
|
8
|
+
display: flex;
|
|
9
|
+
width: 17.0625rem; // 273px
|
|
10
|
+
height: 20rem; // 320px
|
|
11
|
+
padding-top: 1rem; // 16px
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
justify-content: space-between;
|
|
14
|
+
align-items: center;
|
|
15
|
+
|
|
16
|
+
// Configured state: remove container border (CapCustomCard has its own), grow to fit
|
|
17
|
+
&--has-content {
|
|
18
|
+
border: none;
|
|
19
|
+
background: transparent;
|
|
20
|
+
max-width: 100%;
|
|
21
|
+
min-width: 0;
|
|
22
|
+
height: auto;
|
|
23
|
+
min-height: 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Configured state only - these elements exist ONLY when content is configured
|
|
28
|
+
.content-items-list {
|
|
29
|
+
width: 100%;
|
|
30
|
+
flex: 1;
|
|
31
|
+
|
|
32
|
+
.cap-custom-card-list-row {
|
|
33
|
+
display: flex;
|
|
34
|
+
flex-direction: column;
|
|
35
|
+
gap: $CAP_SPACE_12;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.cap-custom-card-list-col {
|
|
39
|
+
width: 100%;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.ant-card .ant-card-actions {
|
|
43
|
+
& > li {
|
|
44
|
+
text-align: unset;
|
|
45
|
+
color: unset;
|
|
46
|
+
}
|
|
47
|
+
.add-incentives-button {
|
|
48
|
+
margin-left: -0.5rem;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.cap-card-action-container {
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
padding: 0.625rem 0;
|
|
57
|
+
|
|
58
|
+
&.add-incentive-action-row {
|
|
59
|
+
justify-content: flex-start;
|
|
60
|
+
gap: $CAP_SPACE_04;
|
|
61
|
+
|
|
62
|
+
.optional-incentive-label {
|
|
63
|
+
padding-left: 0;
|
|
64
|
+
color: $CAP_G05;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.add-incentive-link {
|
|
69
|
+
color: $CAP_BLUE01; // Blue link per Figma (#2466eb)
|
|
70
|
+
padding: 0;
|
|
71
|
+
margin-left: -0.5rem;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.content-card-body {
|
|
76
|
+
flex-direction: column;
|
|
77
|
+
gap: $CAP_SPACE_12;
|
|
78
|
+
align-items: flex-start;
|
|
79
|
+
min-width: 0;
|
|
80
|
+
overflow: hidden;
|
|
81
|
+
|
|
82
|
+
.content-preview-text {
|
|
83
|
+
width: 100%;
|
|
84
|
+
min-width: 0;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
// display: -webkit-box;
|
|
87
|
+
// -webkit-line-clamp: 3;
|
|
88
|
+
// -webkit-box-orient: vertical;
|
|
89
|
+
word-break: break-word;
|
|
90
|
+
overflow-wrap: break-word;
|
|
91
|
+
word-wrap: break-word;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.sender-details-row {
|
|
95
|
+
width: 100%;
|
|
96
|
+
margin-top: $CAP_SPACE_08;
|
|
97
|
+
padding-top: $CAP_SPACE_08;
|
|
98
|
+
border-top: 1px solid $CAP_G07;
|
|
99
|
+
|
|
100
|
+
.sender-details-arrow {
|
|
101
|
+
margin-left: $CAP_SPACE_08;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.card-title-row {
|
|
107
|
+
gap: $CAP_SPACE_08;
|
|
108
|
+
|
|
109
|
+
.channel-icon {
|
|
110
|
+
color: $CAP_G04;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Empty state styles - unchanged from original
|
|
115
|
+
.content-template-section {
|
|
116
|
+
display: flex;
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
height: 15.75rem;
|
|
121
|
+
width: 100%;
|
|
122
|
+
padding: 0 1rem;
|
|
123
|
+
border-bottom: 1px solid $CAP_G07;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.add-content-template-button {
|
|
127
|
+
margin-top: $CAP_SPACE_16;
|
|
128
|
+
padding: $CAP_SPACE_12;
|
|
129
|
+
padding-right: $CAP_SPACE_16;
|
|
130
|
+
display: flex;
|
|
131
|
+
align-items: center;
|
|
132
|
+
justify-content: center;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.incentive-section {
|
|
136
|
+
width: 100%;
|
|
137
|
+
padding: $CAP_SPACE_08 0;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.add-incentive-button {
|
|
141
|
+
/* Styles handled by CapButton with isAddBtn prop */
|
|
142
|
+
display: inline-flex;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.incentive-dropdown-content {
|
|
146
|
+
min-width: 12rem; // 192px
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.channel-dropdown-content {
|
|
151
|
+
.ant-dropdown-menu-item {
|
|
152
|
+
.channel-row {
|
|
153
|
+
gap: $CAP_SPACE_08;
|
|
154
|
+
.cap-icon-v2.channel-icon {
|
|
155
|
+
color: $CAP_G04;
|
|
156
|
+
}
|
|
157
|
+
.cap-icon-v2-zalo {
|
|
158
|
+
/* Override ALL hardcoded fills inside CapIcon SVGs */
|
|
159
|
+
color: $CAP_G04;
|
|
160
|
+
svg path,
|
|
161
|
+
svg use,
|
|
162
|
+
svg rect,
|
|
163
|
+
svg circle,
|
|
164
|
+
svg g {
|
|
165
|
+
fill: currentColor !important;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|