@capillarytech/creatives-library 9.0.73-alpha.sprint-testpreview.22 → 9.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +58 -1
- package/v2Components/CapTagListWithInput/index.js +64 -35
- package/v2Components/CapTagListWithInput/index.scss +9 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/EmailPreviewContent.js +8 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +7 -0
- package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -0
- package/v2Components/CommonTestAndPreview/actions.js +1 -1
- package/v2Components/CommonTestAndPreview/index.js +36 -75
- package/v2Components/CommonTestAndPreview/sagas.js +2 -1
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/EmailPreviewContent.test.js +27 -0
- package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +40 -0
- package/v2Components/CommonTestAndPreview/tests/index.test.js +92 -97
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
- package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -66
- package/v2Components/CommonTestAndPreview/utils.js +0 -14
- package/v2Components/EmailMobilePreview/index.js +8 -1
- package/v2Components/EmailMobilePreview/index.scss +12 -3
- package/v2Components/EmailMobilePreview/tests/index.test.js +31 -0
- package/v2Components/EmailPreviewV2/_emailPreviewV2.scss +37 -5
- package/v2Components/EmailPreviewV2/index.js +18 -6
- package/v2Components/EmailPreviewV2/messages.js +4 -0
- package/v2Components/EmailPreviewV2/tests/index.test.js +43 -0
- package/v2Components/FormBuilder/Classic.js +134 -14
- package/v2Components/FormBuilder/Functional/FormBuilderShell.js +9 -0
- package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +11 -12
- package/v2Components/FormBuilder/Functional/tests/mpush.singlePlatform.test.js +144 -0
- package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +17 -5
- package/v2Components/FormBuilder/_formBuilder.scss +21 -0
- package/v2Components/FormBuilder/messages.js +12 -0
- package/v2Components/FormBuilder/tests/mpush.characterization.test.js +45 -0
- package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
- package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
- package/v2Containers/CommunicationFlow/constants.js +1 -0
- package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
- package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
- package/v2Containers/CreativesContainer/index.js +7 -1
- package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
- package/v2Containers/Email/index.js +31 -4
- package/v2Containers/Email/initialSchema.js +30 -0
- package/v2Containers/Email/messages.js +8 -0
- package/v2Containers/Email/tests/index.test.js +107 -0
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +172 -8
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
- package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
- package/v2Containers/EmailWrapper/constants.js +5 -0
- package/v2Containers/EmailWrapper/messages.js +24 -0
- package/v2Containers/MobilePush/Create/constants.js +1 -0
- package/v2Containers/MobilePush/Create/index.js +2 -2
- package/v2Containers/MobilePush/Create/test/contentValidity.test.js +58 -0
- package/v2Containers/MobilePush/Edit/index.js +2 -2
- package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +17 -0
- package/v2Containers/MobilePush/commonMethods.js +7 -8
- package/v2Containers/Rcs/index.js +1 -27
- package/v2Containers/TagList/index.js +7 -0
- package/v2Containers/TagList/tests/TagList.test.js +42 -0
- package/v2Containers/Templates/index.js +4 -9
- package/v2Containers/Templates/tests/index.test.js +35 -107
- package/v2Containers/Viber/index.scss +31 -0
- package/v2Containers/Whatsapp/index.js +1 -18
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +2 -2
- package/v2Containers/Zalo/index.js +0 -18
- package/v2Containers/Rcs/tests/testAndPreviewFromListing.test.js +0 -132
- package/v2Containers/Whatsapp/tests/testAndPreviewFromListing.test.js +0 -91
- package/v2Containers/Zalo/tests/testAndPreviewFromListing.test.js +0 -98
|
@@ -6,7 +6,6 @@ import { injectIntl, FormattedMessage } from 'react-intl';
|
|
|
6
6
|
import isEmpty from 'lodash/isEmpty';
|
|
7
7
|
import get from 'lodash/get';
|
|
8
8
|
import _ from 'lodash';
|
|
9
|
-
import { CAP_SPACE_16 } from '@capillarytech/cap-ui-library/styled/variables';
|
|
10
9
|
import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
|
|
11
10
|
import CapRow from '@capillarytech/cap-ui-library/CapRow';
|
|
12
11
|
import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
|
|
@@ -15,6 +14,9 @@ import CapDropdown from '@capillarytech/cap-ui-library/CapDropdown';
|
|
|
15
14
|
import CapMenu from '@capillarytech/cap-ui-library/CapMenu';
|
|
16
15
|
import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
|
|
17
16
|
import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
17
|
+
import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
|
|
18
|
+
import CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
19
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
18
20
|
import HTMLEditor from '../../../v2Components/HtmlEditor';
|
|
19
21
|
import CapTagListWithInput from '../../../v2Components/CapTagListWithInput';
|
|
20
22
|
import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
|
|
@@ -24,14 +26,17 @@ import { downloadHtmlFile, sanitizeFileName, getExportHtmlDisabledTooltip } from
|
|
|
24
26
|
import history from '../../../utils/history';
|
|
25
27
|
import messages from '../messages';
|
|
26
28
|
import emailMessages from '../../Email/messages';
|
|
27
|
-
import { validateTags } from '../../../utils/tagValidations';
|
|
29
|
+
import { validateTags, hasUnsubscribeTag } from '../../../utils/tagValidations';
|
|
28
30
|
import {
|
|
29
31
|
TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
|
|
30
32
|
} from '../../Whatsapp/constants';
|
|
31
33
|
import { EMAIL } from '../../CreativesContainer/constants';
|
|
32
34
|
import { OUTBOUND } from '../../../v2Components/FormBuilder/constants';
|
|
35
|
+
import { PREHEADER_EXCLUDED_TAGS } from '../constants';
|
|
33
36
|
import './_emailHTMLEditor.scss';
|
|
34
37
|
|
|
38
|
+
const { CapLabelInline } = CapLabel;
|
|
39
|
+
|
|
35
40
|
/**
|
|
36
41
|
* EmailHTMLEditor Component
|
|
37
42
|
*
|
|
@@ -111,6 +116,17 @@ const EmailHTMLEditor = (props) => {
|
|
|
111
116
|
const [savedTemplate, setSavedTemplate] = useState(null);
|
|
112
117
|
const [subject, setSubject] = useState('');
|
|
113
118
|
const [subjectError, setSubjectError] = useState('');
|
|
119
|
+
const [preheader, setPreheader] = useState('');
|
|
120
|
+
const [preheaderError, setPreheaderError] = useState('');
|
|
121
|
+
// null = show if content exists; true/false = manual "+ Pre-header"/"Remove" click.
|
|
122
|
+
const [preheaderRevealed, setPreheaderRevealed] = useState(null);
|
|
123
|
+
const isPreheaderRevealed = preheaderRevealed !== null ? preheaderRevealed : !!preheader;
|
|
124
|
+
const handleShowPreheaderClick = useCallback(() => setPreheaderRevealed(true), []);
|
|
125
|
+
const handleRemovePreheaderClick = useCallback(() => {
|
|
126
|
+
setPreheader('');
|
|
127
|
+
setPreheaderError('');
|
|
128
|
+
setPreheaderRevealed(false);
|
|
129
|
+
}, []);
|
|
114
130
|
// State for template name (extracted from template data in Edit mode)
|
|
115
131
|
const [extractedTemplateName, setExtractedTemplateName] = useState('');
|
|
116
132
|
const [tagValidationError, setTagValidationError] = useState(null);
|
|
@@ -183,6 +199,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
183
199
|
base: true,
|
|
184
200
|
},
|
|
185
201
|
'template-subject': subject || '',
|
|
202
|
+
'template-preheader': preheader || '',
|
|
186
203
|
};
|
|
187
204
|
},
|
|
188
205
|
getContentForPreview: () => htmlContent || '',
|
|
@@ -206,7 +223,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
206
223
|
html: 0, label: 0, liquid: 0, total: 0,
|
|
207
224
|
};
|
|
208
225
|
},
|
|
209
|
-
}), [htmlContent, subject, currentOrgDetails]);
|
|
226
|
+
}), [htmlContent, subject, preheader, currentOrgDetails]);
|
|
210
227
|
|
|
211
228
|
// Detect edit mode: when isEditEmail is false (create flow), never treat as edit or fetch template details
|
|
212
229
|
const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
|
|
@@ -296,6 +313,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
296
313
|
lastTemplateIdRef.current = currentTemplateId;
|
|
297
314
|
setHtmlContent('');
|
|
298
315
|
setSubject('');
|
|
316
|
+
setPreheader('');
|
|
299
317
|
setExtractedTemplateName('');
|
|
300
318
|
baselineContentRef.current = null;
|
|
301
319
|
setHasContentChanged(false);
|
|
@@ -324,11 +342,13 @@ const EmailHTMLEditor = (props) => {
|
|
|
324
342
|
if (hasCompleteTemplateData && !contentInitializedRef.current) {
|
|
325
343
|
let extractedContent = '';
|
|
326
344
|
let extractedSubject = '';
|
|
345
|
+
let extractedPreheader = '';
|
|
327
346
|
let extractedName = '';
|
|
328
347
|
|
|
329
348
|
if (templateDataProp.base) {
|
|
330
349
|
extractedContent = templateDataProp.base['template-content'] || templateDataProp.base.html_content || templateDataProp.base.emailBody || '';
|
|
331
350
|
extractedSubject = templateDataProp.base.subject || templateDataProp.base.emailSubject || '';
|
|
351
|
+
extractedPreheader = templateDataProp.base.preheader || '';
|
|
332
352
|
} else if (templateDataProp.versions && templateDataProp.versions.base) {
|
|
333
353
|
const { base } = templateDataProp.versions;
|
|
334
354
|
const activeTab = base.activeTab || 'en';
|
|
@@ -341,9 +361,11 @@ const EmailHTMLEditor = (props) => {
|
|
|
341
361
|
|| '';
|
|
342
362
|
|
|
343
363
|
extractedSubject = base.subject || base.emailSubject || '';
|
|
364
|
+
extractedPreheader = base.preheader || '';
|
|
344
365
|
} else {
|
|
345
366
|
extractedContent = templateDataProp['template-content'] || templateDataProp.emailBody || templateDataProp.html_content || '';
|
|
346
367
|
extractedSubject = templateDataProp.emailSubject || templateDataProp.subject || '';
|
|
368
|
+
extractedPreheader = templateDataProp.preheader || '';
|
|
347
369
|
}
|
|
348
370
|
|
|
349
371
|
// Extract template name from templateDataProp
|
|
@@ -354,6 +376,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
354
376
|
setHtmlContent(extractedContent);
|
|
355
377
|
setLoadedHtmlContent(extractedContent);
|
|
356
378
|
setSubject(extractedSubject);
|
|
379
|
+
setPreheader(extractedPreheader);
|
|
357
380
|
setExtractedTemplateName(extractedName);
|
|
358
381
|
contentInitializedRef.current = true;
|
|
359
382
|
subjectInitializedRef.current = true;
|
|
@@ -425,6 +448,11 @@ const EmailHTMLEditor = (props) => {
|
|
|
425
448
|
|| get(templateDataFromRedux, 'versions.base.subject')
|
|
426
449
|
|| '';
|
|
427
450
|
|
|
451
|
+
const extractedPreheader = baseData.preheader
|
|
452
|
+
|| get(templateDataFromRedux, 'preheader')
|
|
453
|
+
|| get(templateDataFromRedux, 'versions.base.preheader')
|
|
454
|
+
|| '';
|
|
455
|
+
|
|
428
456
|
// Extract template name from Redux data
|
|
429
457
|
const extractedName = templateDataFromRedux.name || '';
|
|
430
458
|
|
|
@@ -438,6 +466,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
438
466
|
if (!contentInitializedRef.current || htmlContent === loadedHtmlContent) {
|
|
439
467
|
setHtmlContent(extractedContent);
|
|
440
468
|
setSubject(extractedSubject);
|
|
469
|
+
setPreheader(extractedPreheader);
|
|
441
470
|
setExtractedTemplateName(extractedName);
|
|
442
471
|
}
|
|
443
472
|
} else if (!contentInitializedRef.current) {
|
|
@@ -445,6 +474,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
445
474
|
setHtmlContent(extractedContent);
|
|
446
475
|
setLoadedHtmlContent(extractedContent);
|
|
447
476
|
setSubject(extractedSubject);
|
|
477
|
+
setPreheader(extractedPreheader);
|
|
448
478
|
setExtractedTemplateName(extractedName);
|
|
449
479
|
}
|
|
450
480
|
|
|
@@ -470,6 +500,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
470
500
|
setHtmlContent('');
|
|
471
501
|
setLoadedHtmlContent(''); // Set stable loaded content
|
|
472
502
|
setSubject('');
|
|
503
|
+
setPreheader('');
|
|
473
504
|
setExtractedTemplateName('');
|
|
474
505
|
setIsLoading(false);
|
|
475
506
|
if (setIsLoadingContent) {
|
|
@@ -676,6 +707,53 @@ const EmailHTMLEditor = (props) => {
|
|
|
676
707
|
}
|
|
677
708
|
}, [subjectError]);
|
|
678
709
|
|
|
710
|
+
// Handle tag insertion into Pre-header field
|
|
711
|
+
const handlePreheaderTagSelect = useCallback((data) => {
|
|
712
|
+
if (data) {
|
|
713
|
+
const tagToInsert = `{{${data}}}`;
|
|
714
|
+
const input = document.getElementById('template-preheader') || document.querySelector('#template-preheader input');
|
|
715
|
+
let preheaderValue = preheader || '';
|
|
716
|
+
try {
|
|
717
|
+
if (input && (typeof input.selectionStart === 'number')) {
|
|
718
|
+
const startPos = input.selectionStart;
|
|
719
|
+
const endPos = input.selectionEnd;
|
|
720
|
+
preheaderValue = `${preheaderValue.substring(0, startPos)}${tagToInsert}${preheaderValue.substring(endPos)}`;
|
|
721
|
+
setPreheader(preheaderValue);
|
|
722
|
+
try {
|
|
723
|
+
input.focus();
|
|
724
|
+
const newPos = startPos + tagToInsert.length;
|
|
725
|
+
input.selectionStart = newPos;
|
|
726
|
+
input.selectionEnd = newPos;
|
|
727
|
+
} catch (e) {
|
|
728
|
+
// Ignore focus errors
|
|
729
|
+
}
|
|
730
|
+
} else {
|
|
731
|
+
preheaderValue = `${preheaderValue}${tagToInsert}`;
|
|
732
|
+
setPreheader(preheaderValue);
|
|
733
|
+
if (input) {
|
|
734
|
+
try {
|
|
735
|
+
input.value = preheaderValue;
|
|
736
|
+
} catch (e) {
|
|
737
|
+
// Ignore value setting errors
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
} catch (e) {
|
|
742
|
+
// Fallback: safe append
|
|
743
|
+
preheaderValue = `${preheaderValue}${tagToInsert}`;
|
|
744
|
+
setPreheader(preheaderValue);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}, [preheader]);
|
|
748
|
+
|
|
749
|
+
// Handle pre-header change
|
|
750
|
+
const handlePreheaderChange = useCallback((e) => {
|
|
751
|
+
setPreheader(e.target.value);
|
|
752
|
+
if (preheaderError) {
|
|
753
|
+
setPreheaderError('');
|
|
754
|
+
}
|
|
755
|
+
}, [preheaderError]);
|
|
756
|
+
|
|
679
757
|
// Handle Save/Update with liquid validation
|
|
680
758
|
const handleSave = useCallback(() => {
|
|
681
759
|
// IMPORTANT: Clear API validation errors FIRST before checking for validation errors
|
|
@@ -704,6 +782,15 @@ const EmailHTMLEditor = (props) => {
|
|
|
704
782
|
setSubjectError('');
|
|
705
783
|
}
|
|
706
784
|
|
|
785
|
+
// 1.1. Validate Pre-header - BLOCKING: must never contain the unsubscribe tag
|
|
786
|
+
if (hasUnsubscribeTag(preheader)) {
|
|
787
|
+
setPreheaderError(formatMessage(messages.unsubscribeNotAllowedInPreheader));
|
|
788
|
+
if (onValidationFail) {
|
|
789
|
+
onValidationFail();
|
|
790
|
+
}
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
|
|
707
794
|
// 1.5. Check for validation errors (Errors = blocking, Warnings = non-blocking)
|
|
708
795
|
// Get issue counts from ref or stored state
|
|
709
796
|
let issueCounts = { errors: 0, warnings: 0, total: 0 };
|
|
@@ -835,6 +922,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
835
922
|
base: true,
|
|
836
923
|
tabKey,
|
|
837
924
|
subject: subject || '',
|
|
925
|
+
preheader: preheader || '',
|
|
838
926
|
};
|
|
839
927
|
|
|
840
928
|
const historyEntry = {
|
|
@@ -844,6 +932,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
844
932
|
base: true,
|
|
845
933
|
tabKey,
|
|
846
934
|
subject: subject || '',
|
|
935
|
+
preheader: preheader || '',
|
|
847
936
|
};
|
|
848
937
|
|
|
849
938
|
// Opened in edit mode: name from the loaded template; created in this session: the name entered for it
|
|
@@ -944,6 +1033,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
944
1033
|
secondary_templates: [{ template_data: tmpData }],
|
|
945
1034
|
};
|
|
946
1035
|
libraryPayload.base.subject = subject || '';
|
|
1036
|
+
libraryPayload.base.preheader = preheader || '';
|
|
947
1037
|
|
|
948
1038
|
if (location?.query?.module === 'library' && isGetFormData && getFormdata) {
|
|
949
1039
|
const response = {
|
|
@@ -1006,6 +1096,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1006
1096
|
}
|
|
1007
1097
|
}, [
|
|
1008
1098
|
subject,
|
|
1099
|
+
preheader,
|
|
1009
1100
|
htmlContent,
|
|
1010
1101
|
tags,
|
|
1011
1102
|
injectedTags,
|
|
@@ -1142,7 +1233,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1142
1233
|
<CapRow>
|
|
1143
1234
|
<CapColumn span={24}>
|
|
1144
1235
|
{/* Subject Field */}
|
|
1145
|
-
<CapColumn span={24}
|
|
1236
|
+
<CapColumn span={24} className="email-html-editor-field">
|
|
1146
1237
|
<CapTagListWithInput
|
|
1147
1238
|
inputId="template-subject"
|
|
1148
1239
|
inputValue={subject}
|
|
@@ -1150,9 +1241,13 @@ const EmailHTMLEditor = (props) => {
|
|
|
1150
1241
|
inputPlaceholder={formatMessage(messages.enterEmailSubject)}
|
|
1151
1242
|
inputRequired
|
|
1152
1243
|
inputErrorMessage={subjectError}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1244
|
+
inputProps={{
|
|
1245
|
+
prefix: (
|
|
1246
|
+
<CapLabelInline type="label2" className="email-html-editor-prefix email-html-editor-prefix-label">
|
|
1247
|
+
{formatMessage(messages.subject)}
|
|
1248
|
+
</CapLabelInline>
|
|
1249
|
+
),
|
|
1250
|
+
}}
|
|
1156
1251
|
onTagSelect={handleSubjectTagSelect}
|
|
1157
1252
|
onContextChange={handleOnTagsContextChange}
|
|
1158
1253
|
location={location}
|
|
@@ -1161,9 +1256,77 @@ const EmailHTMLEditor = (props) => {
|
|
|
1161
1256
|
selectedOfferDetails={selectedOfferDetails}
|
|
1162
1257
|
eventContextTags={eventContextTags}
|
|
1163
1258
|
waitEventContextTags={waitEventContextTags}
|
|
1164
|
-
showHeading
|
|
1259
|
+
showHeading={false}
|
|
1260
|
+
showTagList
|
|
1261
|
+
showInput
|
|
1262
|
+
singleLineLayout
|
|
1263
|
+
extraContent={!isPreheaderRevealed ? (
|
|
1264
|
+
<CapButton
|
|
1265
|
+
type="dashed"
|
|
1266
|
+
size="small"
|
|
1267
|
+
className="email-html-editor-add-preheader-btn"
|
|
1268
|
+
onClick={handleShowPreheaderClick}
|
|
1269
|
+
>
|
|
1270
|
+
{formatMessage(messages.addPreheader)}
|
|
1271
|
+
</CapButton>
|
|
1272
|
+
) : null}
|
|
1273
|
+
containerStyle={{
|
|
1274
|
+
display: 'flex',
|
|
1275
|
+
flexDirection: 'column',
|
|
1276
|
+
}}
|
|
1277
|
+
popoverPlacement="bottomRight"
|
|
1278
|
+
/>
|
|
1279
|
+
</CapColumn>
|
|
1280
|
+
|
|
1281
|
+
{/* Pre-header Field */}
|
|
1282
|
+
{isPreheaderRevealed && (
|
|
1283
|
+
<CapColumn span={24} className="email-html-editor-field">
|
|
1284
|
+
<CapTagListWithInput
|
|
1285
|
+
inputId="template-preheader"
|
|
1286
|
+
inputValue={preheader}
|
|
1287
|
+
inputOnChange={handlePreheaderChange}
|
|
1288
|
+
inputPlaceholder={formatMessage(messages.enterEmailPreheader)}
|
|
1289
|
+
inputMaxLength={85}
|
|
1290
|
+
inputErrorMessage={preheaderError}
|
|
1291
|
+
excludeTags={PREHEADER_EXCLUDED_TAGS}
|
|
1292
|
+
inputProps={{
|
|
1293
|
+
prefix: (
|
|
1294
|
+
<CapRow align="middle" type="flex" noWrap gap={4} className="email-html-editor-prefix">
|
|
1295
|
+
<CapLabelInline type="label2" className="email-html-editor-prefix-label">
|
|
1296
|
+
{formatMessage(messages.preheader)}
|
|
1297
|
+
</CapLabelInline>
|
|
1298
|
+
<CapTooltipWithInfo title={formatMessage(messages.preheaderInfoTooltip)} />
|
|
1299
|
+
</CapRow>
|
|
1300
|
+
),
|
|
1301
|
+
suffix: (
|
|
1302
|
+
<CapRow align="middle" type="flex" noWrap gap={8}>
|
|
1303
|
+
<CapLabelInline type="label1" className="email-html-editor-counter">
|
|
1304
|
+
{`${(preheader || '').length} / 85`}
|
|
1305
|
+
</CapLabelInline>
|
|
1306
|
+
<CapLabelInline
|
|
1307
|
+
type="label1"
|
|
1308
|
+
role="button"
|
|
1309
|
+
tabIndex={0}
|
|
1310
|
+
className="email-html-editor-remove-link"
|
|
1311
|
+
onClick={handleRemovePreheaderClick}
|
|
1312
|
+
>
|
|
1313
|
+
{formatMessage(messages.removePreheader)}
|
|
1314
|
+
</CapLabelInline>
|
|
1315
|
+
</CapRow>
|
|
1316
|
+
),
|
|
1317
|
+
}}
|
|
1318
|
+
onTagSelect={handlePreheaderTagSelect}
|
|
1319
|
+
onContextChange={handleOnTagsContextChange}
|
|
1320
|
+
location={location}
|
|
1321
|
+
tags={tags}
|
|
1322
|
+
injectedTags={injectedTags || {}}
|
|
1323
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
1324
|
+
eventContextTags={eventContextTags}
|
|
1325
|
+
waitEventContextTags={waitEventContextTags}
|
|
1326
|
+
showHeading={false}
|
|
1165
1327
|
showTagList
|
|
1166
1328
|
showInput
|
|
1329
|
+
singleLineLayout
|
|
1167
1330
|
containerStyle={{
|
|
1168
1331
|
display: 'flex',
|
|
1169
1332
|
flexDirection: 'column',
|
|
@@ -1171,6 +1334,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1171
1334
|
popoverPlacement="bottomRight"
|
|
1172
1335
|
/>
|
|
1173
1336
|
</CapColumn>
|
|
1337
|
+
)}
|
|
1174
1338
|
|
|
1175
1339
|
<CapColumn span={24} className="html-editor-options-column">
|
|
1176
1340
|
<CapDropdown
|
|
@@ -12,7 +12,7 @@ import '@testing-library/jest-dom';
|
|
|
12
12
|
import { IntlProvider } from 'react-intl';
|
|
13
13
|
import EmailHTMLEditor from '../EmailHTMLEditor';
|
|
14
14
|
import { validateLiquidTemplateContent } from '../../../../utils/commonUtils';
|
|
15
|
-
import { validateTags } from '../../../../utils/tagValidations';
|
|
15
|
+
import { validateTags, hasUnsubscribeTag } from '../../../../utils/tagValidations';
|
|
16
16
|
import { isEmailUnsubscribeTagOptional } from '../../../../utils/common';
|
|
17
17
|
import { downloadHtmlFile } from '../../../../utils/downloadFile';
|
|
18
18
|
|
|
@@ -23,6 +23,7 @@ jest.mock('../../../../utils/commonUtils', () => ({
|
|
|
23
23
|
|
|
24
24
|
jest.mock('../../../../utils/tagValidations', () => ({
|
|
25
25
|
validateTags: jest.fn(),
|
|
26
|
+
hasUnsubscribeTag: jest.fn(() => false),
|
|
26
27
|
}));
|
|
27
28
|
|
|
28
29
|
jest.mock('../../../../utils/common', () => ({
|
|
@@ -217,11 +218,12 @@ jest.mock('../../../../v2Components/CapTagListWithInput', () => {
|
|
|
217
218
|
React.useEffect(() => {
|
|
218
219
|
setValue(props.inputValue || '');
|
|
219
220
|
}, [props.inputValue]);
|
|
221
|
+
const isPreheader = props.inputId === 'template-preheader';
|
|
220
222
|
return (
|
|
221
223
|
<div data-testid="cap-tag-list-input">
|
|
222
224
|
<input
|
|
223
|
-
id=
|
|
224
|
-
data-testid=
|
|
225
|
+
id={props.inputId || 'template-subject'}
|
|
226
|
+
data-testid={isPreheader ? 'preheader-input' : 'subject-input'}
|
|
225
227
|
value={value}
|
|
226
228
|
onChange={(e) => {
|
|
227
229
|
setValue(e.target.value);
|
|
@@ -233,16 +235,35 @@ jest.mock('../../../../v2Components/CapTagListWithInput', () => {
|
|
|
233
235
|
/>
|
|
234
236
|
<button
|
|
235
237
|
onClick={() => props.onTagSelect && props.onTagSelect('customer.name')}
|
|
236
|
-
data-testid=
|
|
238
|
+
data-testid={isPreheader ? 'preheader-trigger-tag-select' : 'trigger-tag-select'}
|
|
237
239
|
>
|
|
238
240
|
Select Tag
|
|
239
241
|
</button>
|
|
242
|
+
{isPreheader && (
|
|
243
|
+
<button
|
|
244
|
+
onClick={() => props.onTagSelect && props.onTagSelect('')}
|
|
245
|
+
data-testid="preheader-trigger-tag-select-empty"
|
|
246
|
+
>
|
|
247
|
+
Select Empty Tag
|
|
248
|
+
</button>
|
|
249
|
+
)}
|
|
240
250
|
<button
|
|
241
251
|
onClick={() => props.onContextChange && props.onContextChange('default')}
|
|
242
|
-
data-testid=
|
|
252
|
+
data-testid={isPreheader ? 'preheader-trigger-context-change' : 'trigger-context-change'}
|
|
243
253
|
>
|
|
244
254
|
Change Context
|
|
245
255
|
</button>
|
|
256
|
+
{props.extraContent}
|
|
257
|
+
{props.inputProps && props.inputProps.prefix}
|
|
258
|
+
{props.inputProps && props.inputProps.suffix}
|
|
259
|
+
<span data-testid={isPreheader ? 'preheader-exclude-tags' : 'subject-exclude-tags'}>
|
|
260
|
+
{JSON.stringify(props.excludeTags || [])}
|
|
261
|
+
</span>
|
|
262
|
+
{props.inputErrorMessage && (
|
|
263
|
+
<span data-testid={isPreheader ? 'preheader-error' : 'subject-error'}>
|
|
264
|
+
{props.inputErrorMessage}
|
|
265
|
+
</span>
|
|
266
|
+
)}
|
|
246
267
|
</div>
|
|
247
268
|
);
|
|
248
269
|
};
|
|
@@ -407,6 +428,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
407
428
|
jest.clearAllMocks();
|
|
408
429
|
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
409
430
|
validateTags.mockReturnValue({ valid: true });
|
|
431
|
+
hasUnsubscribeTag.mockReturnValue(false);
|
|
410
432
|
isEmailUnsubscribeTagOptional.mockReturnValue(false);
|
|
411
433
|
// Reset mock functions
|
|
412
434
|
mockGetAllIssues.mockReturnValue([]);
|
|
@@ -754,6 +776,186 @@ describe('EmailHTMLEditor', () => {
|
|
|
754
776
|
});
|
|
755
777
|
});
|
|
756
778
|
|
|
779
|
+
describe('Pre-header Handling', () => {
|
|
780
|
+
const revealPreheader = () => {
|
|
781
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
it('uses the updated placeholder text and 85-character limit', () => {
|
|
785
|
+
renderWithIntl();
|
|
786
|
+
revealPreheader();
|
|
787
|
+
|
|
788
|
+
expect(screen.getByTestId('preheader-input').placeholder).toBe('e.g. Get 20% off your next order');
|
|
789
|
+
expect(screen.getByText('0 / 85')).toBeInTheDocument();
|
|
790
|
+
|
|
791
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: 'Hello' } });
|
|
792
|
+
expect(screen.getByText('5 / 85')).toBeInTheDocument();
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
it('shows an info tooltip icon next to the Pre-header label', () => {
|
|
796
|
+
const { container } = renderWithIntl();
|
|
797
|
+
revealPreheader();
|
|
798
|
+
|
|
799
|
+
expect(container.querySelector('.cap-icon-v2-info-circle')).toBeInTheDocument();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
it('inserts tag into Pre-header field at the cursor position', () => {
|
|
803
|
+
renderWithIntl();
|
|
804
|
+
revealPreheader();
|
|
805
|
+
const input = screen.getByTestId('preheader-input');
|
|
806
|
+
fireEvent.change(input, { target: { value: 'Hello ' } });
|
|
807
|
+
Object.defineProperty(input, 'selectionStart', { value: 6, writable: true });
|
|
808
|
+
Object.defineProperty(input, 'selectionEnd', { value: 6, writable: true });
|
|
809
|
+
|
|
810
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-tag-select'));
|
|
811
|
+
|
|
812
|
+
expect(screen.getByTestId('preheader-input').value).toBe('Hello {{customer.name}}');
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
it('appends the tag when the Pre-header input has no selection', () => {
|
|
816
|
+
renderWithIntl();
|
|
817
|
+
revealPreheader();
|
|
818
|
+
const input = screen.getByTestId('preheader-input');
|
|
819
|
+
fireEvent.change(input, { target: { value: 'Hello' } });
|
|
820
|
+
Object.defineProperty(input, 'selectionStart', { value: undefined, writable: true });
|
|
821
|
+
Object.defineProperty(input, 'selectionEnd', { value: undefined, writable: true });
|
|
822
|
+
|
|
823
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-tag-select'));
|
|
824
|
+
|
|
825
|
+
expect(screen.getByTestId('preheader-input').value).toBe('Hello{{customer.name}}');
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('appends the tag when the Pre-header input element is not found', () => {
|
|
829
|
+
renderWithIntl();
|
|
830
|
+
revealPreheader();
|
|
831
|
+
const input = screen.getByTestId('preheader-input');
|
|
832
|
+
fireEvent.change(input, { target: { value: 'Hello' } });
|
|
833
|
+
input.remove();
|
|
834
|
+
|
|
835
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-tag-select'));
|
|
836
|
+
// Should handle gracefully without throwing
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
it('falls back to a safe append when reading the Pre-header input throws', () => {
|
|
840
|
+
renderWithIntl();
|
|
841
|
+
revealPreheader();
|
|
842
|
+
const input = screen.getByTestId('preheader-input');
|
|
843
|
+
fireEvent.change(input, { target: { value: 'Hello' } });
|
|
844
|
+
Object.defineProperty(input, 'selectionStart', {
|
|
845
|
+
configurable: true,
|
|
846
|
+
get() { throw new Error('boom'); },
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
expect(() => fireEvent.click(screen.getByTestId('preheader-trigger-tag-select'))).not.toThrow();
|
|
850
|
+
expect(screen.getByTestId('preheader-input').value).toBe('Hello{{customer.name}}');
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
it('does nothing when the Pre-header tag select fires without data', () => {
|
|
854
|
+
renderWithIntl();
|
|
855
|
+
revealPreheader();
|
|
856
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: 'Hello' } });
|
|
857
|
+
|
|
858
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-tag-select-empty'));
|
|
859
|
+
|
|
860
|
+
expect(screen.getByTestId('preheader-input').value).toBe('Hello');
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
it('inserts a tag into an empty Pre-header field', () => {
|
|
864
|
+
renderWithIntl();
|
|
865
|
+
revealPreheader();
|
|
866
|
+
|
|
867
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-tag-select'));
|
|
868
|
+
|
|
869
|
+
expect(screen.getByTestId('preheader-input').value).toBe('{{customer.name}}');
|
|
870
|
+
});
|
|
871
|
+
|
|
872
|
+
it('handles the Pre-header context-change trigger', () => {
|
|
873
|
+
renderWithIntl();
|
|
874
|
+
revealPreheader();
|
|
875
|
+
|
|
876
|
+
fireEvent.click(screen.getByTestId('preheader-trigger-context-change'));
|
|
877
|
+
// Exercises handleOnTagsContextChange for the Pre-header field
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
it('clears a stale Pre-header error once the user edits the field again', () => {
|
|
881
|
+
hasUnsubscribeTag.mockReturnValue(true);
|
|
882
|
+
const { rerender } = renderWithIntl({ isGetFormData: false });
|
|
883
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
884
|
+
revealPreheader();
|
|
885
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}}' } });
|
|
886
|
+
|
|
887
|
+
rerender(
|
|
888
|
+
<IntlProvider locale="en" messages={{}}>
|
|
889
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData />
|
|
890
|
+
</IntlProvider>
|
|
891
|
+
);
|
|
892
|
+
expect(screen.getByTestId('preheader-error')).toBeInTheDocument();
|
|
893
|
+
|
|
894
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}} extra' } });
|
|
895
|
+
|
|
896
|
+
expect(screen.queryByTestId('preheader-error')).not.toBeInTheDocument();
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
it('blocks save for an invalid Pre-header even when onValidationFail is not provided', () => {
|
|
900
|
+
hasUnsubscribeTag.mockReturnValue(true);
|
|
901
|
+
const emailActions = {
|
|
902
|
+
...defaultProps.emailActions,
|
|
903
|
+
createTemplate: jest.fn((obj, callback) => callback({ templateId: { _id: '123', versions: {} } })),
|
|
904
|
+
};
|
|
905
|
+
const props = { ...defaultProps, emailActions };
|
|
906
|
+
delete props.onValidationFail;
|
|
907
|
+
const { rerender } = render(
|
|
908
|
+
<IntlProvider locale="en" messages={{}}>
|
|
909
|
+
<EmailHTMLEditor {...props} isGetFormData={false} />
|
|
910
|
+
</IntlProvider>
|
|
911
|
+
);
|
|
912
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
913
|
+
revealPreheader();
|
|
914
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}}' } });
|
|
915
|
+
|
|
916
|
+
expect(() => rerender(
|
|
917
|
+
<IntlProvider locale="en" messages={{}}>
|
|
918
|
+
<EmailHTMLEditor {...props} isGetFormData />
|
|
919
|
+
</IntlProvider>
|
|
920
|
+
)).not.toThrow();
|
|
921
|
+
|
|
922
|
+
expect(screen.getByTestId('preheader-error')).toBeInTheDocument();
|
|
923
|
+
expect(emailActions.createTemplate).not.toHaveBeenCalled();
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
it('removes the Pre-header field, clearing its value and error, when Remove is clicked', () => {
|
|
927
|
+
hasUnsubscribeTag.mockReturnValue(true);
|
|
928
|
+
const { rerender } = renderWithIntl({ isGetFormData: false });
|
|
929
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
930
|
+
revealPreheader();
|
|
931
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}}' } });
|
|
932
|
+
rerender(
|
|
933
|
+
<IntlProvider locale="en" messages={{}}>
|
|
934
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData />
|
|
935
|
+
</IntlProvider>
|
|
936
|
+
);
|
|
937
|
+
expect(screen.getByTestId('preheader-error')).toBeInTheDocument();
|
|
938
|
+
|
|
939
|
+
fireEvent.click(screen.getByText('Remove'));
|
|
940
|
+
|
|
941
|
+
expect(screen.queryByTestId('preheader-input')).not.toBeInTheDocument();
|
|
942
|
+
expect(screen.queryByTestId('preheader-error')).not.toBeInTheDocument();
|
|
943
|
+
expect(screen.getByText('+ Pre-header')).toBeInTheDocument();
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
it('defaults injectedTags to {} for the Pre-header tag list when the prop is falsy', () => {
|
|
947
|
+
// null (not undefined) is required here: a static defaultProps entry only kicks in for
|
|
948
|
+
// undefined, so deleting the prop would never actually exercise the `|| {}` fallback.
|
|
949
|
+
expect(() => render(
|
|
950
|
+
<IntlProvider locale="en" messages={{}}>
|
|
951
|
+
<EmailHTMLEditor {...defaultProps} injectedTags={null} />
|
|
952
|
+
</IntlProvider>
|
|
953
|
+
)).not.toThrow();
|
|
954
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
955
|
+
expect(screen.getByTestId('preheader-input')).toBeInTheDocument();
|
|
956
|
+
});
|
|
957
|
+
});
|
|
958
|
+
|
|
757
959
|
describe('Content Change Handling', () => {
|
|
758
960
|
it('updates content when HTMLEditor changes', () => {
|
|
759
961
|
renderWithIntl();
|
|
@@ -899,6 +1101,63 @@ describe('EmailHTMLEditor', () => {
|
|
|
899
1101
|
}, { timeout: 3000 });
|
|
900
1102
|
});
|
|
901
1103
|
|
|
1104
|
+
it('hides the unsubscribe tag from the Pre-header Add Label dropdown but not Subject\'s', () => {
|
|
1105
|
+
renderWithIntl();
|
|
1106
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
1107
|
+
expect(screen.getByTestId('preheader-exclude-tags')).toHaveTextContent(JSON.stringify(['unsubscribe']));
|
|
1108
|
+
expect(screen.getByTestId('subject-exclude-tags')).toHaveTextContent(JSON.stringify([]));
|
|
1109
|
+
});
|
|
1110
|
+
|
|
1111
|
+
it('blocks save and shows an inline error when Pre-header contains the unsubscribe tag', async () => {
|
|
1112
|
+
const onValidationFail = jest.fn();
|
|
1113
|
+
hasUnsubscribeTag.mockReturnValue(true);
|
|
1114
|
+
const { rerender } = renderWithIntl({ onValidationFail, isGetFormData: false });
|
|
1115
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
1116
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
1117
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}}' } });
|
|
1118
|
+
|
|
1119
|
+
rerender(
|
|
1120
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1121
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData />
|
|
1122
|
+
</IntlProvider>
|
|
1123
|
+
);
|
|
1124
|
+
|
|
1125
|
+
await waitFor(() => {
|
|
1126
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
1127
|
+
}, { timeout: 3000 });
|
|
1128
|
+
expect(defaultProps.emailActions.createTemplate).not.toHaveBeenCalled();
|
|
1129
|
+
expect(screen.getByTestId('preheader-error')).toHaveTextContent('Unsubscribe tag is not allowed in Pre-header.');
|
|
1130
|
+
});
|
|
1131
|
+
|
|
1132
|
+
it('allows save when Pre-header does not contain the unsubscribe tag', async () => {
|
|
1133
|
+
hasUnsubscribeTag.mockReturnValue(false);
|
|
1134
|
+
const emailActions = {
|
|
1135
|
+
...defaultProps.emailActions,
|
|
1136
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
1137
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
1138
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
1139
|
+
}),
|
|
1140
|
+
};
|
|
1141
|
+
const { rerender } = renderWithIntl({
|
|
1142
|
+
isGetFormData: false, templateName: 'New Template', emailActions, getLiquidTags: null,
|
|
1143
|
+
});
|
|
1144
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
1145
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
1146
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: 'A normal preheader' } });
|
|
1147
|
+
fireEvent.click(screen.getByTestId('trigger-content-change'));
|
|
1148
|
+
|
|
1149
|
+
rerender(
|
|
1150
|
+
<IntlProvider locale="en" messages={{}}>
|
|
1151
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData templateName="New Template" emailActions={emailActions} getLiquidTags={null} />
|
|
1152
|
+
</IntlProvider>
|
|
1153
|
+
);
|
|
1154
|
+
|
|
1155
|
+
await waitFor(() => {
|
|
1156
|
+
expect(emailActions.createTemplate).toHaveBeenCalled();
|
|
1157
|
+
}, { timeout: 3000 });
|
|
1158
|
+
expect(screen.queryByTestId('preheader-error')).not.toBeInTheDocument();
|
|
1159
|
+
});
|
|
1160
|
+
|
|
902
1161
|
it('blocks save when there are HTML validation errors', async () => {
|
|
903
1162
|
const onValidationFail = jest.fn();
|
|
904
1163
|
// Mock getAllIssues to return BLOCKING errors (sanitizer errors or client-side Liquid errors)
|