@capillarytech/creatives-library 8.0.330 → 8.0.331-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.
Files changed (31) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +17 -0
  3. package/services/tests/api.test.js +72 -0
  4. package/utils/commonUtils.js +10 -0
  5. package/utils/tests/commonUtil.test.js +169 -0
  6. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  7. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
  8. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +94 -0
  9. package/v2Components/CommonTestAndPreview/SendTestMessage.js +78 -49
  10. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +134 -34
  11. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  12. package/v2Components/CommonTestAndPreview/constants.js +17 -1
  13. package/v2Components/CommonTestAndPreview/index.js +356 -22
  14. package/v2Components/CommonTestAndPreview/messages.js +106 -0
  15. package/v2Components/CommonTestAndPreview/reducer.js +12 -0
  16. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  17. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  18. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
  19. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +23 -5
  20. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
  21. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  22. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +39 -19
  23. package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
  24. package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
  25. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  26. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  27. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1408 -1276
  28. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +321 -288
  29. package/v2Containers/TagList/index.js +11 -15
  30. package/v2Containers/WebPush/Create/index.js +1 -1
  31. 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
- // Initialize device based on channel: SMS uses Android/iOS, others use Desktop/Mobile
134
- // Initialize device based on channel: SMS, WhatsApp, RCS, InApp, MobilePush, and Viber use Android/iOS, others use Desktop/Mobile
135
- const initialDevice = (channel === CHANNELS.SMS || channel === CHANNELS.WHATSAPP || channel === CHANNELS.RCS || channel === CHANNELS.INAPP || channel === CHANNELS.MOBILEPUSH || channel === CHANNELS.VIBER) ? ANDROID : DESKTOP;
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) => ({ title: group?.groupName, value: group?.groupId })),
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) => ({ title: customer.name, value: customer?.userId })),
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
- setSelectedTestEntities(value);
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
- const group = testGroups.find((g) => g.groupId === entityId);
2522
- if (group) {
2523
- allUserIds.push(...group.userIds);
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
- allUserIds.push(entityId);
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
- <CapRow className="test-preview-container">
2670
- <CapRow className="test-and-preview-panels">
2671
- {/* Left Panel */}
2672
- <CapRow className="left-panel">
2673
- {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
2674
- <CapDivider className="panel-divider" />
2675
-
2676
- {/* Send Test Message Section */}
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
- </CapRow>
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: `${profile?.firstName} ${profile?.lastName}`,
165
+ name: nameParts.join(' '),
165
166
  mobile: mobileIdentifier?.value,
166
167
  email: emailIdentifier?.value,
167
168
  };