@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.
Files changed (50) hide show
  1. package/constants/unified.js +0 -1
  2. package/package.json +1 -1
  3. package/utils/common.js +0 -6
  4. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +58 -1
  5. package/v2Components/CapTagListWithInput/index.js +64 -35
  6. package/v2Components/CapTagListWithInput/index.scss +9 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/EmailPreviewContent.js +8 -0
  8. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +7 -0
  9. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -0
  10. package/v2Components/CommonTestAndPreview/actions.js +1 -1
  11. package/v2Components/CommonTestAndPreview/index.js +29 -13
  12. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  13. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/EmailPreviewContent.test.js +27 -0
  14. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +40 -0
  15. package/v2Components/CommonTestAndPreview/tests/index.test.js +95 -0
  16. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
  17. package/v2Components/EmailMobilePreview/index.js +8 -1
  18. package/v2Components/EmailMobilePreview/index.scss +12 -3
  19. package/v2Components/EmailMobilePreview/tests/index.test.js +31 -0
  20. package/v2Components/EmailPreviewV2/_emailPreviewV2.scss +35 -5
  21. package/v2Components/EmailPreviewV2/index.js +18 -6
  22. package/v2Components/EmailPreviewV2/messages.js +4 -0
  23. package/v2Components/EmailPreviewV2/tests/index.test.js +43 -0
  24. package/v2Components/FormBuilder/Classic.js +113 -10
  25. package/v2Components/FormBuilder/_formBuilder.scss +18 -0
  26. package/v2Components/FormBuilder/messages.js +8 -0
  27. package/v2Containers/Cap/tests/index.test.js +1 -0
  28. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
  29. package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
  30. package/v2Containers/CommunicationFlow/constants.js +1 -0
  31. package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
  32. package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
  33. package/v2Containers/CreativesContainer/index.js +9 -3
  34. package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
  35. package/v2Containers/Email/index.js +31 -4
  36. package/v2Containers/Email/initialSchema.js +30 -0
  37. package/v2Containers/Email/messages.js +8 -0
  38. package/v2Containers/Email/tests/index.test.js +107 -0
  39. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +168 -8
  40. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +72 -3
  41. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +26 -0
  42. package/v2Containers/EmailWrapper/constants.js +5 -0
  43. package/v2Containers/EmailWrapper/messages.js +20 -0
  44. package/v2Containers/TagList/index.js +7 -0
  45. package/v2Containers/TagList/tests/TagList.test.js +42 -0
  46. package/v2Containers/Templates/constants.js +1 -0
  47. package/v2Containers/Templates/index.js +31 -27
  48. package/v2Containers/Templates/messages.js +4 -0
  49. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +128 -4
  50. 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} style={{ marginBottom: CAP_SPACE_16 }}>
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
- headingText={formatMessage(messages.subject)}
1154
- headingType="h4"
1155
- headingStyle={{ marginRight: '85%' }}
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="template-subject"
224
- data-testid="subject-input"
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',