@capillarytech/creatives-library 8.0.125-alpha.6 → 8.0.126

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 (105) hide show
  1. package/config/app.js +6 -0
  2. package/containers/App/constants.js +0 -1
  3. package/containers/Email/index.js +5 -5
  4. package/containers/WeChat/RichmediaTemplates/Create/index.js +1 -1
  5. package/initialReducer.js +2 -0
  6. package/package.json +1 -1
  7. package/services/api.js +94 -1
  8. package/services/tests/api.test.js +191 -0
  9. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +3 -8
  10. package/tests/integration/TemplateCreation/api-response.js +0 -5
  11. package/tests/integration/TemplateCreation/msw-handler.js +63 -42
  12. package/utils/common.js +0 -7
  13. package/utils/commonUtils.js +6 -2
  14. package/v2Components/CapImageUpload/index.js +45 -51
  15. package/v2Components/CapInAppCTA/index.js +0 -1
  16. package/v2Components/CapTagList/index.js +120 -177
  17. package/v2Components/CapVideoUpload/constants.js +0 -3
  18. package/v2Components/CapVideoUpload/index.js +110 -167
  19. package/v2Components/CapVideoUpload/messages.js +0 -16
  20. package/v2Components/Carousel/index.js +13 -15
  21. package/v2Components/CustomerSearchSection/_customerSearch.scss +309 -0
  22. package/v2Components/CustomerSearchSection/constants.js +5 -0
  23. package/v2Components/CustomerSearchSection/index.js +362 -0
  24. package/v2Components/CustomerSearchSection/messages.js +20 -0
  25. package/v2Components/CustomerSearchSection/tests/utils.test.js +334 -0
  26. package/v2Components/CustomerSearchSection/utils.js +49 -0
  27. package/v2Components/ErrorInfoNote/style.scss +0 -1
  28. package/v2Components/MobilePushPreviewV2/index.js +5 -37
  29. package/v2Components/TemplatePreview/_templatePreview.scss +72 -114
  30. package/v2Components/TemplatePreview/index.js +50 -178
  31. package/v2Components/TemplatePreview/messages.js +0 -4
  32. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +543 -0
  33. package/v2Components/TestAndPreviewSlidebox/actions.js +67 -0
  34. package/v2Components/TestAndPreviewSlidebox/constants.js +67 -0
  35. package/v2Components/TestAndPreviewSlidebox/index.js +771 -0
  36. package/v2Components/TestAndPreviewSlidebox/messages.js +147 -0
  37. package/v2Components/TestAndPreviewSlidebox/reducer.js +233 -0
  38. package/v2Components/TestAndPreviewSlidebox/sagas.js +258 -0
  39. package/v2Components/TestAndPreviewSlidebox/selectors.js +142 -0
  40. package/v2Components/TestAndPreviewSlidebox/tests/actions.test.js +80 -0
  41. package/v2Components/TestAndPreviewSlidebox/tests/reducer.test.js +367 -0
  42. package/v2Components/TestAndPreviewSlidebox/tests/saga.rtl.test.js +192 -0
  43. package/v2Components/TestAndPreviewSlidebox/tests/saga.test.js +652 -0
  44. package/v2Components/TestAndPreviewSlidebox/tests/selector.test.js +182 -0
  45. package/v2Containers/CreativesContainer/SlideBoxContent.js +21 -9
  46. package/v2Containers/CreativesContainer/SlideBoxFooter.js +23 -2
  47. package/v2Containers/CreativesContainer/index.js +160 -195
  48. package/v2Containers/CreativesContainer/messages.js +4 -0
  49. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +21 -0
  50. package/v2Containers/Email/index.js +18 -6
  51. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +10 -0
  52. package/v2Containers/EmailWrapper/index.js +6 -0
  53. package/v2Containers/InApp/constants.js +0 -1
  54. package/v2Containers/InApp/index.js +13 -13
  55. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  56. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  57. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  58. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  59. package/v2Containers/MobilePush/Create/index.js +0 -1
  60. package/v2Containers/MobilePush/commonMethods.js +14 -7
  61. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +23 -5
  62. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  63. package/v2Containers/TagList/index.js +10 -56
  64. package/v2Containers/Templates/_templates.scss +1 -101
  65. package/v2Containers/Templates/index.js +35 -147
  66. package/v2Containers/Templates/messages.js +0 -8
  67. package/v2Containers/Templates/sagas.js +0 -2
  68. package/v2Containers/WeChat/RichmediaTemplates/Create/index.js +1 -1
  69. package/v2Containers/Whatsapp/constants.js +0 -1
  70. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +35 -0
  71. package/utils/createPayload.js +0 -270
  72. package/utils/tests/createPayload.test.js +0 -761
  73. package/v2Components/CapMpushCTA/constants.js +0 -25
  74. package/v2Components/CapMpushCTA/index.js +0 -332
  75. package/v2Components/CapMpushCTA/index.scss +0 -95
  76. package/v2Components/CapMpushCTA/messages.js +0 -89
  77. package/v2Components/TemplatePreview/assets/images/Android _ With date and time.svg +0 -29
  78. package/v2Components/TemplatePreview/assets/images/android.svg +0 -9
  79. package/v2Components/TemplatePreview/assets/images/iOS _ With date and time.svg +0 -26
  80. package/v2Components/TemplatePreview/assets/images/ios.svg +0 -9
  81. package/v2Containers/Email/tests/index.test.js +0 -35
  82. package/v2Containers/MobilePushNew/actions.js +0 -116
  83. package/v2Containers/MobilePushNew/components/CtaButtons.js +0 -170
  84. package/v2Containers/MobilePushNew/components/MediaUploaders.js +0 -686
  85. package/v2Containers/MobilePushNew/components/PlatformContentFields.js +0 -279
  86. package/v2Containers/MobilePushNew/components/index.js +0 -5
  87. package/v2Containers/MobilePushNew/components/tests/CtaButtons.test.js +0 -779
  88. package/v2Containers/MobilePushNew/components/tests/MediaUploaders.test.js +0 -2114
  89. package/v2Containers/MobilePushNew/components/tests/PlatformContentFields.test.js +0 -343
  90. package/v2Containers/MobilePushNew/constants.js +0 -115
  91. package/v2Containers/MobilePushNew/hooks/tests/usePlatformSync.test.js +0 -1299
  92. package/v2Containers/MobilePushNew/hooks/tests/useUpload.test.js +0 -1223
  93. package/v2Containers/MobilePushNew/hooks/usePlatformSync.js +0 -246
  94. package/v2Containers/MobilePushNew/hooks/useUpload.js +0 -709
  95. package/v2Containers/MobilePushNew/index.js +0 -2170
  96. package/v2Containers/MobilePushNew/index.scss +0 -308
  97. package/v2Containers/MobilePushNew/messages.js +0 -226
  98. package/v2Containers/MobilePushNew/reducer.js +0 -160
  99. package/v2Containers/MobilePushNew/sagas.js +0 -198
  100. package/v2Containers/MobilePushNew/selectors.js +0 -55
  101. package/v2Containers/MobilePushNew/tests/reducer.test.js +0 -741
  102. package/v2Containers/MobilePushNew/tests/sagas.test.js +0 -863
  103. package/v2Containers/MobilePushNew/tests/selectors.test.js +0 -425
  104. package/v2Containers/MobilePushNew/tests/utils.test.js +0 -322
  105. package/v2Containers/MobilePushNew/utils.js +0 -33
