@capillarytech/creatives-library 8.0.325 → 8.0.327-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/services/api.js +17 -0
- package/services/tests/api.test.js +85 -0
- package/utils/commonUtils.js +10 -0
- package/utils/tagValidations.js +2 -3
- package/utils/tests/commonUtil.test.js +169 -0
- package/utils/tests/tagValidations.test.js +1 -35
- package/v2Components/CapTagList/index.js +22 -14
- package/v2Components/CapTagList/style.scss +0 -48
- package/v2Components/CapTagListWithInput/index.js +0 -4
- package/v2Components/CapWhatsappCTA/index.js +0 -2
- package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
- package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
- package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
- package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
- package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +135 -36
- package/v2Components/CommonTestAndPreview/actions.js +10 -0
- package/v2Components/CommonTestAndPreview/constants.js +15 -1
- package/v2Components/CommonTestAndPreview/index.js +315 -15
- package/v2Components/CommonTestAndPreview/messages.js +106 -0
- package/v2Components/CommonTestAndPreview/reducer.js +10 -0
- 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 +24 -0
- 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 +52 -0
- 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/v2Components/FormBuilder/index.js +0 -7
- package/v2Components/HtmlEditor/HTMLEditor.js +1 -6
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +2 -927
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +0 -3
- package/v2Containers/BeeEditor/index.js +0 -3
- package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -28
- package/v2Containers/CreativesContainer/index.js +6 -10
- package/v2Containers/Email/index.js +0 -1
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1 -7
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +0 -3
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2 -20
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +1 -16
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +0 -3
- package/v2Containers/EmailWrapper/index.js +0 -4
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -1
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +0 -9
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -19
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -3
- package/v2Containers/InAppWrapper/index.js +0 -3
- package/v2Containers/MobilePush/Create/index.js +0 -2
- package/v2Containers/MobilePush/Edit/index.js +0 -2
- package/v2Containers/MobilepushWrapper/index.js +1 -3
- package/v2Containers/Rcs/index.js +1 -9
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1886 -1754
- package/v2Containers/Sms/Create/index.js +0 -2
- package/v2Containers/Sms/Edit/index.js +0 -2
- package/v2Containers/SmsTrai/Edit/index.js +0 -2
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +351 -318
- package/v2Containers/SmsWrapper/index.js +0 -2
- package/v2Containers/TagList/index.js +2 -41
- package/v2Containers/TagList/messages.js +0 -4
- package/v2Containers/TagList/tests/TagList.test.js +20 -122
- package/v2Containers/TagList/tests/mockdata.js +0 -17
- package/v2Containers/Viber/index.js +0 -5
- package/v2Containers/WebPush/Create/hooks/useTagManagement.js +2 -0
- package/v2Containers/WebPush/Create/index.js +1 -9
- package/v2Containers/Whatsapp/index.js +0 -5
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5232
- package/v2Containers/Zalo/index.js +0 -2
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +0 -63
|
@@ -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';
|
|
@@ -30,8 +33,16 @@ import PreviewSection from './PreviewSection';
|
|
|
30
33
|
|
|
31
34
|
import * as Api from '../../services/api';
|
|
32
35
|
import { extractTemplateVariables } from '../../utils/templateVarUtils';
|
|
36
|
+
import AddTestCustomerButton from './AddTestCustomer';
|
|
37
|
+
import ExistingCustomerModal from './ExistingCustomerModal';
|
|
38
|
+
// Import constants
|
|
33
39
|
import {
|
|
34
40
|
CHANNELS,
|
|
41
|
+
CUSTOMER_MODAL_NEW,
|
|
42
|
+
CUSTOMER_MODAL_EXISTING,
|
|
43
|
+
IDENTIFIER_TYPE_EMAIL,
|
|
44
|
+
IDENTIFIER_TYPE_MOBILE,
|
|
45
|
+
IDENTIFIER_TYPE_PHONE,
|
|
35
46
|
TEST,
|
|
36
47
|
DESKTOP,
|
|
37
48
|
ANDROID,
|
|
@@ -87,6 +98,9 @@ import {
|
|
|
87
98
|
getSmsFallbackTextForTagExtraction,
|
|
88
99
|
} from './previewApiUtils';
|
|
89
100
|
|
|
101
|
+
import { isValidEmail, isValidMobile, formatPhoneNumber } from '../../utils/commonUtils';
|
|
102
|
+
import { getMembersLookup } from '../../services/api';
|
|
103
|
+
|
|
90
104
|
/**
|
|
91
105
|
* Drop empty GSM rows. RCS/DLT responses often set gsm_sender_id equal to domainName — keep those rows
|
|
92
106
|
* for RCS defaults (ModifyDeliverySettings uses the same rule for the sender dropdown).
|
|
@@ -186,6 +200,20 @@ const mapRcsSuggestionForTestMeta = (suggestionRow, index) => ({
|
|
|
186
200
|
: '',
|
|
187
201
|
});
|
|
188
202
|
|
|
203
|
+
/**
|
|
204
|
+
* CapTreeSelect and group resolution use strict equality; API data may mix numeric and string ids.
|
|
205
|
+
*/
|
|
206
|
+
const normalizeTestEntityId = (id) => {
|
|
207
|
+
if (id === undefined || id === null) return id;
|
|
208
|
+
return String(id);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const testEntityIdsEqual = (a, b) => {
|
|
212
|
+
if (a == null && b == null) return true;
|
|
213
|
+
if (a == null || b == null) return false;
|
|
214
|
+
return String(a) === String(b);
|
|
215
|
+
};
|
|
216
|
+
|
|
189
217
|
/**
|
|
190
218
|
* Preview Component Factory - REMOVED IN PHASE 5
|
|
191
219
|
* Now using UnifiedPreview component for all channels
|
|
@@ -252,6 +280,11 @@ const CommonTestAndPreview = (props) => {
|
|
|
252
280
|
const [tagsExtracted, setTagsExtracted] = useState(false);
|
|
253
281
|
|
|
254
282
|
const initialDevice = CHANNELS_USING_ANDROID_PREVIEW_DEVICE.includes(channel) ? ANDROID : DESKTOP;
|
|
283
|
+
const [searchValue, setSearchValue] = useState("");
|
|
284
|
+
const [customerModal, setCustomerModal] = useState([false, ""]);
|
|
285
|
+
const [isCustomerDataLoading, setIsCustomerDataLoading] = useState(false);
|
|
286
|
+
const [customerData, setCustomerData] = useState({ name: '', email: '', mobile: '', customerId: '' });
|
|
287
|
+
|
|
255
288
|
const [previewDevice, setPreviewDevice] = useState(initialDevice);
|
|
256
289
|
const [activePreviewTab, setActivePreviewTab] = useState(PREVIEW_TAB_RCS);
|
|
257
290
|
const [smsFallbackPreviewText, setSmsFallbackPreviewText] = useState(undefined);
|
|
@@ -634,19 +667,26 @@ const CommonTestAndPreview = (props) => {
|
|
|
634
667
|
}, [channel, smsFallbackContent, smsFallbackPreviewText]);
|
|
635
668
|
|
|
636
669
|
// Build test entities tree data from testCustomers prop
|
|
670
|
+
// Build test entities tree data from testCustomers prop (includes customers added via addTestCustomer action)
|
|
637
671
|
const testEntitiesTreeData = useMemo(() => {
|
|
638
672
|
const groupsNode = {
|
|
639
673
|
title: 'Groups',
|
|
640
674
|
value: 'groups-node',
|
|
641
675
|
selectable: false,
|
|
642
|
-
children: testGroups?.map((group) => ({
|
|
676
|
+
children: testGroups?.map((group) => ({
|
|
677
|
+
title: group?.groupName,
|
|
678
|
+
value: normalizeTestEntityId(group?.groupId),
|
|
679
|
+
})),
|
|
643
680
|
};
|
|
644
681
|
|
|
645
682
|
const customersNode = {
|
|
646
683
|
title: 'Individuals',
|
|
647
684
|
value: 'customers-node',
|
|
648
685
|
selectable: false,
|
|
649
|
-
children: testCustomers?.map((customer) => ({
|
|
686
|
+
children: testCustomers?.map((customer) => ({
|
|
687
|
+
title: customer?.name?.trim() || customer?.email?.trim() || customer?.mobile?.trim() || customer?.userId || customer?.customerId,
|
|
688
|
+
value: normalizeTestEntityId(customer?.userId ?? customer?.customerId),
|
|
689
|
+
})) || [],
|
|
650
690
|
};
|
|
651
691
|
|
|
652
692
|
return [groupsNode, customersNode];
|
|
@@ -673,6 +713,93 @@ const CommonTestAndPreview = (props) => {
|
|
|
673
713
|
return resolvedText;
|
|
674
714
|
};
|
|
675
715
|
|
|
716
|
+
/**
|
|
717
|
+
* Common handler for saving test customers (both new and existing)
|
|
718
|
+
*/
|
|
719
|
+
const handleSaveTestCustomer = async (validationErrors = {}, setIsLoading = () => {}) => {
|
|
720
|
+
// Check for validation errors before saving (for new customers)
|
|
721
|
+
if (customerModal[1] === CUSTOMER_MODAL_NEW && (validationErrors.email || validationErrors.mobile)) {
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
setIsLoading(true);
|
|
726
|
+
|
|
727
|
+
try {
|
|
728
|
+
let payload;
|
|
729
|
+
|
|
730
|
+
if (customerModal[1] === CUSTOMER_MODAL_EXISTING) {
|
|
731
|
+
// For existing customers, use customerId
|
|
732
|
+
payload = {
|
|
733
|
+
campaignUserId: customerData.customerId
|
|
734
|
+
};
|
|
735
|
+
} else {
|
|
736
|
+
// For new customers, use customer object
|
|
737
|
+
payload = {
|
|
738
|
+
customer: {
|
|
739
|
+
firstName: customerData.name || "",
|
|
740
|
+
mobile: customerData.mobile || "",
|
|
741
|
+
email: customerData.email || ""
|
|
742
|
+
}
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const response = await createTestCustomer(payload);
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
// Handle success: add to test customers list and selection (existing and new)
|
|
750
|
+
if (response && response.success) {
|
|
751
|
+
CapNotification.success({
|
|
752
|
+
message: formatMessage(messages.newTestCustomerAddedSuccess),
|
|
753
|
+
});
|
|
754
|
+
// API may return customerId in response.response (e.g. { response: { customerId: 438845651 } })
|
|
755
|
+
const res = response?.response || response;
|
|
756
|
+
const addedId = customerModal[1] === CUSTOMER_MODAL_EXISTING
|
|
757
|
+
? customerData?.customerId || customerData?.campaignUserId
|
|
758
|
+
: res?.customerId || res?.campaignUserId;
|
|
759
|
+
if (addedId) {
|
|
760
|
+
const normalizedAddedId = normalizeTestEntityId(addedId);
|
|
761
|
+
actions.addTestCustomer({
|
|
762
|
+
userId: normalizedAddedId,
|
|
763
|
+
customerId: normalizedAddedId,
|
|
764
|
+
name: customerData?.name?.trim() || '',
|
|
765
|
+
email: customerData?.email || '',
|
|
766
|
+
mobile: customerData?.mobile || '',
|
|
767
|
+
});
|
|
768
|
+
setSelectedTestEntities((prev) => (
|
|
769
|
+
prev.some((id) => testEntityIdsEqual(id, normalizedAddedId))
|
|
770
|
+
? prev
|
|
771
|
+
: [...prev, normalizedAddedId]
|
|
772
|
+
));
|
|
773
|
+
}
|
|
774
|
+
handleCloseCustomerModal();
|
|
775
|
+
} else {
|
|
776
|
+
// Show error notification for unsuccessful response
|
|
777
|
+
CapNotification.error({
|
|
778
|
+
message: formatMessage(messages.errorTitle),
|
|
779
|
+
description: response?.message || formatMessage(messages.failedToAddTestCustomer),
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
} catch (error) {
|
|
783
|
+
CapNotification.error({
|
|
784
|
+
message: formatMessage(messages.errorTitle),
|
|
785
|
+
description: error?.message || formatMessage(messages.errorAddingTestCustomer),
|
|
786
|
+
});
|
|
787
|
+
} finally {
|
|
788
|
+
setIsLoading(false);
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
const handleCloseCustomerModal = () => {
|
|
793
|
+
setCustomerModal([false, ""]);
|
|
794
|
+
setSearchValue('');
|
|
795
|
+
setCustomerData({
|
|
796
|
+
name: '',
|
|
797
|
+
email: '',
|
|
798
|
+
mobile: '',
|
|
799
|
+
customerId: '',
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
|
|
676
803
|
/**
|
|
677
804
|
* Prepare payload for preview API based on channel
|
|
678
805
|
*/
|
|
@@ -2763,7 +2890,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
2763
2890
|
* Handle slidebox close
|
|
2764
2891
|
*/
|
|
2765
2892
|
const handleClose = () => {
|
|
2766
|
-
// Reset state when closing
|
|
2893
|
+
// Reset state when closing (includes add-customer modal + tree search state)
|
|
2894
|
+
handleCloseCustomerModal();
|
|
2767
2895
|
setSelectedCustomer(null);
|
|
2768
2896
|
setRequiredTags([]);
|
|
2769
2897
|
setOptionalTags([]);
|
|
@@ -3023,16 +3151,132 @@ const CommonTestAndPreview = (props) => {
|
|
|
3023
3151
|
* Handle test entities change
|
|
3024
3152
|
*/
|
|
3025
3153
|
const handleTestEntitiesChange = (value) => {
|
|
3026
|
-
|
|
3154
|
+
if (!Array.isArray(value)) {
|
|
3155
|
+
if (value == null || value === '') {
|
|
3156
|
+
setSelectedTestEntities([]);
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
setSelectedTestEntities([normalizeTestEntityId(value)]);
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
setSelectedTestEntities(value.map((v) => normalizeTestEntityId(v)));
|
|
3027
3163
|
};
|
|
3028
3164
|
|
|
3165
|
+
/**
|
|
3166
|
+
* Map API customerDetails item to our customerData shape
|
|
3167
|
+
*/
|
|
3168
|
+
const mapCustomerDetailsToCustomerData = (detail, identifierValue) => {
|
|
3169
|
+
const firstName = detail.firstName || '';
|
|
3170
|
+
const lastName = detail.lastName || '';
|
|
3171
|
+
const name = [firstName, lastName].filter(Boolean).join(' ').trim() || '';
|
|
3172
|
+
const getIdentifierValue = (type) => {
|
|
3173
|
+
const fromIdentifiers = detail.identifiers?.find((i) => i.type === type)?.value;
|
|
3174
|
+
if (fromIdentifiers) return fromIdentifiers;
|
|
3175
|
+
const fromCommChannels = detail.commChannels?.find((c) => c.type === type)?.value;
|
|
3176
|
+
return fromCommChannels || (channel === CHANNELS.EMAIL && type === IDENTIFIER_TYPE_EMAIL ? identifierValue : channel === CHANNELS.SMS && type === IDENTIFIER_TYPE_MOBILE ? identifierValue : '');
|
|
3177
|
+
};
|
|
3178
|
+
return {
|
|
3179
|
+
name,
|
|
3180
|
+
email: channel === CHANNELS.EMAIL ? (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_EMAIL) || ''),
|
|
3181
|
+
mobile: channel === CHANNELS.SMS ? (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || identifierValue) : (getIdentifierValue(IDENTIFIER_TYPE_MOBILE) || getIdentifierValue(IDENTIFIER_TYPE_PHONE) || ''),
|
|
3182
|
+
customerId: detail.userId != null ? String(detail.userId) : '',
|
|
3183
|
+
};
|
|
3184
|
+
};
|
|
3185
|
+
|
|
3186
|
+
const handleAddTestCustomer = async () => {
|
|
3187
|
+
const identifierType = channel === CHANNELS.EMAIL ? IDENTIFIER_TYPE_EMAIL : IDENTIFIER_TYPE_MOBILE;
|
|
3188
|
+
const searchValueToCheck = channel === CHANNELS.SMS
|
|
3189
|
+
? formatPhoneNumber((searchValue || '').trim())
|
|
3190
|
+
: (searchValue || '').trim();
|
|
3191
|
+
|
|
3192
|
+
// Check if this customer is already in the test customers list
|
|
3193
|
+
const existingTestCustomer = testCustomers?.find(customer => {
|
|
3194
|
+
if (channel === CHANNELS.EMAIL) {
|
|
3195
|
+
return customer.email === searchValueToCheck;
|
|
3196
|
+
} else if (channel === CHANNELS.SMS) {
|
|
3197
|
+
return customer.mobile === searchValueToCheck;
|
|
3198
|
+
}
|
|
3199
|
+
return false;
|
|
3200
|
+
});
|
|
3201
|
+
|
|
3202
|
+
if (existingTestCustomer) {
|
|
3203
|
+
const entityId = existingTestCustomer.userId ?? existingTestCustomer.customerId;
|
|
3204
|
+
if (entityId != null) {
|
|
3205
|
+
const id = normalizeTestEntityId(entityId);
|
|
3206
|
+
setSelectedTestEntities((prev) => (
|
|
3207
|
+
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
3208
|
+
));
|
|
3209
|
+
}
|
|
3210
|
+
setSearchValue('');
|
|
3211
|
+
CapNotification.success({
|
|
3212
|
+
message: formatMessage(messages.customerAlreadyInTestList),
|
|
3213
|
+
});
|
|
3214
|
+
return;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
setIsCustomerDataLoading(true);
|
|
3218
|
+
|
|
3219
|
+
try {
|
|
3220
|
+
const response = await getMembersLookup(identifierType, searchValueToCheck);
|
|
3221
|
+
const success = response?.success && !response?.status?.isError;
|
|
3222
|
+
const res = response?.response || {};
|
|
3223
|
+
const exists = res.exists || false;
|
|
3224
|
+
const details = res.customerDetails || [];
|
|
3225
|
+
|
|
3226
|
+
if (!success) {
|
|
3227
|
+
const errorMessage = response?.message || response?.status?.message || formatMessage(messages.memberLookupError);
|
|
3228
|
+
CapNotification.error({
|
|
3229
|
+
message: formatMessage(messages.memberLookupError),
|
|
3230
|
+
description: errorMessage,
|
|
3231
|
+
});
|
|
3232
|
+
return;
|
|
3233
|
+
}
|
|
3234
|
+
|
|
3235
|
+
if (exists && details.length > 0) {
|
|
3236
|
+
const mapped = mapCustomerDetailsToCustomerData(details[0], searchValueToCheck);
|
|
3237
|
+
const customerIdFromLookup = mapped.customerId;
|
|
3238
|
+
const alreadyInTestListByCustomerId = customerIdFromLookup && testCustomers?.some(
|
|
3239
|
+
(c) => String(c?.customerId) === customerIdFromLookup || String(c?.userId) === customerIdFromLookup
|
|
3240
|
+
);
|
|
3241
|
+
if (alreadyInTestListByCustomerId) {
|
|
3242
|
+
const id = normalizeTestEntityId(customerIdFromLookup);
|
|
3243
|
+
setSelectedTestEntities((prev) => (
|
|
3244
|
+
prev.some((existing) => testEntityIdsEqual(existing, id)) ? prev : [...prev, id]
|
|
3245
|
+
));
|
|
3246
|
+
setSearchValue('');
|
|
3247
|
+
CapNotification.success({
|
|
3248
|
+
message: formatMessage(messages.customerAlreadyInTestList),
|
|
3249
|
+
});
|
|
3250
|
+
return;
|
|
3251
|
+
}
|
|
3252
|
+
setCustomerData(mapped);
|
|
3253
|
+
setCustomerModal([true, CUSTOMER_MODAL_EXISTING]);
|
|
3254
|
+
} else {
|
|
3255
|
+
setCustomerData({
|
|
3256
|
+
name: '',
|
|
3257
|
+
email: channel === CHANNELS.EMAIL ? searchValueToCheck : '',
|
|
3258
|
+
mobile: channel === CHANNELS.SMS ? searchValueToCheck : '',
|
|
3259
|
+
customerId: '',
|
|
3260
|
+
});
|
|
3261
|
+
setCustomerModal([true, CUSTOMER_MODAL_NEW]);
|
|
3262
|
+
}
|
|
3263
|
+
} catch {
|
|
3264
|
+
CapNotification.error({
|
|
3265
|
+
message: formatMessage(messages.memberLookupError),
|
|
3266
|
+
description: formatMessage(messages.memberLookupError),
|
|
3267
|
+
});
|
|
3268
|
+
} finally {
|
|
3269
|
+
setIsCustomerDataLoading(false);
|
|
3270
|
+
}
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3029
3273
|
/**
|
|
3030
3274
|
* Handle send test message
|
|
3031
3275
|
*/
|
|
3032
3276
|
const handleSendTestMessage = () => {
|
|
3033
3277
|
const allUserIds = [];
|
|
3034
3278
|
selectedTestEntities.forEach((entityId) => {
|
|
3035
|
-
const group = testGroups.find((
|
|
3279
|
+
const group = testGroups.find((g) => testEntityIdsEqual(g.groupId, entityId));
|
|
3036
3280
|
if (group) {
|
|
3037
3281
|
allUserIds.push(...group.userIds);
|
|
3038
3282
|
} else {
|
|
@@ -3155,6 +3399,20 @@ const CommonTestAndPreview = (props) => {
|
|
|
3155
3399
|
}));
|
|
3156
3400
|
};
|
|
3157
3401
|
|
|
3402
|
+
/** Trim pasted emails (trailing CR/LF). SMS: strip non-digits so pasted formatted numbers match isValidMobile / API. */
|
|
3403
|
+
const handleTestCustomersSearch = useCallback((value) => {
|
|
3404
|
+
if (value == null || value === '') {
|
|
3405
|
+
setSearchValue('');
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
const raw = String(value).trim();
|
|
3409
|
+
if (channel === CHANNELS.SMS) {
|
|
3410
|
+
setSearchValue(formatPhoneNumber(raw));
|
|
3411
|
+
} else {
|
|
3412
|
+
setSearchValue(raw);
|
|
3413
|
+
}
|
|
3414
|
+
}, [channel]);
|
|
3415
|
+
|
|
3158
3416
|
const renderSendTestMessage = () => (
|
|
3159
3417
|
<SendTestMessage
|
|
3160
3418
|
isFetchingTestCustomers={isFetchingTestCustomers}
|
|
@@ -3167,6 +3425,7 @@ const CommonTestAndPreview = (props) => {
|
|
|
3167
3425
|
content={getCurrentContent}
|
|
3168
3426
|
channel={channel}
|
|
3169
3427
|
isSendingTestMessage={isSendingTestMessage}
|
|
3428
|
+
renderAddTestCustomerButton={renderAddTestCustomerButton}
|
|
3170
3429
|
formatMessage={formatMessage}
|
|
3171
3430
|
deliverySettings={testPreviewDeliverySettings[channel]}
|
|
3172
3431
|
senderDetailsByChannel={senderDetailsByChannel}
|
|
@@ -3176,6 +3435,8 @@ const CommonTestAndPreview = (props) => {
|
|
|
3176
3435
|
smsTraiDltEnabled={smsTraiDltEnabled}
|
|
3177
3436
|
registeredSenderIds={registeredSenderIds}
|
|
3178
3437
|
isChannelSmsFallbackPreviewEnabled={channel === CHANNELS.RCS && !!smsFallbackContent?.templateContent}
|
|
3438
|
+
searchValue={searchValue}
|
|
3439
|
+
setSearchValue={handleTestCustomersSearch}
|
|
3179
3440
|
/>
|
|
3180
3441
|
);
|
|
3181
3442
|
|
|
@@ -3185,6 +3446,21 @@ const CommonTestAndPreview = (props) => {
|
|
|
3185
3446
|
/>
|
|
3186
3447
|
);
|
|
3187
3448
|
|
|
3449
|
+
const renderAddTestCustomerButton = () => {
|
|
3450
|
+
const raw = (searchValue || '').trim();
|
|
3451
|
+
const value = channel === CHANNELS.SMS ? formatPhoneNumber(raw) : raw;
|
|
3452
|
+
const showAddButton =
|
|
3453
|
+
[CHANNELS.EMAIL, CHANNELS.SMS].includes(channel) &&
|
|
3454
|
+
(channel === CHANNELS.EMAIL ? isValidEmail(value) : isValidMobile(value));
|
|
3455
|
+
if (!showAddButton) return null;
|
|
3456
|
+
return (
|
|
3457
|
+
<AddTestCustomerButton
|
|
3458
|
+
searchValue={value}
|
|
3459
|
+
handleAddTestCustomer={handleAddTestCustomer}
|
|
3460
|
+
/>
|
|
3461
|
+
);
|
|
3462
|
+
};
|
|
3463
|
+
|
|
3188
3464
|
// Header content for the slidebox
|
|
3189
3465
|
const slideboxHeader = (
|
|
3190
3466
|
<CapRow className="test-preview-header">
|
|
@@ -3204,14 +3480,18 @@ const CommonTestAndPreview = (props) => {
|
|
|
3204
3480
|
show={show}
|
|
3205
3481
|
size="size-xl"
|
|
3206
3482
|
content={(
|
|
3207
|
-
<
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3483
|
+
<CapSpin
|
|
3484
|
+
spinning={isCustomerDataLoading}
|
|
3485
|
+
className={`common-test-preview-lookup-spin ${isCustomerDataLoading ? 'common-test-preview-customer-loading' : ''}`}
|
|
3486
|
+
>
|
|
3487
|
+
<CapRow className="test-preview-container">
|
|
3488
|
+
<CapRow className="test-and-preview-panels">
|
|
3489
|
+
{/* Left Panel */}
|
|
3490
|
+
<CapRow className="left-panel">
|
|
3491
|
+
{channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
|
|
3492
|
+
<CapDivider className="panel-divider" />
|
|
3493
|
+
|
|
3494
|
+
{/* Send Test Message Section */}
|
|
3215
3495
|
{config.enableTestMessage !== false && (
|
|
3216
3496
|
<CapRow className="panel-section send-test-section">
|
|
3217
3497
|
{renderSendTestMessage()}
|
|
@@ -3225,7 +3505,27 @@ const CommonTestAndPreview = (props) => {
|
|
|
3225
3505
|
{renderPreview()}
|
|
3226
3506
|
</CapRow>
|
|
3227
3507
|
</CapRow>
|
|
3228
|
-
|
|
3508
|
+
{customerModal[0] && customerModal[1] === CUSTOMER_MODAL_EXISTING && (
|
|
3509
|
+
<ExistingCustomerModal
|
|
3510
|
+
customerData={customerData}
|
|
3511
|
+
onCloseCustomerModal={handleCloseCustomerModal}
|
|
3512
|
+
customerModal={customerModal}
|
|
3513
|
+
channel={channel}
|
|
3514
|
+
onSave={handleSaveTestCustomer}
|
|
3515
|
+
/>
|
|
3516
|
+
)}
|
|
3517
|
+
{customerModal[0] && customerModal[1] === CUSTOMER_MODAL_NEW && (
|
|
3518
|
+
<CustomerCreationModal
|
|
3519
|
+
customerData={customerData}
|
|
3520
|
+
setCustomerData={setCustomerData}
|
|
3521
|
+
onCloseCustomerModal={handleCloseCustomerModal}
|
|
3522
|
+
customerModal={customerModal}
|
|
3523
|
+
onSave={handleSaveTestCustomer}
|
|
3524
|
+
channel={channel}
|
|
3525
|
+
/>
|
|
3526
|
+
)}
|
|
3527
|
+
</CapRow>
|
|
3528
|
+
</CapSpin>
|
|
3229
3529
|
)}
|
|
3230
3530
|
/>
|
|
3231
3531
|
);
|
|
@@ -3327,4 +3627,4 @@ CommonTestAndPreview.defaultProps = {
|
|
|
3327
3627
|
// Note: Redux connection is handled by the wrapper components (e.g., TestAndPreviewSlidebox)
|
|
3328
3628
|
// This component receives all Redux props (including intl) from its parent
|
|
3329
3629
|
|
|
3330
|
-
export default CommonTestAndPreview;
|
|
3630
|
+
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',
|
|
@@ -46,6 +120,34 @@ export default defineMessages({
|
|
|
46
120
|
id: `${scope}.showJSON`,
|
|
47
121
|
defaultMessage: 'Show JSON',
|
|
48
122
|
},
|
|
123
|
+
addTestCustomer: {
|
|
124
|
+
id: `${scope}.addTestCustomer`,
|
|
125
|
+
defaultMessage: 'Add as Test Customer',
|
|
126
|
+
},
|
|
127
|
+
addTestCustomerWithValue: {
|
|
128
|
+
id: `${scope}.addTestCustomerWithValue`,
|
|
129
|
+
defaultMessage: 'Add {searchValue} as Test Customer',
|
|
130
|
+
},
|
|
131
|
+
memberLookupError: {
|
|
132
|
+
id: `${scope}.memberLookupError`,
|
|
133
|
+
defaultMessage: 'Unable to look up customer. Please try again.',
|
|
134
|
+
},
|
|
135
|
+
newTestCustomerAddedSuccess: {
|
|
136
|
+
id: `${scope}.newTestCustomerAddedSuccess`,
|
|
137
|
+
defaultMessage: 'New test customer added successfully!',
|
|
138
|
+
},
|
|
139
|
+
errorTitle: {
|
|
140
|
+
id: `${scope}.errorTitle`,
|
|
141
|
+
defaultMessage: 'Error',
|
|
142
|
+
},
|
|
143
|
+
failedToAddTestCustomer: {
|
|
144
|
+
id: `${scope}.failedToAddTestCustomer`,
|
|
145
|
+
defaultMessage: 'Failed to add test customer',
|
|
146
|
+
},
|
|
147
|
+
errorAddingTestCustomer: {
|
|
148
|
+
id: `${scope}.errorAddingTestCustomer`,
|
|
149
|
+
defaultMessage: 'An error occurred while adding test customer',
|
|
150
|
+
},
|
|
49
151
|
discardCustomValues: {
|
|
50
152
|
id: `${scope}.discardCustomValues`,
|
|
51
153
|
defaultMessage: 'Discard custom values',
|
|
@@ -150,6 +252,10 @@ export default defineMessages({
|
|
|
150
252
|
id: `${scope}.testCustomersPlaceholder`,
|
|
151
253
|
defaultMessage: 'Search and select a group or individual test customers',
|
|
152
254
|
},
|
|
255
|
+
noMatchingOptions: {
|
|
256
|
+
id: `${scope}.noMatchingOptions`,
|
|
257
|
+
defaultMessage: 'No matching options',
|
|
258
|
+
},
|
|
153
259
|
updatingPreview: {
|
|
154
260
|
id: `${scope}.updatingPreview`,
|
|
155
261
|
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,15 @@ 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
|
+
const newId = customer.userId || customer.customerId;
|
|
225
|
+
if (list.some((c) => (c.userId || c.customerId) === newId)) return state;
|
|
226
|
+
return state.set('testCustomers', list.concat([customer]));
|
|
227
|
+
}
|
|
228
|
+
|
|
219
229
|
// Test Groups
|
|
220
230
|
case GET_TEST_GROUPS_REQUESTED:
|
|
221
231
|
return state.set('isFetchingTestGroups', true)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for AddTestCustomerButton Component
|
|
3
|
+
*
|
|
4
|
+
* The parent (index.js) only renders this button when channel is EMAIL/SMS and value is valid.
|
|
5
|
+
* This component always renders the button when mounted; visibility is the parent's responsibility.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import React from 'react';
|
|
9
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
10
|
+
import { IntlProvider } from 'react-intl';
|
|
11
|
+
import AddTestCustomerButton from '../AddTestCustomer';
|
|
12
|
+
|
|
13
|
+
const mockMessages = {
|
|
14
|
+
'app.v2Components.TestAndPreviewSlidebox.addTestCustomerWithValue': 'Add {searchValue} as Test Customer',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const TestWrapper = ({ children }) => (
|
|
18
|
+
<IntlProvider locale="en" messages={mockMessages}>
|
|
19
|
+
{children}
|
|
20
|
+
</IntlProvider>
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
describe('AddTestCustomerButton', () => {
|
|
24
|
+
const defaultProps = {
|
|
25
|
+
searchValue: 'user@example.com',
|
|
26
|
+
handleAddTestCustomer: jest.fn(),
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should render button with searchValue in message', () => {
|
|
34
|
+
render(
|
|
35
|
+
<TestWrapper>
|
|
36
|
+
<AddTestCustomerButton {...defaultProps} />
|
|
37
|
+
</TestWrapper>
|
|
38
|
+
);
|
|
39
|
+
const button = screen.getByRole('button', { name: /add.*test customer/i });
|
|
40
|
+
expect(button).toBeTruthy();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should show searchValue in button text', () => {
|
|
44
|
+
render(
|
|
45
|
+
<TestWrapper>
|
|
46
|
+
<AddTestCustomerButton {...defaultProps} searchValue="user@example.com" />
|
|
47
|
+
</TestWrapper>
|
|
48
|
+
);
|
|
49
|
+
expect(screen.getByRole('button', { name: /user@example.com/i })).toBeTruthy();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should call handleAddTestCustomer when button is clicked', () => {
|
|
53
|
+
const handleAddTestCustomer = jest.fn();
|
|
54
|
+
render(
|
|
55
|
+
<TestWrapper>
|
|
56
|
+
<AddTestCustomerButton
|
|
57
|
+
searchValue="user@example.com"
|
|
58
|
+
handleAddTestCustomer={handleAddTestCustomer}
|
|
59
|
+
/>
|
|
60
|
+
</TestWrapper>
|
|
61
|
+
);
|
|
62
|
+
const button = screen.getByRole('button', { name: /add.*test customer/i });
|
|
63
|
+
fireEvent.click(button);
|
|
64
|
+
expect(handleAddTestCustomer).toHaveBeenCalledTimes(1);
|
|
65
|
+
});
|
|
66
|
+
});
|