@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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/services/api.js +17 -0
  3. package/services/tests/api.test.js +85 -0
  4. package/utils/commonUtils.js +10 -0
  5. package/utils/tagValidations.js +2 -3
  6. package/utils/tests/commonUtil.test.js +169 -0
  7. package/utils/tests/tagValidations.test.js +1 -35
  8. package/v2Components/CapTagList/index.js +22 -14
  9. package/v2Components/CapTagList/style.scss +0 -48
  10. package/v2Components/CapTagListWithInput/index.js +0 -4
  11. package/v2Components/CapWhatsappCTA/index.js +0 -2
  12. package/v2Components/CommonTestAndPreview/AddTestCustomer.js +42 -0
  13. package/v2Components/CommonTestAndPreview/CustomerCreationModal.js +155 -0
  14. package/v2Components/CommonTestAndPreview/ExistingCustomerModal.js +93 -0
  15. package/v2Components/CommonTestAndPreview/SendTestMessage.js +79 -51
  16. package/v2Components/CommonTestAndPreview/_commonTestAndPreview.scss +135 -36
  17. package/v2Components/CommonTestAndPreview/actions.js +10 -0
  18. package/v2Components/CommonTestAndPreview/constants.js +15 -1
  19. package/v2Components/CommonTestAndPreview/index.js +315 -15
  20. package/v2Components/CommonTestAndPreview/messages.js +106 -0
  21. package/v2Components/CommonTestAndPreview/reducer.js +10 -0
  22. package/v2Components/CommonTestAndPreview/tests/AddTestCustomer.test.js +66 -0
  23. package/v2Components/CommonTestAndPreview/tests/CommonTestAndPreview.addTestCustomer.test.js +648 -0
  24. package/v2Components/CommonTestAndPreview/tests/CustomValuesEditor.test.js +24 -0
  25. package/v2Components/CommonTestAndPreview/tests/CustomerCreationModal.test.js +174 -0
  26. package/v2Components/CommonTestAndPreview/tests/ExistingCustomerModal.test.js +114 -0
  27. package/v2Components/CommonTestAndPreview/tests/SendTestMessage.test.js +52 -0
  28. package/v2Components/CommonTestAndPreview/tests/constants.test.js +31 -1
  29. package/v2Components/CommonTestAndPreview/tests/index.test.js +36 -0
  30. package/v2Components/CommonTestAndPreview/tests/reducer.test.js +71 -0
  31. package/v2Components/CommonTestAndPreview/tests/selectors.test.js +17 -0
  32. package/v2Components/FormBuilder/index.js +0 -7
  33. package/v2Components/HtmlEditor/HTMLEditor.js +1 -6
  34. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -1
  35. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +2 -927
  36. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +0 -3
  37. package/v2Containers/BeeEditor/index.js +0 -3
  38. package/v2Containers/CreativesContainer/SlideBoxContent.js +1 -28
  39. package/v2Containers/CreativesContainer/index.js +6 -10
  40. package/v2Containers/Email/index.js +0 -1
  41. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1 -7
  42. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +0 -3
  43. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2 -20
  44. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +1 -16
  45. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +0 -3
  46. package/v2Containers/EmailWrapper/index.js +0 -4
  47. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -1
  48. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +0 -9
  49. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -19
  50. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -3
  51. package/v2Containers/InAppWrapper/index.js +0 -3
  52. package/v2Containers/MobilePush/Create/index.js +0 -2
  53. package/v2Containers/MobilePush/Edit/index.js +0 -2
  54. package/v2Containers/MobilepushWrapper/index.js +1 -3
  55. package/v2Containers/Rcs/index.js +1 -9
  56. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +1886 -1754
  57. package/v2Containers/Sms/Create/index.js +0 -2
  58. package/v2Containers/Sms/Edit/index.js +0 -2
  59. package/v2Containers/SmsTrai/Edit/index.js +0 -2
  60. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +351 -318
  61. package/v2Containers/SmsWrapper/index.js +0 -2
  62. package/v2Containers/TagList/index.js +2 -41
  63. package/v2Containers/TagList/messages.js +0 -4
  64. package/v2Containers/TagList/tests/TagList.test.js +20 -122
  65. package/v2Containers/TagList/tests/mockdata.js +0 -17
  66. package/v2Containers/Viber/index.js +0 -5
  67. package/v2Containers/WebPush/Create/hooks/useTagManagement.js +2 -0
  68. package/v2Containers/WebPush/Create/index.js +1 -9
  69. package/v2Containers/Whatsapp/index.js +0 -5
  70. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +5586 -5232
  71. package/v2Containers/Zalo/index.js +0 -2
  72. 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) => ({ title: group?.groupName, value: group?.groupId })),
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) => ({ title: customer.name, value: customer?.userId })),
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
- setSelectedTestEntities(value);
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((testGroup) => testGroup.groupId === entityId);
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
- <CapRow className="test-preview-container">
3208
- <CapRow className="test-and-preview-panels">
3209
- {/* Left Panel */}
3210
- <CapRow className="left-panel">
3211
- {channel === CHANNELS.ZALO ? null : renderLeftPanelContent()}
3212
- <CapDivider className="panel-divider" />
3213
-
3214
- {/* Send Test Message Section */}
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
- </CapRow>
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
+ });