@@ -0,0 +1,771 @@
1
+ /**
2
+ *
3
+ * TestAndPreviewSlidebox
4
+ *
5
+ */
6
+
7
+ import PropTypes from 'prop-types';
8
+ import React, { useState, useEffect, useMemo } from 'react';
9
+ import { convert } from "html-to-text";
10
+ import { createStructuredSelector } from 'reselect';
11
+ import { connect } from 'react-redux';
12
+ import isEmpty from 'lodash/isEmpty';
13
+ import { FormattedMessage, injectIntl } from 'react-intl';
14
+ import { compose, bindActionCreators } from 'redux';
15
+ import CapSlideBox from '@capillarytech/cap-ui-library/CapSlideBox';
16
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
17
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
18
+ import CapSwitch from '@capillarytech/cap-ui-library/CapSwitch';
19
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
20
+ import CapInput from '@capillarytech/cap-ui-library/CapInput';
21
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
22
+ import CapHeader from '@capillarytech/cap-ui-library/CapHeader';
23
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
24
+ import CapDivider from '@capillarytech/cap-ui-library/CapDivider';
25
+ import CapStepsAccordian from '@capillarytech/cap-ui-library/CapStepsAccordian';
26
+ import CapTreeSelect from '@capillarytech/cap-ui-library/CapTreeSelect';
27
+ import CapInfoNote from '@capillarytech/cap-ui-library/CapInfoNote';
28
+ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
29
+ import { DAEMON } from '@capillarytech/vulcan-react-sdk/utils/sagaInjectorTypes';
30
+ import messages from './messages';
31
+ import injectReducer from '../../utils/injectReducer';
32
+ import injectSaga from '../../utils/injectSaga';
33
+ import * as previewAndTestActions from './actions';
34
+ import { previewAndTestSaga } from './sagas';
35
+ import './_testAndPreviewSlidebox.scss';
36
+ import reducer from './reducer';
37
+ import CustomerSearchSection from '../CustomerSearchSection';
38
+ import {
39
+ makeSelectPreviewAndTest,
40
+ makeSelectExtractedTags,
41
+ makeSelectIsExtractingTags,
42
+ makeSelectCustomers,
43
+ makeSelectIsSearchingCustomer,
44
+ makeSelectTags,
45
+ makeSelectPreviewData,
46
+ makeSelectIsUpdatingPreview,
47
+ makeSelectTestCustomers,
48
+ makeSelectIsFetchingTestCustomers,
49
+ makeSelectTestGroups,
50
+ makeSelectIsFetchingTestGroups,
51
+ makeSelectMessageMetaConfigId,
52
+ makeSelectPrefilledValues,
53
+ makeSelectIsSendingTestMessage,
54
+ } from './selectors';
55
+ import {
56
+ INITIAL_PAYLOAD, EMAIL, TEST, DESKTOP, ACTIVE, MOBILE,
57
+ } from './constants';
58
+ import { GLOBAL_CONVERT_OPTIONS } from '../FormBuilder/constants';
59
+
60
+ const TestAndPreviewSlidebox = (props) => {
61
+ const {
62
+ intl: { formatMessage },
63
+ show,
64
+ onClose,
65
+ formData,
66
+ content,
67
+ actions,
68
+ extractedTags,
69
+ isExtractingTags,
70
+ previewData,
71
+ isUpdatingPreview,
72
+ customers,
73
+ isSearchingCustomer,
74
+ testCustomers,
75
+ isFetchingTestCustomers,
76
+ testGroups,
77
+ isFetchingTestGroups,
78
+ messageMetaConfigId,
79
+ prefilledValues,
80
+ isSendingTestMessage,
81
+ } = props;
82
+
83
+ // State management
84
+ const [selectedCustomer, setSelectedCustomer] = useState(null);
85
+ const [requiredTags, setRequiredTags] = useState([]);
86
+ const [optionalTags, setOptionalTags] = useState([]);
87
+ const [customValues, setCustomValues] = useState({});
88
+ const [showJSON, setShowJSON] = useState(false);
89
+ const [tagsExtracted, setTagsExtracted] = useState(false);
90
+ const [previewDevice, setPreviewDevice] = useState('desktop');
91
+ const [previewDataHtml, setPreviewDataHtml] = useState(previewData || '');
92
+ const [selectedTestEntities, setSelectedTestEntities] = useState([]);
93
+
94
+ const isUpdatePreviewDisabled = useMemo(() => (
95
+ requiredTags.some((tag) => !customValues[tag.fullPath])
96
+ ), [requiredTags, customValues]);
97
+
98
+ useEffect(() => {
99
+ if (show) {
100
+ actions.fetchTestCustomersRequested();
101
+ actions.fetchTestGroupsRequested();
102
+ const payloadContent = convert(
103
+ content,
104
+ GLOBAL_CONVERT_OPTIONS
105
+ );
106
+ actions.extractTagsRequested(formData['template-subject'] || '', payloadContent);
107
+ }
108
+ }, [show]);
109
+
110
+ useEffect(() => {
111
+ if (previewData) {
112
+ setPreviewDataHtml(previewData);
113
+ }
114
+ }, [previewData]);
115
+
116
+ // Listen for extract tags API result
117
+ useEffect(() => {
118
+ if (extractedTags?.length > 0) {
119
+ // Categorize tags into required and optional
120
+ const required = [];
121
+ const optional = [];
122
+
123
+ const processTag = (tag, parentPath = '') => {
124
+ const currentPath = parentPath ? `${parentPath}.${tag.name}` : tag.name;
125
+
126
+ if (tag?.metaData?.userDriven === false) {
127
+ required.push({
128
+ ...tag,
129
+ fullPath: currentPath,
130
+ });
131
+ } else if (tag?.metaData?.userDriven === true) {
132
+ optional.push({
133
+ ...tag,
134
+ fullPath: currentPath,
135
+ });
136
+ }
137
+
138
+ if (tag?.children?.length > 0) {
139
+ tag.children.forEach((child) => processTag(child, currentPath));
140
+ }
141
+ };
142
+
143
+ extractedTags.forEach((tag) => processTag(tag));
144
+
145
+ setRequiredTags(required);
146
+ setOptionalTags(optional);
147
+
148
+ // Initialize custom values for required tags
149
+ const initialValues = {};
150
+ required.forEach((tag) => {
151
+ initialValues[tag.fullPath] = '';
152
+ });
153
+ optional.forEach((tag) => {
154
+ initialValues[tag.fullPath] = '';
155
+ });
156
+ setCustomValues(initialValues);
157
+ }
158
+ }, [extractedTags]);
159
+
160
+ useEffect(() => {
161
+ if (tagsExtracted && selectedCustomer) {
162
+ const userDrivenTags = optionalTags?.map((tag) => tag.name);
163
+ if (userDrivenTags?.length > 0) {
164
+ const payload = {
165
+ channel: EMAIL,
166
+ messageTitle: formData['template-subject'],
167
+ messageBody: content,
168
+ resolvedTags: customValues,
169
+ userId: selectedCustomer?.customerId,
170
+ };
171
+ actions.getPrefilledValuesRequested(payload);
172
+ }
173
+ }
174
+ }, [selectedCustomer, tagsExtracted]);
175
+
176
+ useEffect(() => {
177
+ if (prefilledValues && !isEmpty(prefilledValues)) {
178
+ setCustomValues((prev) => ({
179
+ ...prev,
180
+ ...prefilledValues,
181
+ }));
182
+ }
183
+ }, [JSON.stringify(prefilledValues)]);
184
+
185
+ const testEntitiesTreeData = useMemo(() => {
186
+ const groupsNode = {
187
+ title: 'Groups',
188
+ value: 'groups-node',
189
+ children: testGroups?.map((group) => ({ title: group?.groupName, value: group?.groupId })),
190
+ };
191
+
192
+ const customersNode = {
193
+ title: 'Individuals',
194
+ value: 'customers-node',
195
+ children: testCustomers?.map((customer) => ({ title: customer.name, value: customer?.userId })),
196
+ };
197
+
198
+ return [groupsNode, customersNode];
199
+ }, [testCustomers, testGroups]);
200
+
201
+ // Handler to close the slidebox
202
+ const handleClose = () => {
203
+ // Reset state when closing
204
+ setSelectedCustomer(null);
205
+ setRequiredTags([]);
206
+ setOptionalTags([]);
207
+ setCustomValues({});
208
+ setShowJSON(false);
209
+ setTagsExtracted(false);
210
+ setPreviewDevice('desktop');
211
+ setPreviewDataHtml('');
212
+ setSelectedTestEntities([]);
213
+ actions.clearPrefilledValues();
214
+ if (onClose) {
215
+ onClose();
216
+ }
217
+ };
218
+
219
+ const handleSearchCustomer = (query) => {
220
+ actions.searchCustomersRequested(query);
221
+ };
222
+
223
+ // Handle customer selection from CustomerSearchSection
224
+ const handleCustomerSelect = (customer) => {
225
+ setSelectedCustomer(customer);
226
+ setCustomValues((prev) => {
227
+ const newValues = { ...prev };
228
+ optionalTags.forEach((tag) => {
229
+ delete newValues[tag.fullPath];
230
+ });
231
+ return newValues;
232
+ });
233
+ };
234
+
235
+ const handleClearSelection = () => {
236
+ setSelectedCustomer(null);
237
+ setCustomValues((prev) => {
238
+ const newValues = { ...prev };
239
+ optionalTags.forEach((tag) => {
240
+ delete newValues[tag.fullPath];
241
+ });
242
+ return newValues;
243
+ });
244
+ };
245
+
246
+ // Handle custom value changes
247
+ const handleCustomValueChange = (tagPath, value) => {
248
+ setCustomValues((prev) => ({
249
+ ...prev,
250
+ [tagPath]: value?.trim(),
251
+ }));
252
+ };
253
+
254
+ // Handle JSON text change
255
+ const handleJSONTextChange = (value) => {
256
+ try {
257
+ const parsedJSON = JSON.parse(value);
258
+ setCustomValues(parsedJSON);
259
+ } catch (error) {
260
+ CapNotification.error({
261
+ message: formatMessage(messages.invalidJSON),
262
+ });
263
+ }
264
+ };
265
+
266
+ // Handle discard custom values
267
+ const handleDiscardCustomValues = () => {
268
+ const resetValues = {};
269
+ requiredTags.forEach((tag) => {
270
+ resetValues[tag?.fullPath] = '';
271
+ });
272
+ optionalTags.forEach((tag) => {
273
+ resetValues[tag?.fullPath] = '';
274
+ });
275
+ setCustomValues(resetValues);
276
+ };
277
+
278
+ // Handle update preview
279
+ const handleUpdatePreview = () => {
280
+ const payload = {
281
+ channel: EMAIL,
282
+ messageTitle: formData['template-subject'],
283
+ messageBody: content,
284
+ resolvedTags: customValues,
285
+ userId: selectedCustomer?.customerId,
286
+ };
287
+ actions.updatePreviewRequested(payload);
288
+ };
289
+
290
+ // Handle extract tags button click
291
+ const handleExtractTags = () => {
292
+ setTagsExtracted(true);
293
+ };
294
+
295
+ const handleTestEntitiesChange = (value) => {
296
+ setSelectedTestEntities(value);
297
+ };
298
+
299
+ const handleSendTestMessage = () => {
300
+ const allUserIds = [];
301
+ selectedTestEntities.forEach((entityId) => {
302
+ const group = testGroups.find((g) => g.groupId === entityId);
303
+ if (group) {
304
+ allUserIds.push(...group.userIds);
305
+ } else {
306
+ allUserIds.push(entityId);
307
+ }
308
+ });
309
+ const uniqueUserIds = [...new Set(allUserIds)];
310
+ actions.createMessageMetaRequested({
311
+ ...INITIAL_PAYLOAD,
312
+ emailMessageContent: {
313
+ channel: EMAIL,
314
+ messageBody: previewData?.resolvedBody || content,
315
+ messageSubject: previewData?.resolvedTitle || formData['template-subject'],
316
+ },
317
+ }, messageMetaConfigId, (response) => {
318
+ const payload = {
319
+ messageMetaConfigId: response?.entity || messageMetaConfigId,
320
+ recipientDetails: uniqueUserIds,
321
+ resolvedTagValues: customValues,
322
+ clientId: 434,
323
+ communicationType: TEST,
324
+ };
325
+ actions.sendTestMessageRequested(payload, (result) => {
326
+ if (result) {
327
+ CapNotification.success({
328
+ message: formatMessage(messages.testMessageSent),
329
+ });
330
+ } else {
331
+ CapNotification.error({
332
+ message: formatMessage(messages.testMessageFailed),
333
+ });
334
+ }
335
+ });
336
+ });
337
+ };
338
+
339
+ const renderLeftPanelContent = () => {
340
+ if (isExtractingTags) {
341
+ return (
342
+ <CapRow className="loading-container">
343
+ <CapSpin size="large" />
344
+ <CapRow className="loading-text">
345
+ <FormattedMessage {...messages.extractingTags} />
346
+ </CapRow>
347
+ </CapRow>
348
+ );
349
+ }
350
+ if (extractedTags?.length > 0) {
351
+ return (
352
+ <>
353
+ {/* Customer Search Section */}
354
+ <CapRow className="panel-section customer-section">
355
+ <CapHeader size="label1" title={<FormattedMessage {...messages.customerSearchTitle} />} />
356
+ <CustomerSearchSection
357
+ selectedCustomer={selectedCustomer}
358
+ onCustomerSelect={handleCustomerSelect}
359
+ onSearch={handleSearchCustomer}
360
+ customers={customers}
361
+ isSearchingCustomer={isSearchingCustomer}
362
+ onClearSelection={handleClearSelection}
363
+ />
364
+ </CapRow>
365
+ {/* Tags Section */}
366
+ {!tagsExtracted && (
367
+ <CapRow className="panel-section">
368
+ <CapButton type="flat" className="extract-tags-button" onClick={handleExtractTags}>
369
+ <CapLabel type="label33">
370
+ <FormattedMessage {...messages.enterCustomValuesForTags} />
371
+ </CapLabel>
372
+ </CapButton>
373
+ </CapRow>
374
+ )}
375
+ {tagsExtracted && renderCustomValuesEditor()}
376
+ </>
377
+ );
378
+ }
379
+ return (
380
+ <CapInfoNote
381
+ className="no-tags-extracted-info-note"
382
+ noteText={(
383
+ <CapLabel type="label31">
384
+ <FormattedMessage {...messages.noTagsExtracted} />
385
+ </CapLabel>
386
+ )}
387
+ />
388
+ );
389
+ };
390
+
391
+ // Header content for the slidebox
392
+ const slideboxHeader = (
393
+ <CapRow className="test-preview-header">
394
+ <CapIcon type="arrow-left" onClick={handleClose} className="back-icon" />
395
+ <CapHeader title={<FormattedMessage {...messages.testAndPreviewHeader} />} />
396
+ </CapRow>
397
+ );
398
+
399
+ // Render custom values editor table
400
+ const renderCustomValuesEditor = () => {
401
+ if (isExtractingTags) {
402
+ return (
403
+ <CapRow className="loading-container">
404
+ <CapSpin size="large" />
405
+ <CapRow className="loading-text">
406
+ <FormattedMessage {...messages.extractingTags} />
407
+ </CapRow>
408
+ </CapRow>
409
+ );
410
+ }
411
+
412
+ return (
413
+ <CapRow className="custom-values-editor">
414
+ {isUpdatePreviewDisabled && (
415
+ <CapLabel type="label16" className="values-missing-message">
416
+ <FormattedMessage {...messages.valuesMissing} />
417
+ </CapLabel>
418
+ )}
419
+ <CapRow className="editor-header">
420
+ <CapRow className="json-toggle">
421
+ <span className="toggle-label">
422
+ <FormattedMessage {...messages.showJSON} />
423
+ </span>
424
+ <CapSwitch
425
+ checked={showJSON}
426
+ onChange={setShowJSON}
427
+ size="small"
428
+ />
429
+ </CapRow>
430
+ </CapRow>
431
+ {showJSON ? (
432
+ <CapRow className="json-editor">
433
+ <CapRow className="json-editor-container">
434
+ <CapRow className="line-numbers">
435
+ {JSON.stringify(customValues, null, 2).split('\n').map((_, index) => (
436
+ <CapRow key={`line-${index + 1}`} className="line-number">
437
+ {index + 1}
438
+ </CapRow>
439
+ ))}
440
+ </CapRow>
441
+ <textarea
442
+ className="json-textarea"
443
+ value={JSON.stringify(customValues, null, 2)}
444
+ onChange={(e) => handleJSONTextChange(e.target.value)}
445
+ placeholder={formatMessage(messages.enterValue)}
446
+ rows={Math.max(10, JSON.stringify(customValues, null, 2).split('\n').length)}
447
+ spellCheck={false}
448
+ />
449
+ </CapRow>
450
+ </CapRow>
451
+ ) : (
452
+ <>
453
+ {extractedTags?.length > 0 && (
454
+ <CapRow className="values-table">
455
+ <CapRow className="table-header">
456
+ <CapLabel type="label31" className="header-cell">
457
+ <FormattedMessage {...messages.personalizationTags} />
458
+ </CapLabel>
459
+ <CapLabel type="label31" className="header-cell">
460
+ <FormattedMessage {...messages.customValues} />
461
+ </CapLabel>
462
+ </CapRow>
463
+ {requiredTags.map((tag) => (
464
+ <CapRow key={tag.fullPath} className="value-row">
465
+ <CapRow className="tag-name">
466
+ {tag.fullPath}
467
+ <span className="required-tag-indicator">*</span>
468
+ </CapRow>
469
+ <CapRow className="tag-input">
470
+ <CapInput
471
+ type="text"
472
+ isRequired
473
+ className="tag-input-field"
474
+ value={customValues[tag.fullPath] || ''}
475
+ onChange={(e) => handleCustomValueChange(tag.fullPath, e.target.value)}
476
+ placeholder={formatMessage(messages.enterValue)}
477
+ size="small"
478
+ />
479
+ </CapRow>
480
+ </CapRow>
481
+ ))}
482
+ {optionalTags.map((tag) => (
483
+ <CapRow key={tag.fullPath} className="value-row">
484
+ <CapRow className="tag-name">{tag.fullPath}</CapRow>
485
+ <CapRow className="tag-input">
486
+ <CapInput
487
+ type="text"
488
+ className="tag-input-field"
489
+ value={customValues[tag.fullPath] || ''}
490
+ onChange={(e) => handleCustomValueChange(tag.fullPath, e.target.value)}
491
+ placeholder={formatMessage(messages.enterValue)}
492
+ size="small"
493
+ />
494
+ </CapRow>
495
+ </CapRow>
496
+ ))}
497
+ </CapRow>
498
+ )}
499
+ </>
500
+ )}
501
+ <div className="editor-actions">
502
+ <CapButton
503
+ className="discard-button"
504
+ type="flat"
505
+ size="small"
506
+ onClick={handleDiscardCustomValues}
507
+ icon="close"
508
+ >
509
+ <FormattedMessage {...messages.discardCustomValues} />
510
+ </CapButton>
511
+ <CapButton
512
+ type="primary"
513
+ size="small"
514
+ onClick={handleUpdatePreview}
515
+ loading={isUpdatingPreview}
516
+ disabled={isUpdatePreviewDisabled}
517
+ >
518
+ <FormattedMessage {...messages.updatePreview} />
519
+ </CapButton>
520
+ </div>
521
+ </CapRow>
522
+ );
523
+ };
524
+
525
+ // Render send test message section
526
+ const renderSendTestMessage = () => (
527
+ <CapStepsAccordian
528
+ showNumberSteps={false}
529
+ isChevronIcon
530
+ expandIconPosition="right"
531
+ items={[
532
+ {
533
+ header: <CapHeader
534
+ size="regular"
535
+ description={<FormattedMessage {...messages.testMessageDescription} />}
536
+ title={<FormattedMessage {...messages.sendTestMessage} />}
537
+ />,
538
+ content: (
539
+ <CapRow className="send-test-content">
540
+ <CapHeader size="label1" title={<FormattedMessage {...messages.testCustomers} />} />
541
+ <CapTreeSelect
542
+ className="test-customers-tree-select"
543
+ loading={isFetchingTestCustomers || isFetchingTestGroups}
544
+ treeData={testEntitiesTreeData}
545
+ onChange={handleTestEntitiesChange}
546
+ value={selectedTestEntities}
547
+ multiple
548
+ placeholder={formatMessage(messages.testCustomersPlaceholder)}
549
+ />
550
+ <CapButton onClick={handleSendTestMessage} disabled={isEmpty(selectedTestEntities) || isEmpty(formData['template-subject']) || isSendingTestMessage}>
551
+ <FormattedMessage {...messages.sendTestButton} />
552
+ </CapButton>
553
+ </CapRow>),
554
+ key: 1,
555
+ },
556
+ ]}
557
+ />
558
+ );
559
+
560
+ const renderPreview = () => (
561
+ <CapRow className="preview-section panel-section">
562
+ <PreviewChrome
563
+ device={previewDevice}
564
+ onDeviceChange={setPreviewDevice}
565
+ customer={selectedCustomer}
566
+ subject={formData['template-subject']}
567
+ >
568
+ {isUpdatingPreview && (
569
+ <CapRow className="loading-container">
570
+ <CapSpin />
571
+ <CapRow className="loading-text">{formatMessage(messages.updatingPreview)}</CapRow>
572
+ </CapRow>
573
+ )}
574
+ {!isUpdatingPreview && previewDataHtml && (
575
+ <iframe
576
+ srcDoc={previewDataHtml?.resolvedBody}
577
+ title="Email Preview"
578
+ width="100%"
579
+ height="100%"
580
+ frameBorder="0"
581
+ />
582
+ )}
583
+ {!isUpdatingPreview && !previewDataHtml && (
584
+ <iframe
585
+ srcDoc={content}
586
+ title="Email Preview"
587
+ width="100%"
588
+ height="100%"
589
+ frameBorder="0" />
590
+ )}
591
+ </PreviewChrome>
592
+ </CapRow>
593
+ );
594
+
595
+ return (
596
+ <CapSlideBox
597
+ className="test-and-preview-slidebox"
598
+ header={slideboxHeader}
599
+ handleClose={handleClose}
600
+ show={show}
601
+ size="size-xl"
602
+ content={(
603
+ <CapRow className="test-preview-container">
604
+ {(
605
+ <CapRow className="test-and-preview-panels">
606
+ {/* Left Panel */}
607
+ <CapRow className="left-panel">
608
+ {renderLeftPanelContent()}
609
+ <CapDivider className="panel-divider" />
610
+ {/* Send Test Message Section */}
611
+ {(
612
+ <CapRow className="panel-section send-test-section">
613
+ {renderSendTestMessage()}
614
+ </CapRow>
615
+ )}
616
+ <CapDivider className="panel-divider" />
617
+ </CapRow>
618
+
619
+ {/* Right Panel - Email Preview */}
620
+ <CapRow className="right-panel">
621
+ {renderPreview()}
622
+ </CapRow>
623
+ </CapRow>
624
+ )}
625
+ </CapRow>
626
+ )}
627
+ />
628
+ );
629
+ };
630
+
631
+ const PreviewChrome = ({
632
+ device, onDeviceChange, subject, customer, children,
633
+ }) => (
634
+ <CapRow className="preview-chrome">
635
+ <div className="preview-header">
636
+ <CapRow type="flex" className="preview-for">
637
+ <CapLabel type="label1">
638
+ <FormattedMessage {...messages.previewFor} />
639
+ </CapLabel>
640
+ <CapLabel type="label2">
641
+ {customer ? customer.name : <FormattedMessage {...messages.defaultPreview} />}
642
+ </CapLabel>
643
+ </CapRow>
644
+ <CapRow className="device-toggle">
645
+ <CapIcon
646
+ type="desktop"
647
+ size="s"
648
+ className={device === DESKTOP ? ACTIVE : ''}
649
+ onClick={() => onDeviceChange('desktop')} />
650
+ <CapIcon
651
+ type="mobile"
652
+ className={device === MOBILE ? ACTIVE : ''}
653
+ onClick={() => onDeviceChange('mobile')} />
654
+ </CapRow>
655
+ </div>
656
+ <CapRow className={`preview-body ${device}`}>
657
+ <CapRow className="browser-bar">
658
+ <CapRow className="browser-controls">
659
+ <CapIcon type="chevron-left" />
660
+ <CapIcon type="chevron-right" />
661
+ <CapIcon className="refresh-icon" type="refreshCircle" size="s" />
662
+ </CapRow>
663
+ <CapRow className="address-bar">
664
+ <CapLabel type="label3" className="address-bar-label">
665
+ <FormattedMessage {...messages.browserAddressBar} />
666
+ </CapLabel>
667
+ <CapIcon className="browser-address-bar-icon" type="star" size="s" />
668
+ </CapRow>
669
+ </CapRow>
670
+ <CapDivider className="preview-divider" />
671
+ <CapRow className="email-header">
672
+ <CapIcon type="arrow-left" className="back-arrow" />
673
+ <CapLabel type="label17" className="email-subject">{subject}</CapLabel>
674
+ <CapRow className="email-meta">
675
+ <span><FormattedMessage {...messages.timeAgo} /></span>
676
+ <CapRow className="dots" />
677
+ <CapRow className="dots" />
678
+ <CapRow className="dots" />
679
+ </CapRow>
680
+ </CapRow>
681
+ <CapRow className="email-from">
682
+ <CapRow className="sender-avatar" />
683
+ <CapRow className="sender-info">
684
+ <CapRow className="sender-name"><FormattedMessage {...messages.senderName} /></CapRow>
685
+ <CapRow className="recipient-info">to me</CapRow>
686
+ </CapRow>
687
+ </CapRow>
688
+ <CapRow className="email-content">
689
+ {children}
690
+ </CapRow>
691
+ </CapRow>
692
+ </CapRow>
693
+ );
694
+
695
+ PreviewChrome.propTypes = {
696
+ device: PropTypes.string.isRequired,
697
+ onDeviceChange: PropTypes.func.isRequired,
698
+ subject: PropTypes.string,
699
+ customer: PropTypes.object.isRequired,
700
+ children: PropTypes.node.isRequired,
701
+ };
702
+
703
+ PreviewChrome.defaultProps = {
704
+ subject: 'Welcome email',
705
+ };
706
+
707
+ TestAndPreviewSlidebox.propTypes = {
708
+ show: PropTypes.bool.isRequired,
709
+ onClose: PropTypes.func.isRequired,
710
+ formData: PropTypes.object,
711
+ currentChannel: PropTypes.string,
712
+ actions: PropTypes.object.isRequired,
713
+ previewData: PropTypes.object.isRequired,
714
+ isUpdatingPreview: PropTypes.bool.isRequired,
715
+ content: PropTypes.string.isRequired,
716
+ extractedTags: PropTypes.array.isRequired,
717
+ isExtractingTags: PropTypes.bool.isRequired,
718
+ customers: PropTypes.array.isRequired,
719
+ isSearchingCustomer: PropTypes.bool.isRequired,
720
+ intl: PropTypes.object.isRequired,
721
+ testCustomers: PropTypes.array.isRequired,
722
+ isFetchingTestCustomers: PropTypes.bool.isRequired,
723
+ testGroups: PropTypes.array.isRequired,
724
+ isFetchingTestGroups: PropTypes.bool.isRequired,
725
+ messageMetaConfigId: PropTypes.string,
726
+ prefilledValues: PropTypes.object,
727
+ isSendingTestMessage: PropTypes.bool.isRequired,
728
+ };
729
+
730
+ TestAndPreviewSlidebox.defaultProps = {
731
+ formData: null,
732
+ currentChannel: EMAIL,
733
+ messageMetaConfigId: null,
734
+ prefilledValues: {},
735
+ };
736
+
737
+ // Redux connection
738
+ const mapStateToProps = createStructuredSelector({
739
+ previewAndTest: makeSelectPreviewAndTest(),
740
+ extractedTags: makeSelectExtractedTags(),
741
+ isExtractingTags: makeSelectIsExtractingTags(),
742
+ customers: makeSelectCustomers(),
743
+ tags: makeSelectTags(),
744
+ previewData: makeSelectPreviewData(),
745
+ isUpdatingPreview: makeSelectIsUpdatingPreview(),
746
+ isSearchingCustomer: makeSelectIsSearchingCustomer(),
747
+ testCustomers: makeSelectTestCustomers(),
748
+ isFetchingTestCustomers: makeSelectIsFetchingTestCustomers(),
749
+ testGroups: makeSelectTestGroups(),
750
+ isFetchingTestGroups: makeSelectIsFetchingTestGroups(),
751
+ messageMetaConfigId: makeSelectMessageMetaConfigId(),
752
+ prefilledValues: makeSelectPrefilledValues(),
753
+ isSendingTestMessage: makeSelectIsSendingTestMessage(),
754
+ });
755
+
756
+ function mapDispatchToProps(dispatch) {
757
+ return {
758
+ actions: bindActionCreators(previewAndTestActions, dispatch),
759
+ };
760
+ }
761
+
762
+ const withReducer = injectReducer({ key: 'previewAndTest', reducer });
763
+ const withSaga = injectSaga({ key: 'previewAndTest', saga: previewAndTestSaga, mode: DAEMON });
764
+ const withConnect = connect(mapStateToProps, mapDispatchToProps);
765
+
766
+ export default compose(
767
+ withReducer,
768
+ withSaga,
769
+ withConnect,
770
+ injectIntl,
771
+ )(TestAndPreviewSlidebox);