@capillarytech/creatives-library 9.0.70 → 9.0.71-alpha.sprint-cap-211543.17
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/constants/unified.js +0 -1
- package/package.json +1 -1
- package/utils/common.js +0 -6
- 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 +29 -13
- 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 +95 -0
- package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
- 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 +35 -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 +113 -10
- package/v2Components/FormBuilder/_formBuilder.scss +18 -0
- package/v2Components/FormBuilder/messages.js +8 -0
- package/v2Containers/Cap/tests/index.test.js +1 -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/index.js +9 -3
- 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 +168 -8
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +72 -3
- package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +26 -0
- package/v2Containers/EmailWrapper/constants.js +5 -0
- package/v2Containers/EmailWrapper/messages.js +20 -0
- package/v2Containers/TagList/index.js +7 -0
- package/v2Containers/TagList/tests/TagList.test.js +42 -0
- package/v2Containers/Templates/constants.js +1 -0
- package/v2Containers/Templates/index.js +31 -27
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +128 -4
- package/v2Containers/Templates/tests/archival.test.js +221 -0
|
@@ -377,6 +377,27 @@ describe('Email container', () => {
|
|
|
377
377
|
const addLangCol = cols.find((c) => c.id === 'add-language-button');
|
|
378
378
|
expect(addLangCol.colStyle.display).toBe('');
|
|
379
379
|
});
|
|
380
|
+
|
|
381
|
+
const getStandaloneFieldIds = (schema) => _.flatMap(
|
|
382
|
+
schema.standalone.sections,
|
|
383
|
+
(section) => _.flatMap(section.inputFields, (inputField) => _.map(inputField.cols, (col) => col.id)),
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
it('keeps both template-subject and template-preheader as standalone fields for embedded/library module', () => {
|
|
387
|
+
const wrapper = renderEmail({ location: { query: { type: 'embedded', module: 'library' } } });
|
|
388
|
+
|
|
389
|
+
const result = wrapper.instance().removeStandAlone(_.cloneDeep(emailDefinition));
|
|
390
|
+
|
|
391
|
+
expect(getStandaloneFieldIds(result)).toEqual(['template-subject', 'template-preheader']);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('drops template-name from standalone fields for embedded/library module (unchanged prior behavior)', () => {
|
|
395
|
+
const wrapper = renderEmail({ location: { query: { type: 'embedded', module: 'library' } } });
|
|
396
|
+
|
|
397
|
+
const result = wrapper.instance().removeStandAlone(_.cloneDeep(emailDefinition));
|
|
398
|
+
|
|
399
|
+
expect(getStandaloneFieldIds(result)).not.toContain('template-name');
|
|
400
|
+
});
|
|
380
401
|
});
|
|
381
402
|
|
|
382
403
|
describe('handleEdmSave (mode === "switchEditor")', () => {
|
|
@@ -717,4 +738,90 @@ describe('Email container', () => {
|
|
|
717
738
|
expect(wrapper.state('savedTemplate')).toBeNull();
|
|
718
739
|
});
|
|
719
740
|
});
|
|
741
|
+
|
|
742
|
+
describe('pre-header field', () => {
|
|
743
|
+
describe('onTagSelect', () => {
|
|
744
|
+
it('appends the tag to template-preheader when a tag is selected from the pre-header field', () => {
|
|
745
|
+
const wrapper = renderEmail();
|
|
746
|
+
wrapper.setState({ formData: { 'template-preheader': 'Hello ' } });
|
|
747
|
+
|
|
748
|
+
wrapper.instance().onTagSelect('first_name', 1, { id: 'template-preheader' });
|
|
749
|
+
|
|
750
|
+
expect(wrapper.state('formData')['template-preheader']).toBe('Hello {{first_name}}');
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
it('does not touch template-subject when the tag is selected from the pre-header field', () => {
|
|
754
|
+
const wrapper = renderEmail();
|
|
755
|
+
wrapper.setState({ formData: { 'template-preheader': '', 'template-subject': 'Original subject' } });
|
|
756
|
+
|
|
757
|
+
wrapper.instance().onTagSelect('loyalty_points', 1, { id: 'template-preheader' });
|
|
758
|
+
|
|
759
|
+
expect(wrapper.state('formData')['template-subject']).toBe('Original subject');
|
|
760
|
+
expect(wrapper.state('formData')['template-preheader']).toBe('{{loyalty_points}}');
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
describe('setEditData', () => {
|
|
765
|
+
it('populates formData["template-preheader"] from versions.base.preheader', () => {
|
|
766
|
+
const wrapper = renderEmail();
|
|
767
|
+
wrapper.setState({ schema: { containers: [] } });
|
|
768
|
+
const instance = wrapper.instance();
|
|
769
|
+
const editData = {
|
|
770
|
+
name: 'My Template',
|
|
771
|
+
versions: {
|
|
772
|
+
base: {
|
|
773
|
+
subject: 'My subject',
|
|
774
|
+
preheader: 'My preheader text',
|
|
775
|
+
activeTab: 'en',
|
|
776
|
+
selectedLanguages: ['en'],
|
|
777
|
+
en: {},
|
|
778
|
+
},
|
|
779
|
+
history: [],
|
|
780
|
+
},
|
|
781
|
+
};
|
|
782
|
+
|
|
783
|
+
instance.setEditData(editData);
|
|
784
|
+
|
|
785
|
+
expect(wrapper.state('formData')['template-preheader']).toBe('My preheader text');
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('defaults to an empty string when versions.base.preheader is absent', () => {
|
|
789
|
+
const wrapper = renderEmail();
|
|
790
|
+
wrapper.setState({ schema: { containers: [] } });
|
|
791
|
+
const instance = wrapper.instance();
|
|
792
|
+
const editData = {
|
|
793
|
+
name: 'My Template',
|
|
794
|
+
versions: {
|
|
795
|
+
base: {
|
|
796
|
+
subject: 'My subject',
|
|
797
|
+
activeTab: 'en',
|
|
798
|
+
selectedLanguages: ['en'],
|
|
799
|
+
en: {},
|
|
800
|
+
},
|
|
801
|
+
history: [],
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
|
|
805
|
+
instance.setEditData(editData);
|
|
806
|
+
|
|
807
|
+
expect(wrapper.state('formData')['template-preheader']).toBe('');
|
|
808
|
+
});
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
describe('transformFormData', () => {
|
|
812
|
+
it('carries template-preheader into the returned base.preheader', () => {
|
|
813
|
+
const wrapper = renderEmail();
|
|
814
|
+
wrapper.setState({
|
|
815
|
+
formData: {
|
|
816
|
+
'template-preheader': 'Preview snippet text',
|
|
817
|
+
base: { activeTab: 'en', en: { 'template-content': '<p>hi</p>' } },
|
|
818
|
+
},
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
const result = wrapper.instance().transformFormData(wrapper.state('formData'));
|
|
822
|
+
|
|
823
|
+
expect(result.base.preheader).toBe('Preview snippet text');
|
|
824
|
+
});
|
|
825
|
+
});
|
|
826
|
+
});
|
|
720
827
|
});
|
|
@@ -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,8 @@ 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 CapButton from '@capillarytech/cap-ui-library/CapButton';
|
|
18
|
+
import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
|
|
18
19
|
import HTMLEditor from '../../../v2Components/HtmlEditor';
|
|
19
20
|
import CapTagListWithInput from '../../../v2Components/CapTagListWithInput';
|
|
20
21
|
import formBuilderMessages from '../../../v2Components/FormBuilder/messages';
|
|
@@ -24,14 +25,17 @@ import { downloadHtmlFile, sanitizeFileName, getExportHtmlDisabledTooltip } from
|
|
|
24
25
|
import history from '../../../utils/history';
|
|
25
26
|
import messages from '../messages';
|
|
26
27
|
import emailMessages from '../../Email/messages';
|
|
27
|
-
import { validateTags } from '../../../utils/tagValidations';
|
|
28
|
+
import { validateTags, hasUnsubscribeTag } from '../../../utils/tagValidations';
|
|
28
29
|
import {
|
|
29
30
|
TAG, EMBEDDED, DEFAULT, FULL, LIBRARY,
|
|
30
31
|
} from '../../Whatsapp/constants';
|
|
31
32
|
import { EMAIL } from '../../CreativesContainer/constants';
|
|
32
33
|
import { OUTBOUND } from '../../../v2Components/FormBuilder/constants';
|
|
34
|
+
import { PREHEADER_EXCLUDED_TAGS } from '../constants';
|
|
33
35
|
import './_emailHTMLEditor.scss';
|
|
34
36
|
|
|
37
|
+
const { CapLabelInline } = CapLabel;
|
|
38
|
+
|
|
35
39
|
/**
|
|
36
40
|
* EmailHTMLEditor Component
|
|
37
41
|
*
|
|
@@ -111,6 +115,17 @@ const EmailHTMLEditor = (props) => {
|
|
|
111
115
|
const [savedTemplate, setSavedTemplate] = useState(null);
|
|
112
116
|
const [subject, setSubject] = useState('');
|
|
113
117
|
const [subjectError, setSubjectError] = useState('');
|
|
118
|
+
const [preheader, setPreheader] = useState('');
|
|
119
|
+
const [preheaderError, setPreheaderError] = useState('');
|
|
120
|
+
// null = show if content exists; true/false = manual "+ Pre-header"/"Remove" click.
|
|
121
|
+
const [preheaderRevealed, setPreheaderRevealed] = useState(null);
|
|
122
|
+
const isPreheaderRevealed = preheaderRevealed !== null ? preheaderRevealed : !!preheader;
|
|
123
|
+
const handleShowPreheaderClick = useCallback(() => setPreheaderRevealed(true), []);
|
|
124
|
+
const handleRemovePreheaderClick = useCallback(() => {
|
|
125
|
+
setPreheader('');
|
|
126
|
+
setPreheaderError('');
|
|
127
|
+
setPreheaderRevealed(false);
|
|
128
|
+
}, []);
|
|
114
129
|
// State for template name (extracted from template data in Edit mode)
|
|
115
130
|
const [extractedTemplateName, setExtractedTemplateName] = useState('');
|
|
116
131
|
const [tagValidationError, setTagValidationError] = useState(null);
|
|
@@ -183,6 +198,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
183
198
|
base: true,
|
|
184
199
|
},
|
|
185
200
|
'template-subject': subject || '',
|
|
201
|
+
'template-preheader': preheader || '',
|
|
186
202
|
};
|
|
187
203
|
},
|
|
188
204
|
getContentForPreview: () => htmlContent || '',
|
|
@@ -206,7 +222,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
206
222
|
html: 0, label: 0, liquid: 0, total: 0,
|
|
207
223
|
};
|
|
208
224
|
},
|
|
209
|
-
}), [htmlContent, subject, currentOrgDetails]);
|
|
225
|
+
}), [htmlContent, subject, preheader, currentOrgDetails]);
|
|
210
226
|
|
|
211
227
|
// Detect edit mode: when isEditEmail is false (create flow), never treat as edit or fetch template details
|
|
212
228
|
const hasParamsId = params?.id || location?.query?.id || location?.params?.id || location?.pathname?.includes('/edit/');
|
|
@@ -296,6 +312,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
296
312
|
lastTemplateIdRef.current = currentTemplateId;
|
|
297
313
|
setHtmlContent('');
|
|
298
314
|
setSubject('');
|
|
315
|
+
setPreheader('');
|
|
299
316
|
setExtractedTemplateName('');
|
|
300
317
|
baselineContentRef.current = null;
|
|
301
318
|
setHasContentChanged(false);
|
|
@@ -324,11 +341,13 @@ const EmailHTMLEditor = (props) => {
|
|
|
324
341
|
if (hasCompleteTemplateData && !contentInitializedRef.current) {
|
|
325
342
|
let extractedContent = '';
|
|
326
343
|
let extractedSubject = '';
|
|
344
|
+
let extractedPreheader = '';
|
|
327
345
|
let extractedName = '';
|
|
328
346
|
|
|
329
347
|
if (templateDataProp.base) {
|
|
330
348
|
extractedContent = templateDataProp.base['template-content'] || templateDataProp.base.html_content || templateDataProp.base.emailBody || '';
|
|
331
349
|
extractedSubject = templateDataProp.base.subject || templateDataProp.base.emailSubject || '';
|
|
350
|
+
extractedPreheader = templateDataProp.base.preheader || '';
|
|
332
351
|
} else if (templateDataProp.versions && templateDataProp.versions.base) {
|
|
333
352
|
const { base } = templateDataProp.versions;
|
|
334
353
|
const activeTab = base.activeTab || 'en';
|
|
@@ -341,9 +360,11 @@ const EmailHTMLEditor = (props) => {
|
|
|
341
360
|
|| '';
|
|
342
361
|
|
|
343
362
|
extractedSubject = base.subject || base.emailSubject || '';
|
|
363
|
+
extractedPreheader = base.preheader || '';
|
|
344
364
|
} else {
|
|
345
365
|
extractedContent = templateDataProp['template-content'] || templateDataProp.emailBody || templateDataProp.html_content || '';
|
|
346
366
|
extractedSubject = templateDataProp.emailSubject || templateDataProp.subject || '';
|
|
367
|
+
extractedPreheader = templateDataProp.preheader || '';
|
|
347
368
|
}
|
|
348
369
|
|
|
349
370
|
// Extract template name from templateDataProp
|
|
@@ -354,6 +375,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
354
375
|
setHtmlContent(extractedContent);
|
|
355
376
|
setLoadedHtmlContent(extractedContent);
|
|
356
377
|
setSubject(extractedSubject);
|
|
378
|
+
setPreheader(extractedPreheader);
|
|
357
379
|
setExtractedTemplateName(extractedName);
|
|
358
380
|
contentInitializedRef.current = true;
|
|
359
381
|
subjectInitializedRef.current = true;
|
|
@@ -425,6 +447,11 @@ const EmailHTMLEditor = (props) => {
|
|
|
425
447
|
|| get(templateDataFromRedux, 'versions.base.subject')
|
|
426
448
|
|| '';
|
|
427
449
|
|
|
450
|
+
const extractedPreheader = baseData.preheader
|
|
451
|
+
|| get(templateDataFromRedux, 'preheader')
|
|
452
|
+
|| get(templateDataFromRedux, 'versions.base.preheader')
|
|
453
|
+
|| '';
|
|
454
|
+
|
|
428
455
|
// Extract template name from Redux data
|
|
429
456
|
const extractedName = templateDataFromRedux.name || '';
|
|
430
457
|
|
|
@@ -438,6 +465,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
438
465
|
if (!contentInitializedRef.current || htmlContent === loadedHtmlContent) {
|
|
439
466
|
setHtmlContent(extractedContent);
|
|
440
467
|
setSubject(extractedSubject);
|
|
468
|
+
setPreheader(extractedPreheader);
|
|
441
469
|
setExtractedTemplateName(extractedName);
|
|
442
470
|
}
|
|
443
471
|
} else if (!contentInitializedRef.current) {
|
|
@@ -445,6 +473,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
445
473
|
setHtmlContent(extractedContent);
|
|
446
474
|
setLoadedHtmlContent(extractedContent);
|
|
447
475
|
setSubject(extractedSubject);
|
|
476
|
+
setPreheader(extractedPreheader);
|
|
448
477
|
setExtractedTemplateName(extractedName);
|
|
449
478
|
}
|
|
450
479
|
|
|
@@ -470,6 +499,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
470
499
|
setHtmlContent('');
|
|
471
500
|
setLoadedHtmlContent(''); // Set stable loaded content
|
|
472
501
|
setSubject('');
|
|
502
|
+
setPreheader('');
|
|
473
503
|
setExtractedTemplateName('');
|
|
474
504
|
setIsLoading(false);
|
|
475
505
|
if (setIsLoadingContent) {
|
|
@@ -676,6 +706,53 @@ const EmailHTMLEditor = (props) => {
|
|
|
676
706
|
}
|
|
677
707
|
}, [subjectError]);
|
|
678
708
|
|
|
709
|
+
// Handle tag insertion into Pre-header field
|
|
710
|
+
const handlePreheaderTagSelect = useCallback((data) => {
|
|
711
|
+
if (data) {
|
|
712
|
+
const tagToInsert = `{{${data}}}`;
|
|
713
|
+
const input = document.getElementById('template-preheader') || document.querySelector('#template-preheader input');
|
|
714
|
+
let preheaderValue = preheader || '';
|
|
715
|
+
try {
|
|
716
|
+
if (input && (typeof input.selectionStart === 'number')) {
|
|
717
|
+
const startPos = input.selectionStart;
|
|
718
|
+
const endPos = input.selectionEnd;
|
|
719
|
+
preheaderValue = `${preheaderValue.substring(0, startPos)}${tagToInsert}${preheaderValue.substring(endPos)}`;
|
|
720
|
+
setPreheader(preheaderValue);
|
|
721
|
+
try {
|
|
722
|
+
input.focus();
|
|
723
|
+
const newPos = startPos + tagToInsert.length;
|
|
724
|
+
input.selectionStart = newPos;
|
|
725
|
+
input.selectionEnd = newPos;
|
|
726
|
+
} catch (e) {
|
|
727
|
+
// Ignore focus errors
|
|
728
|
+
}
|
|
729
|
+
} else {
|
|
730
|
+
preheaderValue = `${preheaderValue}${tagToInsert}`;
|
|
731
|
+
setPreheader(preheaderValue);
|
|
732
|
+
if (input) {
|
|
733
|
+
try {
|
|
734
|
+
input.value = preheaderValue;
|
|
735
|
+
} catch (e) {
|
|
736
|
+
// Ignore value setting errors
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
} catch (e) {
|
|
741
|
+
// Fallback: safe append
|
|
742
|
+
preheaderValue = `${preheaderValue}${tagToInsert}`;
|
|
743
|
+
setPreheader(preheaderValue);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}, [preheader]);
|
|
747
|
+
|
|
748
|
+
// Handle pre-header change
|
|
749
|
+
const handlePreheaderChange = useCallback((e) => {
|
|
750
|
+
setPreheader(e.target.value);
|
|
751
|
+
if (preheaderError) {
|
|
752
|
+
setPreheaderError('');
|
|
753
|
+
}
|
|
754
|
+
}, [preheaderError]);
|
|
755
|
+
|
|
679
756
|
// Handle Save/Update with liquid validation
|
|
680
757
|
const handleSave = useCallback(() => {
|
|
681
758
|
// IMPORTANT: Clear API validation errors FIRST before checking for validation errors
|
|
@@ -704,6 +781,15 @@ const EmailHTMLEditor = (props) => {
|
|
|
704
781
|
setSubjectError('');
|
|
705
782
|
}
|
|
706
783
|
|
|
784
|
+
// 1.1. Validate Pre-header - BLOCKING: must never contain the unsubscribe tag
|
|
785
|
+
if (hasUnsubscribeTag(preheader)) {
|
|
786
|
+
setPreheaderError(formatMessage(messages.unsubscribeNotAllowedInPreheader));
|
|
787
|
+
if (onValidationFail) {
|
|
788
|
+
onValidationFail();
|
|
789
|
+
}
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
|
|
707
793
|
// 1.5. Check for validation errors (Errors = blocking, Warnings = non-blocking)
|
|
708
794
|
// Get issue counts from ref or stored state
|
|
709
795
|
let issueCounts = { errors: 0, warnings: 0, total: 0 };
|
|
@@ -835,6 +921,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
835
921
|
base: true,
|
|
836
922
|
tabKey,
|
|
837
923
|
subject: subject || '',
|
|
924
|
+
preheader: preheader || '',
|
|
838
925
|
};
|
|
839
926
|
|
|
840
927
|
const historyEntry = {
|
|
@@ -844,6 +931,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
844
931
|
base: true,
|
|
845
932
|
tabKey,
|
|
846
933
|
subject: subject || '',
|
|
934
|
+
preheader: preheader || '',
|
|
847
935
|
};
|
|
848
936
|
|
|
849
937
|
// Opened in edit mode: name from the loaded template; created in this session: the name entered for it
|
|
@@ -944,6 +1032,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
944
1032
|
secondary_templates: [{ template_data: tmpData }],
|
|
945
1033
|
};
|
|
946
1034
|
libraryPayload.base.subject = subject || '';
|
|
1035
|
+
libraryPayload.base.preheader = preheader || '';
|
|
947
1036
|
|
|
948
1037
|
if (location?.query?.module === 'library' && isGetFormData && getFormdata) {
|
|
949
1038
|
const response = {
|
|
@@ -1006,6 +1095,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1006
1095
|
}
|
|
1007
1096
|
}, [
|
|
1008
1097
|
subject,
|
|
1098
|
+
preheader,
|
|
1009
1099
|
htmlContent,
|
|
1010
1100
|
tags,
|
|
1011
1101
|
injectedTags,
|
|
@@ -1142,7 +1232,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1142
1232
|
<CapRow>
|
|
1143
1233
|
<CapColumn span={24}>
|
|
1144
1234
|
{/* Subject Field */}
|
|
1145
|
-
<CapColumn span={24}
|
|
1235
|
+
<CapColumn span={24} className="email-html-editor-field">
|
|
1146
1236
|
<CapTagListWithInput
|
|
1147
1237
|
inputId="template-subject"
|
|
1148
1238
|
inputValue={subject}
|
|
@@ -1150,9 +1240,13 @@ const EmailHTMLEditor = (props) => {
|
|
|
1150
1240
|
inputPlaceholder={formatMessage(messages.enterEmailSubject)}
|
|
1151
1241
|
inputRequired
|
|
1152
1242
|
inputErrorMessage={subjectError}
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1243
|
+
inputProps={{
|
|
1244
|
+
prefix: (
|
|
1245
|
+
<CapLabelInline type="label2" className="email-html-editor-prefix-label">
|
|
1246
|
+
{formatMessage(messages.subject)}
|
|
1247
|
+
</CapLabelInline>
|
|
1248
|
+
),
|
|
1249
|
+
}}
|
|
1156
1250
|
onTagSelect={handleSubjectTagSelect}
|
|
1157
1251
|
onContextChange={handleOnTagsContextChange}
|
|
1158
1252
|
location={location}
|
|
@@ -1161,9 +1255,74 @@ const EmailHTMLEditor = (props) => {
|
|
|
1161
1255
|
selectedOfferDetails={selectedOfferDetails}
|
|
1162
1256
|
eventContextTags={eventContextTags}
|
|
1163
1257
|
waitEventContextTags={waitEventContextTags}
|
|
1164
|
-
showHeading
|
|
1258
|
+
showHeading={false}
|
|
1259
|
+
showTagList
|
|
1260
|
+
showInput
|
|
1261
|
+
singleLineLayout
|
|
1262
|
+
extraContent={!isPreheaderRevealed ? (
|
|
1263
|
+
<CapButton
|
|
1264
|
+
type="dashed"
|
|
1265
|
+
size="small"
|
|
1266
|
+
className="email-html-editor-add-preheader-btn"
|
|
1267
|
+
onClick={handleShowPreheaderClick}
|
|
1268
|
+
>
|
|
1269
|
+
{formatMessage(messages.addPreheader)}
|
|
1270
|
+
</CapButton>
|
|
1271
|
+
) : null}
|
|
1272
|
+
containerStyle={{
|
|
1273
|
+
display: 'flex',
|
|
1274
|
+
flexDirection: 'column',
|
|
1275
|
+
}}
|
|
1276
|
+
popoverPlacement="bottomRight"
|
|
1277
|
+
/>
|
|
1278
|
+
</CapColumn>
|
|
1279
|
+
|
|
1280
|
+
{/* Pre-header Field */}
|
|
1281
|
+
{isPreheaderRevealed && (
|
|
1282
|
+
<CapColumn span={24} className="email-html-editor-field">
|
|
1283
|
+
<CapTagListWithInput
|
|
1284
|
+
inputId="template-preheader"
|
|
1285
|
+
inputValue={preheader}
|
|
1286
|
+
inputOnChange={handlePreheaderChange}
|
|
1287
|
+
inputPlaceholder={formatMessage(messages.enterEmailPreheader)}
|
|
1288
|
+
inputMaxLength={150}
|
|
1289
|
+
inputErrorMessage={preheaderError}
|
|
1290
|
+
excludeTags={PREHEADER_EXCLUDED_TAGS}
|
|
1291
|
+
inputProps={{
|
|
1292
|
+
prefix: (
|
|
1293
|
+
<CapLabelInline type="label2" className="email-html-editor-prefix-label">
|
|
1294
|
+
{formatMessage(messages.preheader)}
|
|
1295
|
+
</CapLabelInline>
|
|
1296
|
+
),
|
|
1297
|
+
suffix: (
|
|
1298
|
+
<CapRow align="middle" type="flex" noWrap gap={8}>
|
|
1299
|
+
<CapLabelInline type="label1" className="email-html-editor-counter">
|
|
1300
|
+
{`${(preheader || '').length} / 150`}
|
|
1301
|
+
</CapLabelInline>
|
|
1302
|
+
<CapLabelInline
|
|
1303
|
+
type="label1"
|
|
1304
|
+
role="button"
|
|
1305
|
+
tabIndex={0}
|
|
1306
|
+
className="email-html-editor-remove-link"
|
|
1307
|
+
onClick={handleRemovePreheaderClick}
|
|
1308
|
+
>
|
|
1309
|
+
{formatMessage(messages.removePreheader)}
|
|
1310
|
+
</CapLabelInline>
|
|
1311
|
+
</CapRow>
|
|
1312
|
+
),
|
|
1313
|
+
}}
|
|
1314
|
+
onTagSelect={handlePreheaderTagSelect}
|
|
1315
|
+
onContextChange={handleOnTagsContextChange}
|
|
1316
|
+
location={location}
|
|
1317
|
+
tags={tags}
|
|
1318
|
+
injectedTags={injectedTags || {}}
|
|
1319
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
1320
|
+
eventContextTags={eventContextTags}
|
|
1321
|
+
waitEventContextTags={waitEventContextTags}
|
|
1322
|
+
showHeading={false}
|
|
1165
1323
|
showTagList
|
|
1166
1324
|
showInput
|
|
1325
|
+
singleLineLayout
|
|
1167
1326
|
containerStyle={{
|
|
1168
1327
|
display: 'flex',
|
|
1169
1328
|
flexDirection: 'column',
|
|
@@ -1171,6 +1330,7 @@ const EmailHTMLEditor = (props) => {
|
|
|
1171
1330
|
popoverPlacement="bottomRight"
|
|
1172
1331
|
/>
|
|
1173
1332
|
</CapColumn>
|
|
1333
|
+
)}
|
|
1174
1334
|
|
|
1175
1335
|
<CapColumn span={24} className="html-editor-options-column">
|
|
1176
1336
|
<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);
|
|
@@ -243,6 +245,15 @@ jest.mock('../../../../v2Components/CapTagListWithInput', () => {
|
|
|
243
245
|
>
|
|
244
246
|
Change Context
|
|
245
247
|
</button>
|
|
248
|
+
{props.extraContent}
|
|
249
|
+
<span data-testid={isPreheader ? 'preheader-exclude-tags' : 'subject-exclude-tags'}>
|
|
250
|
+
{JSON.stringify(props.excludeTags || [])}
|
|
251
|
+
</span>
|
|
252
|
+
{props.inputErrorMessage && (
|
|
253
|
+
<span data-testid={isPreheader ? 'preheader-error' : 'subject-error'}>
|
|
254
|
+
{props.inputErrorMessage}
|
|
255
|
+
</span>
|
|
256
|
+
)}
|
|
246
257
|
</div>
|
|
247
258
|
);
|
|
248
259
|
};
|
|
@@ -407,6 +418,7 @@ describe('EmailHTMLEditor', () => {
|
|
|
407
418
|
jest.clearAllMocks();
|
|
408
419
|
validateLiquidTemplateContent.mockResolvedValue(true);
|
|
409
420
|
validateTags.mockReturnValue({ valid: true });
|
|
421
|
+
hasUnsubscribeTag.mockReturnValue(false);
|
|
410
422
|
isEmailUnsubscribeTagOptional.mockReturnValue(false);
|
|
411
423
|
// Reset mock functions
|
|
412
424
|
mockGetAllIssues.mockReturnValue([]);
|
|
@@ -899,6 +911,63 @@ describe('EmailHTMLEditor', () => {
|
|
|
899
911
|
}, { timeout: 3000 });
|
|
900
912
|
});
|
|
901
913
|
|
|
914
|
+
it('hides the unsubscribe tag from the Pre-header Add Label dropdown but not Subject\'s', () => {
|
|
915
|
+
renderWithIntl();
|
|
916
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
917
|
+
expect(screen.getByTestId('preheader-exclude-tags')).toHaveTextContent(JSON.stringify(['unsubscribe']));
|
|
918
|
+
expect(screen.getByTestId('subject-exclude-tags')).toHaveTextContent(JSON.stringify([]));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
it('blocks save and shows an inline error when Pre-header contains the unsubscribe tag', async () => {
|
|
922
|
+
const onValidationFail = jest.fn();
|
|
923
|
+
hasUnsubscribeTag.mockReturnValue(true);
|
|
924
|
+
const { rerender } = renderWithIntl({ onValidationFail, isGetFormData: false });
|
|
925
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
926
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
927
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: '{{unsubscribe}}' } });
|
|
928
|
+
|
|
929
|
+
rerender(
|
|
930
|
+
<IntlProvider locale="en" messages={{}}>
|
|
931
|
+
<EmailHTMLEditor {...defaultProps} onValidationFail={onValidationFail} isGetFormData />
|
|
932
|
+
</IntlProvider>
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
await waitFor(() => {
|
|
936
|
+
expect(onValidationFail).toHaveBeenCalled();
|
|
937
|
+
}, { timeout: 3000 });
|
|
938
|
+
expect(defaultProps.emailActions.createTemplate).not.toHaveBeenCalled();
|
|
939
|
+
expect(screen.getByTestId('preheader-error')).toHaveTextContent('Unsubscribe tag is not allowed in Pre-header.');
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
it('allows save when Pre-header does not contain the unsubscribe tag', async () => {
|
|
943
|
+
hasUnsubscribeTag.mockReturnValue(false);
|
|
944
|
+
const emailActions = {
|
|
945
|
+
...defaultProps.emailActions,
|
|
946
|
+
transformEmailTemplate: jest.fn((obj, callback) => callback(obj)),
|
|
947
|
+
createTemplate: jest.fn((obj, callback) => {
|
|
948
|
+
callback({ templateId: { _id: '123', versions: {} } });
|
|
949
|
+
}),
|
|
950
|
+
};
|
|
951
|
+
const { rerender } = renderWithIntl({
|
|
952
|
+
isGetFormData: false, templateName: 'New Template', emailActions, getLiquidTags: null,
|
|
953
|
+
});
|
|
954
|
+
fireEvent.change(screen.getByTestId('subject-input'), { target: { value: 'Valid Subject' } });
|
|
955
|
+
fireEvent.click(screen.getByText('+ Pre-header'));
|
|
956
|
+
fireEvent.change(screen.getByTestId('preheader-input'), { target: { value: 'A normal preheader' } });
|
|
957
|
+
fireEvent.click(screen.getByTestId('trigger-content-change'));
|
|
958
|
+
|
|
959
|
+
rerender(
|
|
960
|
+
<IntlProvider locale="en" messages={{}}>
|
|
961
|
+
<EmailHTMLEditor {...defaultProps} isGetFormData templateName="New Template" emailActions={emailActions} getLiquidTags={null} />
|
|
962
|
+
</IntlProvider>
|
|
963
|
+
);
|
|
964
|
+
|
|
965
|
+
await waitFor(() => {
|
|
966
|
+
expect(emailActions.createTemplate).toHaveBeenCalled();
|
|
967
|
+
}, { timeout: 3000 });
|
|
968
|
+
expect(screen.queryByTestId('preheader-error')).not.toBeInTheDocument();
|
|
969
|
+
});
|
|
970
|
+
|
|
902
971
|
it('blocks save when there are HTML validation errors', async () => {
|
|
903
972
|
const onValidationFail = jest.fn();
|
|
904
973
|
// Mock getAllIssues to return BLOCKING errors (sanitizer errors or client-side Liquid errors)
|
|
@@ -9,3 +9,29 @@
|
|
|
9
9
|
.html-editor-options-icon {
|
|
10
10
|
cursor: pointer;
|
|
11
11
|
}
|
|
12
|
+
|
|
13
|
+
// Subject/Pre-header field rows
|
|
14
|
+
.email-html-editor-field {
|
|
15
|
+
margin-bottom: $CAP_SPACE_16;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.email-html-editor-add-preheader-btn {
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Subject/Pre-header inline prefix label and Pre-header suffix (counter + Remove)
|
|
23
|
+
.email-html-editor-prefix-label {
|
|
24
|
+
font-weight: 600;
|
|
25
|
+
padding-right: $CAP_SPACE_08;
|
|
26
|
+
margin-right: $CAP_SPACE_08;
|
|
27
|
+
border-right: 0.0625rem solid $CAP_COLOR_16;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.email-html-editor-counter {
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.email-html-editor-remove-link {
|
|
35
|
+
white-space: nowrap;
|
|
36
|
+
cursor: pointer;
|
|
37
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { UNSUBSCRIBE_TAG } from '../../constants/unified';
|
|
8
|
+
|
|
7
9
|
export const DEFAULT_ACTION = "app/EmailWrapper/DEFAULT_ACTION";
|
|
8
10
|
export const EMAIL_CREATE_MODES = {
|
|
9
11
|
UPLOAD: "upload",
|
|
@@ -17,3 +19,6 @@ export const STEPS = {
|
|
|
17
19
|
TEMPLATE_SELECTION: "templateSelection",
|
|
18
20
|
CREATE_TEMPLATE_CONTENT: "createTemplateContent",
|
|
19
21
|
};
|
|
22
|
+
|
|
23
|
+
// Unsubscribe is not a valid Pre-header tag — hide it from the "Add Label" dropdown there.
|
|
24
|
+
export const PREHEADER_EXCLUDED_TAGS = [UNSUBSCRIBE_TAG];
|
|
@@ -122,6 +122,26 @@ export default defineMessages({
|
|
|
122
122
|
id: 'creatives.containersV2.EmailWrapper.enterEmailSubject',
|
|
123
123
|
defaultMessage: 'Enter Email Subject',
|
|
124
124
|
},
|
|
125
|
+
preheader: {
|
|
126
|
+
id: 'creatives.containersV2.EmailWrapper.preheader',
|
|
127
|
+
defaultMessage: 'Pre-header',
|
|
128
|
+
},
|
|
129
|
+
enterEmailPreheader: {
|
|
130
|
+
id: 'creatives.containersV2.EmailWrapper.enterEmailPreheader',
|
|
131
|
+
defaultMessage: 'Enter Pre-header Text',
|
|
132
|
+
},
|
|
133
|
+
addPreheader: {
|
|
134
|
+
id: 'creatives.containersV2.EmailWrapper.addPreheader',
|
|
135
|
+
defaultMessage: '+ Pre-header',
|
|
136
|
+
},
|
|
137
|
+
removePreheader: {
|
|
138
|
+
id: 'creatives.containersV2.EmailWrapper.removePreheader',
|
|
139
|
+
defaultMessage: 'Remove',
|
|
140
|
+
},
|
|
141
|
+
unsubscribeNotAllowedInPreheader: {
|
|
142
|
+
id: 'creatives.containersV2.EmailWrapper.unsubscribeNotAllowedInPreheader',
|
|
143
|
+
defaultMessage: 'Unsubscribe tag is not allowed in Pre-header.',
|
|
144
|
+
},
|
|
125
145
|
editorType: {
|
|
126
146
|
id: 'creatives.containersV2.EmailWrapper.editorType',
|
|
127
147
|
defaultMessage: 'Editor type',
|