@capillarytech/creatives-library 8.0.331 → 8.0.332
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 +2 -2
- package/services/api.js +17 -0
- package/services/tests/api.test.js +72 -0
- package/utils/commonUtils.js +10 -0
- package/utils/tests/commonUtil.test.js +169 -0
- package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
- package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +94 -0
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +78 -49
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
- package/v2Components/CommonTestAndPreview/actions.js +10 -0
- package/v2Components/CommonTestAndPreview/constants.js +17 -1
- package/v2Components/CommonTestAndPreview/index.js +356 -22
- package/v2Components/CommonTestAndPreview/messages.js +106 -0
- package/v2Components/CommonTestAndPreview/reducer.js +12 -0
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
- package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
- package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +23 -5
- package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
- package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
- package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +39 -19
- package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
- package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
- package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
- package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1408 -1276
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +321 -288
- package/v2Containers/TagList/index.js +11 -15
- package/v2Containers/WebPush/Create/index.js +1 -1
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5246 -4872
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import PropTypes from 'prop-types';
|
|
10
10
|
import React, {
|
|
11
|
-
useState, useEffect, useMemo, useRef,
|
|
11
|
+
useState, useEffect, useMemo, useRef, useCallback,
|
|
12
12
|
} from 'react';
|
|
13
13
|
import { FormattedMessage } from 'react-intl';
|
|
14
14
|
import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
|
|
@@ -17,6 +17,9 @@ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
|
17
17
|
import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
|
|
18
18
|
import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
|
|
19
19
|
import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
|
|
20
|
+
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
21
|
+
import CustomerCreationModal from './CustomerCreationModal';
|
|
22
|
+
import { createTestCustomer } from '../../services/api';
|
|
20
23
|
|
|
21
24
|
// Import messages and styles
|
|
22
25
|
import messages from './messages';
|
|
@@ -28,9 +31,17 @@ import CustomValuesEditor from './CustomValuesEditor';
|
|
|
28
31
|
import SendTestMessage from './SendTestMessage';
|
|
29
32
|
import PreviewSection from './PreviewSection';
|
|
30
33
|
|
|
34
|
+
// Import constants
|
|
35
|
+
import AddTestCustomerButton from './AddTestCustomer';
|
|
36
|
+
import ExistingCustomerModal from './ExistingCustomerModal';
|
|
31
37
|
// Import constants
|
|
32
38
|
import {
|
|
33
39
|
CHANNELS,
|
|
40
|
+
CUSTOMER_MODAL_NEW,
|
|
41
|
+
CUSTOMER_MODAL_EXISTING,
|
|
42
|
+
IDENTIFIER_TYPE_EMAIL,
|
|
43
|
+
IDENTIFIER_TYPE_MOBILE,
|
|
44
|
+
IDENTIFIER_TYPE_PHONE,
|
|
34
45
|
TEST,
|
|
35
46
|
DESKTOP,
|
|
36
47
|
ANDROID,
|
|
@@ -70,10 +81,47 @@ import {
|
|
|
70
81
|
IMAGE,
|
|
71
82
|
VIDEO,
|
|
72
83
|
URL,
|
|
84
|
+
CHANNELS_USING_ANDROID_PREVIEW_DEVICE
|
|
73
85
|
} from './constants';
|
|
74
86
|
|
|
75
87
|
// Import utilities
|
|
76
88
|
import { getCdnUrl } from '../../utils/cdnTransformation';
|
|
89
|
+
import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
|
|
90
|
+
import { getMembersLookup } from '../../services/api';
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Drop empty GSM rows. RCS/DLT responses often set gsm_sender_id equal to domainName — keep those rows
|
|
94
|
+
* for RCS defaults (ModifyDeliverySettings uses the same rule for the sender dropdown).
|
|
95
|
+
*/
|
|
96
|
+
const filterUsableGsmSendersForDomain = (domain, gsmSenders, { skipDomainNameEchoFilter = false } = {}) => {
|
|
97
|
+
const normalizedDomainName =
|
|
98
|
+
domain?.domainName != null ? String(domain.domainName).trim().toLowerCase() : '';
|
|
99
|
+
return (gsmSenders || []).filter((gsmSenderRow) => {
|
|
100
|
+
const rawValue = gsmSenderRow?.value;
|
|
101
|
+
if (rawValue == null) return false;
|
|
102
|
+
const trimmedSenderValue = String(rawValue).trim();
|
|
103
|
+
if (!trimmedSenderValue) return false;
|
|
104
|
+
const senderMatchesDomainLabel =
|
|
105
|
+
normalizedDomainName && trimmedSenderValue.toLowerCase() === normalizedDomainName;
|
|
106
|
+
if (!skipDomainNameEchoFilter && senderMatchesDomainLabel) return false;
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
|
|
114
|
+
*/
|
|
115
|
+
const normalizeTestEntityId = (id) => {
|
|
116
|
+
if (id === undefined || id === null) return id;
|
|
117
|
+
return String(id);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const testEntityIdsEqual = (a, b) => {
|
|
121
|
+
if (a == null && b == null) return true;
|
|
122
|
+
if (a == null || b == null) return false;
|
|
123
|
+
return String(a) === String(b);
|
|
124
|
+
};
|
|
77
125
|
|
|
78
126
|
/**
|
|
79
127
|
* Preview Component Factory - REMOVED IN PHASE 5
|
|
@@ -130,9 +178,13 @@ const CommonTestAndPreview = (props) => {
|
|
|
130
178
|
const [customValues, setCustomValues] = useState({});
|
|
131
179
|
const [showJSON, setShowJSON] = useState(false);
|
|
132
180
|
const [tagsExtracted, setTagsExtracted] = useState(false);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const
|
|
181
|
+
|
|
182
|
+
const initialDevice = CHANNELS_USING_ANDROID_PREVIEW_DEVICE.includes(channel) ? ANDROID : DESKTOP;
|
|
183
|
+
const [searchValue, setSearchValue] = useState("");
|
|
184
|
+
const [customerModal, setCustomerModal] = useState([false, ""]);
|
|
185
|
+
const [isCustomerDataLoading, setIsCustomerDataLoading] = useState(false);
|
|
186
|
+
const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
|
|
187
|
+
|
|
136
188
|
const [previewDevice, setPreviewDevice] = useState(initialDevice);
|
|
137
189
|
// Track if a preview call has been made (to know when to use previewDataHtml vs raw content)
|
|
138
190
|
const [hasPreviewCallBeenMade, setHasPreviewCallBeenMade] = useState(false);
|
|
@@ -189,6 +241,14 @@ const CommonTestAndPreview = (props) => {
|
|
|
189
241
|
}
|
|
190
242
|
}, [show, channel, orgUnitId, actions]);
|
|
191
243
|
|
|
244
|
+
useEffect(() => {
|
|
245
|
+
if (!show) {
|
|
246
|
+
setCustomerModal([false, '']);
|
|
247
|
+
setSearchValue('');
|
|
248
|
+
setCustomerData({ name: '', email: '', mobile: '', customerId: '' });
|
|
249
|
+
}
|
|
250
|
+
}, [show]);
|
|
251
|
+
|
|
192
252
|
const findDefault = (arr) => (arr && arr.find((x) => x.default)) || (arr && arr[0]) || {};
|
|
193
253
|
|
|
194
254
|
// Auto-set default delivery setting when sender details load (campaigns-style: first domain + default/first sender)
|
|
@@ -349,19 +409,26 @@ const CommonTestAndPreview = (props) => {
|
|
|
349
409
|
}, [channel, formData, currentTab, beeContent, content, beeInstance]);
|
|
350
410
|
|
|
351
411
|
// Build test entities tree data
|
|
412
|
+
// Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
|
|
352
413
|
const testEntitiesTreeData = useMemo(() => {
|
|
353
414
|
const groupsNode = {
|
|
354
415
|
title: 'Groups',
|
|
355
416
|
value: 'groups-node',
|
|
356
417
|
selectable: false,
|
|
357
|
-
children: testGroups?.map((group) => ({
|
|
418
|
+
children: testGroups?.map((group) => ({
|
|
419
|
+
title: group?.groupName,
|
|
420
|
+
value: 'group:' + normalizeTestEntityId(group?.groupId),
|
|
421
|
+
})),
|
|
358
422
|
};
|
|
359
423
|
|
|
360
424
|
const customersNode = {
|
|
361
425
|
title: 'Individuals',
|
|
362
426
|
value: 'customers-node',
|
|
363
427
|
selectable: false,
|
|
364
|
-
children: testCustomers?.map((customer) => ({
|
|
428
|
+
children: testCustomers?.map((customer) => ({
|
|
429
|
+
title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
|
|
430
|
+
value: 'customer:' + normalizeTestEntityId(customer?.userId ?? customer?.customerId),
|
|
431
|
+
})) || [],
|
|
365
432
|
};
|
|
366
433
|
|
|
367
434
|
return [groupsNode, customersNode];
|
|
@@ -388,6 +455,94 @@ const CommonTestAndPreview = (props) => {
|
|
|
388
455
|
return resolvedText;
|
|
389
456
|
};
|
|
390
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Common handler for saving test customers (both new and existing)
|
|
460
|
+
*/
|
|
461
|
+
const handleSaveTestCustomer = async (validationErrors = {}, setIsLoading = () => {}) => {
|
|
462
|
+
// Check for validation errors before saving (for new customers)
|
|
463
|
+
if (customerModal[1] === CUSTOMER_MODAL_NEW && (validationErrors.email || validationErrors.mobile)) {
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
setIsLoading(true);
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
let payload;
|
|
471
|
+
|
|
472
|
+
if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
|
|
473
|
+
// For existing customers, use customerId
|
|
474
|
+
payload = {
|
|
475
|
+
campaignUserId: customerData.customerId
|
|
476
|
+
};
|
|
477
|
+
} else {
|
|
478
|
+
// For new customers, use customer object
|
|
479
|
+
payload = {
|
|
480
|
+
customer: {
|
|
481
|
+
firstName: customerData.name || "",
|
|
482
|
+
mobile: customerData.mobile || "",
|
|
483
|
+
email: customerData.email || ""
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const response = await createTestCustomer(payload);
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
// Handle success: add to test customers list and selection (existing and new)
|
|
492
|
+
if (response && response.success) {
|
|
493
|
+
CapNotification.success({
|
|
494
|
+
message: formatMessage(messages.newTestCustomerAddedSuccess),
|
|
495
|
+
});
|
|
496
|
+
// API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
|
|
497
|
+
const res = response?.response || response;
|
|
498
|
+
const addedId = customerModal[1] === CUSTOMER_MODAL_EXISTING
|
|
499
|
+
? customerData?.customerId || customerData?.campaignUserId
|
|
500
|
+
: res?.customerId || res?.campaignUserId;
|
|
501
|
+
if (addedId) {
|
|
502
|
+
const normalizedAddedId = normalizeTestEntityId(addedId);
|
|
503
|
+
actions.addTestCustomer({
|
|
504
|
+
userId: normalizedAddedId,
|
|
505
|
+
customerId: normalizedAddedId,
|
|
506
|
+
name: customerData?.name?.trim() || '',
|
|
507
|
+
email: customerData?.email || '',
|
|
508
|
+
mobile: customerData?.mobile || '',
|
|
509
|
+
});
|
|
510
|
+
const prefixedAddedId = 'customer:' + normalizedAddedId;
|
|
511
|
+
setSelectedTestEntities((prev) => (
|
|
512
|
+
prev.some((id) => id === prefixedAddedId)
|
|
513
|
+
? prev
|
|
514
|
+
: [...prev, prefixedAddedId]
|
|
515
|
+
));
|
|
516
|
+
}
|
|
517
|
+
handleCloseCustomerModal();
|
|
518
|
+
} else {
|
|
519
|
+
// Show error notification for unsuccessful response
|
|
520
|
+
CapNotification.error({
|
|
521
|
+
message: formatMessage(messages.errorTitle),
|
|
522
|
+
description: response?.message || formatMessage(messages.failedToAddTestCustomer),
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
} catch (error) {
|
|
526
|
+
CapNotification.error({
|
|
527
|
+
message: formatMessage(messages.errorTitle),
|
|
528
|
+
description: error?.message || formatMessage(messages.errorAddingTestCustomer),
|
|
529
|
+
});
|
|
530
|
+
} finally {
|
|
531
|
+
setIsLoading(false);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const handleCloseCustomerModal = () => {
|
|
536
|
+
setCustomerModal([false, ""]);
|
|
537
|
+
setSearchValue('');
|
|
538
|
+
setCustomerData({
|
|
539
|
+
name: '',
|
|
540
|
+
email: '',
|
|
541
|
+
mobile: '',
|
|
542
|
+
customerId: '',
|
|
543
|
+
});
|
|
544
|
+
};
|
|
545
|
+
|
|
391
546
|
/**
|
|
392
547
|
* Prepare payload for preview API based on channel
|
|
393
548
|
*/
|
|
@@ -2332,7 +2487,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
2332
2487
|
* Handle slidebox close
|
|
2333
2488
|
*/
|
|
2334
2489
|
const handleClose = () => {
|
|
2335
|
-
// Reset state when closing
|
|
2490
|
+
// Reset state when closing (includes add-customer modal + tree search state)
|
|
2491
|
+
handleCloseCustomerModal();
|
|
2336
2492
|
setSelectedCustomer(null);
|
|
2337
2493
|
setRequiredTags([]);
|
|
2338
2494
|
setOptionalTags([]);
|
|
@@ -2509,20 +2665,142 @@ const CommonTestAndPreview = (props) => {
|
|
|
2509
2665
|
* Handle test entities change
|
|
2510
2666
|
*/
|
|
2511
2667
|
const handleTestEntitiesChange = (value) => {
|
|
2512
|
-
|
|
2668
|
+
if (!Array.isArray(value)) {
|
|
2669
|
+
if (value == null || value === '') {
|
|
2670
|
+
setSelectedTestEntities([]);
|
|
2671
|
+
return;
|
|
2672
|
+
}
|
|
2673
|
+
setSelectedTestEntities([normalizeTestEntityId(value)]);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
setSelectedTestEntities(value.map((v) => normalizeTestEntityId(v)));
|
|
2513
2677
|
};
|
|
2514
2678
|
|
|
2679
|
+
/**
|
|
2680
|
+
* Map API customerDetails item to our customerData shape
|
|
2681
|
+
*/
|
|
2682
|
+
const mapCustomerDetailsToCustomerData = (detail, identifierValue) => {
|
|
2683
|
+
const firstName = detail.firstName || '';
|
|
2684
|
+
const lastName = detail.lastName || '';
|
|
2685
|
+
const name = [firstName, lastName].filter(Boolean).join(' ').trim() || '';
|
|
2686
|
+
const getIdentifierValue = (type) => {
|
|
2687
|
+
const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
|
|
2688
|
+
if (fromIdentifiers) return fromIdentifiers;
|
|
2689
|
+
const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
|
|
2690
|
+
return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
|
|
2691
|
+
};
|
|
2692
|
+
return {
|
|
2693
|
+
name,
|
|
2694
|
+
email: channel === CHANNELS.EMAIL ? (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || ''),
|
|
2695
|
+
mobile: channel === CHANNELS.SMS ? (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || ''),
|
|
2696
|
+
customerId: detail.userId != null ? String(detail.userId) : '',
|
|
2697
|
+
};
|
|
2698
|
+
};
|
|
2699
|
+
|
|
2700
|
+
const handleAddTestCustomer = async () => {
|
|
2701
|
+
const identifierType = channel === CHANNELS.EMAIL ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
|
|
2702
|
+
const searchValueToCheck = channel === CHANNELS.SMS
|
|
2703
|
+
? formatPhoneNumber((searchValue || '').trim())
|
|
2704
|
+
: (searchValue || '').trim();
|
|
2705
|
+
|
|
2706
|
+
// Check if this customer is already in the test customers list
|
|
2707
|
+
const existingTestCustomer = testCustomers?.find(customer => {
|
|
2708
|
+
if (channel === CHANNELS.EMAIL) {
|
|
2709
|
+
return customer.email === searchValueToCheck;
|
|
2710
|
+
} else if (channel === CHANNELS.SMS) {
|
|
2711
|
+
return customer.mobile === searchValueToCheck;
|
|
2712
|
+
}
|
|
2713
|
+
return false;
|
|
2714
|
+
});
|
|
2715
|
+
|
|
2716
|
+
if (existingTestCustomer) {
|
|
2717
|
+
const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
|
|
2718
|
+
if (entityId != null) {
|
|
2719
|
+
const id = 'customer:' + normalizeTestEntityId(entityId);
|
|
2720
|
+
setSelectedTestEntities((prev) => (
|
|
2721
|
+
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
2722
|
+
));
|
|
2723
|
+
}
|
|
2724
|
+
setSearchValue('');
|
|
2725
|
+
CapNotification.success({
|
|
2726
|
+
message: formatMessage(messages.customerAlreadyInTestList),
|
|
2727
|
+
});
|
|
2728
|
+
return;
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2731
|
+
setIsCustomerDataLoading(true);
|
|
2732
|
+
|
|
2733
|
+
try {
|
|
2734
|
+
const response = await getMembersLookup(identifierType, searchValueToCheck);
|
|
2735
|
+
const success = response?.success && !response?.status?.isError;
|
|
2736
|
+
const res = response?.response || {};
|
|
2737
|
+
const exists = res.exists || false;
|
|
2738
|
+
const details = res.customerDetails || [];
|
|
2739
|
+
|
|
2740
|
+
if (!success) {
|
|
2741
|
+
const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
|
|
2742
|
+
CapNotification.error({
|
|
2743
|
+
message: formatMessage(messages.memberLookupError),
|
|
2744
|
+
description: errorMessage,
|
|
2745
|
+
});
|
|
2746
|
+
return;
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
if (exists && details.length > 0) {
|
|
2750
|
+
const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
|
|
2751
|
+
const customerIdFromLookup = mapped.customerId;
|
|
2752
|
+
const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
|
|
2753
|
+
(c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
|
|
2754
|
+
);
|
|
2755
|
+
if (alreadyInTestListByCustomerId) {
|
|
2756
|
+
const id = 'customer:' + normalizeTestEntityId(customerIdFromLookup);
|
|
2757
|
+
setSelectedTestEntities((prev) => (
|
|
2758
|
+
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
2759
|
+
));
|
|
2760
|
+
setSearchValue('');
|
|
2761
|
+
CapNotification.success({
|
|
2762
|
+
message: formatMessage(messages.customerAlreadyInTestList),
|
|
2763
|
+
});
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
setCustomerData(mapped);
|
|
2767
|
+
setCustomerModal([true, CUSTOMER_MODAL_EXISTING]);
|
|
2768
|
+
} else {
|
|
2769
|
+
setCustomerData({
|
|
2770
|
+
name: '',
|
|
2771
|
+
email: channel === CHANNELS.EMAIL ? searchValueToCheck : '',
|
|
2772
|
+
mobile: channel === CHANNELS.SMS ? searchValueToCheck : '',
|
|
2773
|
+
customerId: '',
|
|
2774
|
+
});
|
|
2775
|
+
setCustomerModal([true, CUSTOMER_MODAL_NEW]);
|
|
2776
|
+
}
|
|
2777
|
+
} catch {
|
|
2778
|
+
CapNotification.error({
|
|
2779
|
+
message: formatMessage(messages.memberLookupError),
|
|
2780
|
+
description: formatMessage(messages.memberLookupError),
|
|
2781
|
+
});
|
|
2782
|
+
} finally {
|
|
2783
|
+
setIsCustomerDataLoading(false);
|
|
2784
|
+
}
|
|
2785
|
+
};
|
|
2786
|
+
|
|
2515
2787
|
/**
|
|
2516
2788
|
* Handle send test message
|
|
2517
2789
|
*/
|
|
2518
2790
|
const handleSendTestMessage = () => {
|
|
2519
2791
|
const allUserIds = [];
|
|
2520
2792
|
selectedTestEntities.forEach((entityId) => {
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2793
|
+
if (String(entityId).startsWith('group:')) {
|
|
2794
|
+
const rawId = String(entityId).slice('group:'.length);
|
|
2795
|
+
const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, rawId));
|
|
2796
|
+
if (group) {
|
|
2797
|
+
allUserIds.push(...group.userIds);
|
|
2798
|
+
}
|
|
2524
2799
|
} else {
|
|
2525
|
-
|
|
2800
|
+
const rawId = String(entityId).startsWith('customer:')
|
|
2801
|
+
? String(entityId).slice('customer:'.length)
|
|
2802
|
+
: String(entityId);
|
|
2803
|
+
allUserIds.push(Number(rawId));
|
|
2526
2804
|
}
|
|
2527
2805
|
});
|
|
2528
2806
|
const uniqueUserIds = [...new Set(allUserIds)];
|
|
@@ -2618,6 +2896,20 @@ const CommonTestAndPreview = (props) => {
|
|
|
2618
2896
|
}));
|
|
2619
2897
|
};
|
|
2620
2898
|
|
|
2899
|
+
/** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
|
|
2900
|
+
const handleTestCustomersSearch = useCallback((value) => {
|
|
2901
|
+
if (value == null || value === '') {
|
|
2902
|
+
setSearchValue('');
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
const raw = String(value).trim();
|
|
2906
|
+
if (channel === CHANNELS.SMS) {
|
|
2907
|
+
setSearchValue(formatPhoneNumber(raw));
|
|
2908
|
+
} else {
|
|
2909
|
+
setSearchValue(raw);
|
|
2910
|
+
}
|
|
2911
|
+
}, [channel]);
|
|
2912
|
+
|
|
2621
2913
|
const renderSendTestMessage = () => (
|
|
2622
2914
|
<SendTestMessage
|
|
2623
2915
|
isFetchingTestCustomers={isFetchingTestCustomers}
|
|
@@ -2630,6 +2922,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
2630
2922
|
content={getCurrentContent}
|
|
2631
2923
|
channel={channel}
|
|
2632
2924
|
isSendingTestMessage={isSendingTestMessage}
|
|
2925
|
+
renderAddTestCustomerButton={renderAddTestCustomerButton}
|
|
2633
2926
|
formatMessage={formatMessage}
|
|
2634
2927
|
deliverySettings={testPreviewDeliverySettings[channel]}
|
|
2635
2928
|
senderDetailsOptions={senderDetailsByChannel[channel]}
|
|
@@ -2638,6 +2931,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
2638
2931
|
isLoadingSenderDetails={isLoadingSenderDetails}
|
|
2639
2932
|
smsTraiDltEnabled={smsTraiDltEnabled}
|
|
2640
2933
|
registeredSenderIds={registeredSenderIds}
|
|
2934
|
+
searchValue={searchValue}
|
|
2935
|
+
setSearchValue={handleTestCustomersSearch}
|
|
2641
2936
|
/>
|
|
2642
2937
|
);
|
|
2643
2938
|
|
|
@@ -2647,6 +2942,21 @@ const CommonTestAndPreview = (props) => {
|
|
|
2647
2942
|
/>
|
|
2648
2943
|
);
|
|
2649
2944
|
|
|
2945
|
+
const renderAddTestCustomerButton = () => {
|
|
2946
|
+
const raw = (searchValue || '').trim();
|
|
2947
|
+
const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
|
|
2948
|
+
const showAddButton =
|
|
2949
|
+
[CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
|
|
2950
|
+
(channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
|
|
2951
|
+
if (!showAddButton) return null;
|
|
2952
|
+
return (
|
|
2953
|
+
<AddTestCustomerButton
|
|
2954
|
+
searchValue={value}
|
|
2955
|
+
handleAddTestCustomer={handleAddTestCustomer}
|
|
2956
|
+
/>
|
|
2957
|
+
);
|
|
2958
|
+
};
|
|
2959
|
+
|
|
2650
2960
|
// Header content for the slidebox
|
|
2651
2961
|
const slideboxHeader = (
|
|
2652
2962
|
<CapRow className="test-preview-header">
|
|
@@ -2666,14 +2976,18 @@ const CommonTestAndPreview = (props) => {
|
|
|
2666
2976
|
show={show}
|
|
2667
2977
|
size="size-xl"
|
|
2668
2978
|
content={(
|
|
2669
|
-
<
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2979
|
+
<CapSpin
|
|
2980
|
+
spinning={isCustomerDataLoading}
|
|
2981
|
+
className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
|
|
2982
|
+
>
|
|
2983
|
+
<CapRow className="test-preview-container">
|
|
2984
|
+
<CapRow className="test-and-preview-panels">
|
|
2985
|
+
{/* Left Panel */}
|
|
2986
|
+
<CapRow className="left-panel">
|
|
2987
|
+
{channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
|
|
2988
|
+
<CapDivider className="panel-divider" />
|
|
2989
|
+
|
|
2990
|
+
{/* Send Test Message Section */}
|
|
2677
2991
|
{config.enableTestMessage !== false && (
|
|
2678
2992
|
<CapRow className="panel-section send-test-section">
|
|
2679
2993
|
{renderSendTestMessage()}
|
|
@@ -2687,7 +3001,27 @@ const CommonTestAndPreview = (props) => {
|
|
|
2687
3001
|
{renderPreview()}
|
|
2688
3002
|
</CapRow>
|
|
2689
3003
|
</CapRow>
|
|
2690
|
-
|
|
3004
|
+
{customerModal[0] && customerModal[1] === CUSTOMER_MODAL_EXISTING && (
|
|
3005
|
+
<ExistingCustomerModal
|
|
3006
|
+
customerData={customerData}
|
|
3007
|
+
onCloseCustomerModal={handleCloseCustomerModal}
|
|
3008
|
+
customerModal={customerModal}
|
|
3009
|
+
channel={channel}
|
|
3010
|
+
onSave={handleSaveTestCustomer}
|
|
3011
|
+
/>
|
|
3012
|
+
)}
|
|
3013
|
+
{customerModal[0] && customerModal[1] === CUSTOMER_MODAL_NEW && (
|
|
3014
|
+
<CustomerCreationModal
|
|
3015
|
+
customerData={customerData}
|
|
3016
|
+
setCustomerData={setCustomerData}
|
|
3017
|
+
onCloseCustomerModal={handleCloseCustomerModal}
|
|
3018
|
+
customerModal={customerModal}
|
|
3019
|
+
onSave={handleSaveTestCustomer}
|
|
3020
|
+
channel={channel}
|
|
3021
|
+
/>
|
|
3022
|
+
)}
|
|
3023
|
+
</CapRow>
|
|
3024
|
+
</CapSpin>
|
|
2691
3025
|
)}
|
|
2692
3026
|
/>
|
|
2693
3027
|
);
|
|
@@ -2789,4 +3123,4 @@ CommonTestAndPreview.defaultProps = {
|
|
|
2789
3123
|
// Note: Redux connection is handled by the wrapper components (e.g., TestAndPreviewSlidebox)
|
|
2790
3124
|
// This component receives all Redux props (including intl) from its parent
|
|
2791
3125
|
|
|
2792
|
-
export default CommonTestAndPreview;
|
|
3126
|
+
export default CommonTestAndPreview;
|
|
@@ -8,10 +8,84 @@ import { defineMessages } from 'react-intl';
|
|
|
8
8
|
export const scope = 'app.v2Components.TestAndPreviewSlidebox';
|
|
9
9
|
|
|
10
10
|
export default defineMessages({
|
|
11
|
+
mobileAlreadyExists: {
|
|
12
|
+
id: `${scope}.mobileAlreadyExists`,
|
|
13
|
+
defaultMessage: 'This phone number matches with already existing user profile. Please remove or use a different number.',
|
|
14
|
+
},
|
|
15
|
+
customerNamePlaceholder: {
|
|
16
|
+
id: `${scope}.customerNamePlaceholder`,
|
|
17
|
+
defaultMessage: 'Enter the name',
|
|
18
|
+
},
|
|
19
|
+
customerEmailPlaceholder: {
|
|
20
|
+
id: `${scope}.customerEmailPlaceholder`,
|
|
21
|
+
defaultMessage: 'Enter the Email',
|
|
22
|
+
},
|
|
23
|
+
customerMobilePlaceholder: {
|
|
24
|
+
id: `${scope}.customerMobilePlaceholder`,
|
|
25
|
+
defaultMessage: 'Enter the Mobile Number',
|
|
26
|
+
},
|
|
27
|
+
customerAlreadyInTestList: {
|
|
28
|
+
id: `${scope}.customerAlreadyInTestList`,
|
|
29
|
+
defaultMessage: 'This customer is already in the test customers list.',
|
|
30
|
+
},
|
|
31
|
+
emailAlreadyExists: {
|
|
32
|
+
id: `${scope}.emailAlreadyExists`,
|
|
33
|
+
defaultMessage: 'This email matches with already existing user profile. Please remove or use a different email.',
|
|
34
|
+
},
|
|
35
|
+
customerName: {
|
|
36
|
+
id: `${scope}.customerName`,
|
|
37
|
+
defaultMessage: 'Name',
|
|
38
|
+
},
|
|
39
|
+
customerNameOptional: {
|
|
40
|
+
id: `${scope}.customerNameOptional`,
|
|
41
|
+
defaultMessage: '(Optional)',
|
|
42
|
+
},
|
|
43
|
+
customerCreationInvalidEmail: {
|
|
44
|
+
id: `${scope}.customerCreationInvalidEmail`,
|
|
45
|
+
defaultMessage: 'Please enter a valid email address',
|
|
46
|
+
},
|
|
47
|
+
customerCreationInvalidMobile: {
|
|
48
|
+
id: `${scope}.customerCreationInvalidMobile`,
|
|
49
|
+
defaultMessage: 'Please enter a valid mobile number',
|
|
50
|
+
},
|
|
51
|
+
customerEmail: {
|
|
52
|
+
id: `${scope}.customerEmail`,
|
|
53
|
+
defaultMessage: 'Email',
|
|
54
|
+
},
|
|
55
|
+
customerMobile: {
|
|
56
|
+
id: `${scope}.customerMobileNumber`,
|
|
57
|
+
defaultMessage: 'Mobile Number',
|
|
58
|
+
},
|
|
59
|
+
saveButton: {
|
|
60
|
+
id: `${scope}.saveButton`,
|
|
61
|
+
defaultMessage: 'Save',
|
|
62
|
+
},
|
|
63
|
+
cancelButton: {
|
|
64
|
+
id: `${scope}.cancelButton`,
|
|
65
|
+
defaultMessage: 'Cancel',
|
|
66
|
+
},
|
|
67
|
+
customerID: {
|
|
68
|
+
id: `${scope}.customerID`,
|
|
69
|
+
defaultMessage: 'Customer ID',
|
|
70
|
+
},
|
|
71
|
+
existingCustomerModalDescription: {
|
|
72
|
+
id: `${scope}.existingCustomerModalDescription`,
|
|
73
|
+
defaultMessage: 'This user profile already exists in the system. Would you like to add them to Test Customer list?'
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
customerCreationModalTitle: {
|
|
77
|
+
id: `${scope}.customerCreationModalTitle`,
|
|
78
|
+
defaultMessage: 'Add new test customer',
|
|
79
|
+
},
|
|
80
|
+
customerCreationModalDescription: {
|
|
81
|
+
id: `${scope}.customerCreationModalDescription`,
|
|
82
|
+
defaultMessage: 'This customer profile will be available for testing across multiple communication channels.',
|
|
83
|
+
},
|
|
11
84
|
testAndPreviewHeader: {
|
|
12
85
|
id: `${scope}.testAndPreviewHeader`,
|
|
13
86
|
defaultMessage: 'Preview and Test',
|
|
14
87
|
},
|
|
88
|
+
|
|
15
89
|
customerSearchTitle: {
|
|
16
90
|
id: `${scope}.customerSearchTitle`,
|
|
17
91
|
defaultMessage: 'Customer',
|
|
@@ -32,6 +106,34 @@ export default defineMessages({
|
|
|
32
106
|
id: `${scope}.showJSON`,
|
|
33
107
|
defaultMessage: 'Show JSON',
|
|
34
108
|
},
|
|
109
|
+
addTestCustomer: {
|
|
110
|
+
id: `${scope}.addTestCustomer`,
|
|
111
|
+
defaultMessage: 'Add as Test Customer',
|
|
112
|
+
},
|
|
113
|
+
addTestCustomerWithValue: {
|
|
114
|
+
id: `${scope}.addTestCustomerWithValue`,
|
|
115
|
+
defaultMessage: 'Add {searchValue} as Test Customer',
|
|
116
|
+
},
|
|
117
|
+
memberLookupError: {
|
|
118
|
+
id: `${scope}.memberLookupError`,
|
|
119
|
+
defaultMessage: 'Unable to look up customer. Please try again.',
|
|
120
|
+
},
|
|
121
|
+
newTestCustomerAddedSuccess: {
|
|
122
|
+
id: `${scope}.newTestCustomerAddedSuccess`,
|
|
123
|
+
defaultMessage: 'New test customer added successfully!',
|
|
124
|
+
},
|
|
125
|
+
errorTitle: {
|
|
126
|
+
id: `${scope}.errorTitle`,
|
|
127
|
+
defaultMessage: 'Error',
|
|
128
|
+
},
|
|
129
|
+
failedToAddTestCustomer: {
|
|
130
|
+
id: `${scope}.failedToAddTestCustomer`,
|
|
131
|
+
defaultMessage: 'Failed to add test customer',
|
|
132
|
+
},
|
|
133
|
+
errorAddingTestCustomer: {
|
|
134
|
+
id: `${scope}.errorAddingTestCustomer`,
|
|
135
|
+
defaultMessage: 'An error occurred while adding test customer',
|
|
136
|
+
},
|
|
35
137
|
discardCustomValues: {
|
|
36
138
|
id: `${scope}.discardCustomValues`,
|
|
37
139
|
defaultMessage: 'Discard custom values',
|
|
@@ -120,6 +222,10 @@ export default defineMessages({
|
|
|
120
222
|
id: `${scope}.testCustomersPlaceholder`,
|
|
121
223
|
defaultMessage: 'Search and select a group or individual test customers',
|
|
122
224
|
},
|
|
225
|
+
noMatchingOptions: {
|
|
226
|
+
id: `${scope}.noMatchingOptions`,
|
|
227
|
+
defaultMessage: 'No matching options',
|
|
228
|
+
},
|
|
123
229
|
updatingPreview: {
|
|
124
230
|
id: `${scope}.updatingPreview`,
|
|
125
231
|
defaultMessage: 'Updating preview with the latest changes',
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
GET_TEST_CUSTOMERS_REQUESTED,
|
|
21
21
|
GET_TEST_CUSTOMERS_SUCCESS,
|
|
22
22
|
GET_TEST_CUSTOMERS_FAILURE,
|
|
23
|
+
ADD_TEST_CUSTOMER,
|
|
23
24
|
GET_TEST_GROUPS_REQUESTED,
|
|
24
25
|
GET_TEST_GROUPS_SUCCESS,
|
|
25
26
|
GET_TEST_GROUPS_FAILURE,
|
|
@@ -216,6 +217,17 @@ const previewAndTestReducer = (state = initialState, action) => {
|
|
|
216
217
|
return state.set('isFetchingTestCustomers', false)
|
|
217
218
|
.set('fetchTestCustomersError', action.payload.error);
|
|
218
219
|
|
|
220
|
+
case ADD_TEST_CUSTOMER: {
|
|
221
|
+
const raw = state.get('testCustomers');
|
|
222
|
+
const list = Array.isArray(raw) ? raw : (raw && raw.toArray ? raw.toArray().map((i) => (i && i.toJS ? i.toJS() : i)) : []);
|
|
223
|
+
const customer = action.payload?.customer;
|
|
224
|
+
if (!customer) return state;
|
|
225
|
+
const newId = customer.userId || customer.customerId;
|
|
226
|
+
if (!newId) return state;
|
|
227
|
+
if (list.some((c) => (c.userId || c.customerId) === newId)) return state;
|
|
228
|
+
return state.set('testCustomers', list.concat([customer]));
|
|
229
|
+
}
|
|
230
|
+
|
|
219
231
|
// Test Groups
|
|
220
232
|
case GET_TEST_GROUPS_REQUESTED:
|
|
221
233
|
return state.set('isFetchingTestGroups', true)
|
|
@@ -159,9 +159,10 @@ export function* getBulkCustomerDetails({fetchedUserIds}) {
|
|
|
159
159
|
const emailIdentifier = profile?.identifiers?.find(
|
|
160
160
|
(identifier) => identifier.type === IDENTIFIER_TYPE_EMAIL,
|
|
161
161
|
);
|
|
162
|
+
const nameParts = [profile?.firstName, profile?.lastName].filter(Boolean);
|
|
162
163
|
return {
|
|
163
164
|
userId: item?.entity?.id,
|
|
164
|
-
name:
|
|
165
|
+
name: nameParts.join(' '),
|
|
165
166
|
mobile: mobileIdentifier?.value,
|
|
166
167
|
email: emailIdentifier?.value,
|
|
167
168
|
};
|