@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
@@ -59,7 +59,6 @@ export const EXTENDED_TAG = 'ExtendedTagMessage';
59
59
  export const BADGES_UI_ENABLED = 'BADGES_UI_ENABLED';
60
60
  export const JP_LOCALE_HIDE_FEATURE = 'JP_LOCALE_HIDE_FEATURE';
61
61
  export const ENABLE_WECHAT = 'ENABLE_WECHAT';
62
- export const ENABLE_CREATIVES_ARCHIVAL = 'ENABLE_CREATIVES_ARCHIVAL';
63
62
  export const ENABLE_WEBPUSH = 'ENABLE_WEBPUSH';
64
63
  export const ENABLE_CUSTOMER_BARCODE_TAG = 'ENABLE_CUSTOMER_BARCODE_TAG';
65
64
  export const EMAIL_UNSUBSCRIBE_TAG_MANDATORY = 'EMAIL_UNSUBSCRIBE_TAG_MANDATORY';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.70",
4
+ "version": "9.0.71-alpha.sprint-cap-211543.17",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
package/utils/common.js CHANGED
@@ -29,7 +29,6 @@ import {
29
29
  ENABLE_NEW_FORMBUILDER_SMS,
30
30
  ENABLE_NEW_FORMBUILDER_MPUSH,
31
31
  SUPPORT_ENGAGEMENT_MODULE,
32
- ENABLE_CREATIVES_ARCHIVAL,
33
32
  } from '../constants/unified';
34
33
  import { apiMessageFormatHandler } from './commonUtils';
35
34
 
@@ -121,11 +120,6 @@ export const hasWechatFeatureEnabled = Auth.hasFeatureAccess.bind(
121
120
  ENABLE_WECHAT,
122
121
  );
123
122
 
124
- export const hasCreativesArchivalEnabled = Auth.hasFeatureAccess.bind(
125
- null,
126
- ENABLE_CREATIVES_ARCHIVAL,
127
- );
128
-
129
123
  export const hasWebPushFeatureEnabled = Auth.hasFeatureAccess.bind(
130
124
  null,
131
125
  ENABLE_WEBPUSH,
@@ -5,6 +5,7 @@ import { IntlProvider } from 'react-intl';
5
5
  import CapTagListWithInput from '../index';
6
6
 
7
7
  const capturedTagListProps = { current: null };
8
+ const capturedCapRowProps = { current: null };
8
9
 
9
10
  jest.mock('../../../v2Containers/TagList', () => {
10
11
  const React = require('react');
@@ -15,7 +16,14 @@ jest.mock('../../../v2Containers/TagList', () => {
15
16
  return Mock;
16
17
  });
17
18
 
18
- jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({ children }) => <div>{children}</div>);
19
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
20
+ const React = require('react');
21
+ const Mock = ({ children, ...rest }) => {
22
+ capturedCapRowProps.current = rest;
23
+ return <div>{children}</div>;
24
+ };
25
+ return Mock;
26
+ });
19
27
  jest.mock('@capillarytech/cap-ui-library/CapColumn', () => ({ children }) => <div>{children}</div>);
20
28
  jest.mock('@capillarytech/cap-ui-library/CapHeading', () => () => null);
21
29
  jest.mock('@capillarytech/cap-ui-library/CapInput', () => () => <input data-testid="cap-input" />);
@@ -27,6 +35,7 @@ const waitMap = {
27
35
  describe('CapTagListWithInput', () => {
28
36
  beforeEach(() => {
29
37
  capturedTagListProps.current = null;
38
+ capturedCapRowProps.current = null;
30
39
  });
31
40
 
32
41
  it('forwards waitEventContextTags to TagList', () => {
@@ -60,4 +69,52 @@ describe('CapTagListWithInput', () => {
60
69
 
61
70
  expect(capturedTagListProps.current.waitEventContextTags).toEqual({});
62
71
  });
72
+
73
+ it('forwards excludeTags to TagList', () => {
74
+ render(
75
+ <IntlProvider locale="en" messages={{}}>
76
+ <CapTagListWithInput
77
+ inputId="template-preheader"
78
+ inputOnChange={jest.fn()}
79
+ excludeTags={['unsubscribe']}
80
+ onTagSelect={jest.fn()}
81
+ onContextChange={jest.fn()}
82
+ />
83
+ </IntlProvider>
84
+ );
85
+
86
+ expect(capturedTagListProps.current.excludeTags).toEqual(['unsubscribe']);
87
+ });
88
+
89
+ it('uses default empty array for excludeTags when omitted', () => {
90
+ render(
91
+ <IntlProvider locale="en" messages={{}}>
92
+ <CapTagListWithInput
93
+ inputId="test-url"
94
+ inputOnChange={jest.fn()}
95
+ onTagSelect={jest.fn()}
96
+ onContextChange={jest.fn()}
97
+ />
98
+ </IntlProvider>
99
+ );
100
+
101
+ expect(capturedTagListProps.current.excludeTags).toEqual([]);
102
+ });
103
+
104
+ it('top-aligns the singleLineLayout row so an error message growing the input does not shift the Add Label button', () => {
105
+ render(
106
+ <IntlProvider locale="en" messages={{}}>
107
+ <CapTagListWithInput
108
+ inputId="template-preheader"
109
+ inputOnChange={jest.fn()}
110
+ inputErrorMessage="Unsubscribe tag is not allowed in Pre-header."
111
+ singleLineLayout
112
+ onTagSelect={jest.fn()}
113
+ onContextChange={jest.fn()}
114
+ />
115
+ </IntlProvider>
116
+ );
117
+
118
+ expect(capturedCapRowProps.current.align).toBe('top');
119
+ });
63
120
  });
@@ -7,6 +7,7 @@ import CapInput from '@capillarytech/cap-ui-library/CapInput';
7
7
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
8
8
  import TagList from '../../v2Containers/TagList';
9
9
  import messages from './messages';
10
+ import './index.scss';
10
11
 
11
12
  /**
12
13
  * CapTagListWithInput - A reusable component that combines TagList and CapInput
@@ -18,6 +19,7 @@ export const CapTagListWithInput = (props) => {
18
19
  // TagList props
19
20
  tags = [],
20
21
  injectedTags = {},
22
+ excludeTags = [],
21
23
  location = {},
22
24
  selectedOfferDetails = [],
23
25
  onTagSelect,
@@ -51,10 +53,65 @@ export const CapTagListWithInput = (props) => {
51
53
  showInput = true,
52
54
  inputProps = {},
53
55
  popoverPlacement,
56
+ // Tag-insert trigger inline with the input (not in a row above)
57
+ singleLineLayout = false,
58
+ // Content between input and tag trigger (singleLineLayout only)
59
+ extraContent = null,
54
60
  } = props;
55
61
 
56
62
  const { formatMessage } = intl;
57
63
 
64
+ const tagListElement = showTagList && (
65
+ <TagList
66
+ key={`${inputId}_tags`}
67
+ className={className}
68
+ moduleFilterEnabled={moduleFilterEnabled}
69
+ label={formatMessage(messages.addLabels)}
70
+ onTagSelect={onTagSelect}
71
+ onContextChange={onContextChange}
72
+ location={location}
73
+ tags={tags}
74
+ injectedTags={injectedTags}
75
+ excludeTags={excludeTags}
76
+ userLocale={userLocale}
77
+ selectedOfferDetails={selectedOfferDetails}
78
+ eventContextTags={eventContextTags}
79
+ waitEventContextTags={waitEventContextTags}
80
+ style={tagListStyle}
81
+ popoverPlacement={popoverPlacement}
82
+ restrictPersonalization={restrictPersonalization}
83
+ />
84
+ );
85
+
86
+ const inputElement = showInput && (
87
+ <CapInput
88
+ id={inputId}
89
+ onChange={inputOnChange}
90
+ placeholder={inputPlaceholder}
91
+ value={inputValue}
92
+ size={inputSize}
93
+ maxLength={inputMaxLength}
94
+ errorMessage={inputErrorMessage}
95
+ isRequired={inputRequired}
96
+ disabled={inputDisabled}
97
+ style={inputStyle}
98
+ {...inputProps}
99
+ />
100
+ );
101
+
102
+ if (singleLineLayout) {
103
+ return (
104
+ <CapColumn style={containerStyle}>
105
+ {/* "top", not "middle": keeps the buttons in place when an error grows the input */}
106
+ <CapRow align="top" type="flex" noWrap gap={8} className="cap-tag-list-with-input__single-line-row">
107
+ <CapColumn className="cap-tag-list-with-input__input-column">{inputElement}</CapColumn>
108
+ {extraContent}
109
+ {tagListElement}
110
+ </CapRow>
111
+ </CapColumn>
112
+ );
113
+ }
114
+
58
115
  return (
59
116
  <CapColumn style={containerStyle}>
60
117
  <CapRow align="middle" type="flex" noWrap>
@@ -64,42 +121,9 @@ export const CapTagListWithInput = (props) => {
64
121
  </CapHeading>
65
122
  )}
66
123
 
67
- {showTagList && (
68
- <TagList
69
- key={`${inputId}_tags`}
70
- className={className}
71
- moduleFilterEnabled={moduleFilterEnabled}
72
- label={formatMessage(messages.addLabels)}
73
- onTagSelect={onTagSelect}
74
- onContextChange={onContextChange}
75
- location={location}
76
- tags={tags}
77
- injectedTags={injectedTags}
78
- userLocale={userLocale}
79
- selectedOfferDetails={selectedOfferDetails}
80
- eventContextTags={eventContextTags}
81
- waitEventContextTags={waitEventContextTags}
82
- style={tagListStyle}
83
- popoverPlacement={popoverPlacement}
84
- restrictPersonalization={restrictPersonalization}
85
- />
86
- )}
124
+ {tagListElement}
87
125
  </CapRow>
88
- {showInput && (
89
- <CapInput
90
- id={inputId}
91
- onChange={inputOnChange}
92
- placeholder={inputPlaceholder}
93
- value={inputValue}
94
- size={inputSize}
95
- maxLength={inputMaxLength}
96
- errorMessage={inputErrorMessage}
97
- isRequired={inputRequired}
98
- disabled={inputDisabled}
99
- style={inputStyle}
100
- {...inputProps}
101
- />
102
- )}
126
+ {inputElement}
103
127
  </CapColumn>
104
128
  );
105
129
  };
@@ -109,6 +133,7 @@ CapTagListWithInput.propTypes = {
109
133
  // TagList props
110
134
  tags: PropTypes.array,
111
135
  injectedTags: PropTypes.object,
136
+ excludeTags: PropTypes.array,
112
137
  location: PropTypes.object,
113
138
  selectedOfferDetails: PropTypes.array,
114
139
  onTagSelect: PropTypes.func.isRequired,
@@ -145,6 +170,8 @@ CapTagListWithInput.propTypes = {
145
170
  showTagList: PropTypes.bool,
146
171
  showInput: PropTypes.bool,
147
172
  popoverPlacement: PropTypes.string,
173
+ singleLineLayout: PropTypes.bool,
174
+ extraContent: PropTypes.node,
148
175
  };
149
176
 
150
177
  CapTagListWithInput.defaultProps = {
@@ -176,6 +203,8 @@ CapTagListWithInput.defaultProps = {
176
203
  showInput: true,
177
204
  inputProps: {},
178
205
  popoverPlacement: undefined,
206
+ singleLineLayout: false,
207
+ extraContent: null,
179
208
  };
180
209
 
181
210
  export default injectIntl(CapTagListWithInput);
@@ -0,0 +1,9 @@
1
+ .cap-tag-list-with-input__single-line-row {
2
+ width: 100%;
3
+ }
4
+
5
+ // Input takes the remaining width and can shrink below its content size
6
+ .cap-tag-list-with-input__input-column {
7
+ flex: 1;
8
+ min-width: 0;
9
+ }
@@ -22,6 +22,7 @@ import {
22
22
  const EmailPreviewContent = ({
23
23
  content,
24
24
  subject,
25
+ preheader,
25
26
  device,
26
27
  isUpdating,
27
28
  error,
@@ -75,6 +76,11 @@ const EmailPreviewContent = ({
75
76
  </CapRow>
76
77
  </CapRow>
77
78
 
79
+ {/* Email Pre-header */}
80
+ {preheader && (
81
+ <CapRow useLegacy className="email-preheader">{preheader}</CapRow>
82
+ )}
83
+
78
84
  {/* Email Content - Varies based on state */}
79
85
  <CapRow useLegacy className="email-content">
80
86
  {contentSection}
@@ -131,6 +137,7 @@ const EmailPreviewContent = ({
131
137
  EmailPreviewContent.propTypes = {
132
138
  content: PropTypes.string,
133
139
  subject: PropTypes.string,
140
+ preheader: PropTypes.string,
134
141
  device: PropTypes.oneOf([DESKTOP, MOBILE, TABLET]),
135
142
  isUpdating: PropTypes.bool,
136
143
  error: PropTypes.string,
@@ -140,6 +147,7 @@ EmailPreviewContent.propTypes = {
140
147
  EmailPreviewContent.defaultProps = {
141
148
  content: '',
142
149
  subject: '',
150
+ preheader: '',
143
151
  device: DESKTOP,
144
152
  isUpdating: false,
145
153
  error: null,
@@ -520,6 +520,13 @@
520
520
  }
521
521
  }
522
522
 
523
+ // Email Pre-header
524
+ .email-preheader {
525
+ padding: $CAP_SPACE_12 $CAP_SPACE_16;
526
+ font-size: $FONT_SIZE_M;
527
+ color: #8c8c8c;
528
+ }
529
+
523
530
  // Email Content
524
531
  .email-content {
525
532
  border-top: $CAP_SPACE_01 solid $CAP_G12;
@@ -87,6 +87,7 @@ const UnifiedPreview = ({
87
87
  <EmailPreviewContent
88
88
  content={content?.resolvedBody || ''}
89
89
  subject={content?.resolvedTitle || ''}
90
+ preheader={content?.resolvedPreheader || ''}
90
91
  device={device}
91
92
  isUpdating={isUpdating}
92
93
  error={error}
@@ -43,7 +43,7 @@ export const searchCustomersRequested = (query) => ({
43
43
  */
44
44
  export const extractTagsRequested = (payload) => ({
45
45
  type: EXTRACT_TAGS_REQUESTED,
46
- payload, // { templateSubject, templateContent }
46
+ payload, // { templateSubject, templatePreheader, templateContent }
47
47
  });
48
48
 
49
49
  /**
@@ -947,6 +947,7 @@ const CommonTestAndPreview = (props) => {
947
947
  return {
948
948
  ...basePayload,
949
949
  messageTitle: resolveTagsInText(formDataObj['template-subject'], customValuesObj),
950
+ messageSubtitle: resolveTagsInText(formDataObj['template-preheader'], customValuesObj),
950
951
  messageBody: contentStr,
951
952
  };
952
953
 
@@ -1069,6 +1070,7 @@ const CommonTestAndPreview = (props) => {
1069
1070
  case CHANNELS.EMAIL:
1070
1071
  return {
1071
1072
  templateSubject: formDataObj['template-subject'] || '',
1073
+ templatePreheader: formDataObj['template-preheader'] || '',
1072
1074
  templateContent: contentStr,
1073
1075
  };
1074
1076
 
@@ -1410,13 +1412,22 @@ const CommonTestAndPreview = (props) => {
1410
1412
  || formDataObj?.base?.['template-subject']
1411
1413
  || '';
1412
1414
 
1415
+ // Extract pre-header from formData (same shapes as subject)
1416
+ const preheader = formDataObj?.['template-preheader']
1417
+ || formDataObj?.[0]?.['template-preheader']
1418
+ || formDataObj?.base?.['template-preheader']
1419
+ || '';
1420
+
1413
1421
  // Extract body from previewData (resolved) or content
1414
1422
  const messageBody = previewDataObj?.resolvedBody || contentStr || '';
1415
1423
 
1416
- // Resolve tags in subject if custom values are provided
1424
+ // Resolve tags in subject/pre-header if custom values are provided
1417
1425
  const resolvedSubject = customValuesObj && Object.keys(customValuesObj).length > 0
1418
1426
  ? resolveTagsInText(subject, customValuesObj)
1419
1427
  : subject;
1428
+ const resolvedPreheader = customValuesObj && Object.keys(customValuesObj).length > 0
1429
+ ? resolveTagsInText(preheader, customValuesObj)
1430
+ : preheader;
1420
1431
 
1421
1432
  const emailChannelSettings = {
1422
1433
  channel: CHANNELS.EMAIL,
@@ -1442,6 +1453,7 @@ const CommonTestAndPreview = (props) => {
1442
1453
  channel: CHANNELS.EMAIL,
1443
1454
  messageBody,
1444
1455
  messageSubject: resolvedSubject,
1456
+ messagePreHeader: resolvedPreheader,
1445
1457
  },
1446
1458
  };
1447
1459
  }
@@ -2587,6 +2599,10 @@ const CommonTestAndPreview = (props) => {
2587
2599
  resolvedTitle: hasPreviewCallBeenMade && previewDataHtml?.resolvedTitle
2588
2600
  ? previewDataHtml.resolvedTitle
2589
2601
  : formData?.['template-subject'] || '',
2602
+ // The liquid preview API returns the resolved pre-header as `resolvedSubtitle`.
2603
+ resolvedPreheader: hasPreviewCallBeenMade && previewDataHtml?.resolvedSubtitle
2604
+ ? previewDataHtml.resolvedSubtitle
2605
+ : formData?.['template-preheader'] || '',
2590
2606
  };
2591
2607
  } else if (channel === CHANNELS.SMS) {
2592
2608
  // For SMS, content is the text message (string)
@@ -2931,12 +2947,12 @@ const CommonTestAndPreview = (props) => {
2931
2947
  });
2932
2948
 
2933
2949
  // Always extract tags when content changes
2934
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
2950
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
2935
2951
  channel,
2936
2952
  formData,
2937
2953
  htmlFile,
2938
2954
  );
2939
- actions.extractTagsRequested({ templateSubject, templateContent });
2955
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
2940
2956
  }
2941
2957
 
2942
2958
  // Restore original handler
@@ -2951,32 +2967,32 @@ const CommonTestAndPreview = (props) => {
2951
2967
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2952
2968
  if (templateContent) {
2953
2969
  // Extract tags when showing (but don't set previewDataHtml - use raw content initially)
2954
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
2970
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
2955
2971
  channel,
2956
2972
  formData,
2957
2973
  templateContent
2958
2974
  );
2959
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
2975
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
2960
2976
  } else {
2961
2977
  // Fallback to content prop if no template content
2962
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
2978
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
2963
2979
  channel,
2964
2980
  formData,
2965
2981
  getCurrentContent
2966
2982
  );
2967
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
2983
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
2968
2984
  }
2969
2985
  }
2970
2986
  } else {
2971
2987
  // For non-email channels, extract tags from content
2972
2988
  // Use getCurrentContent for SMS (extracts from formData) or content prop for others
2973
2989
  const contentToConvert = getCurrentContent;
2974
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
2990
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
2975
2991
  channel,
2976
2992
  formData || {},
2977
2993
  contentToConvert
2978
2994
  );
2979
- actions.extractTagsRequested({ templateSubject, templateContent });
2995
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
2980
2996
  }
2981
2997
 
2982
2998
  // Fetch test customers and groups if enabled
@@ -3057,12 +3073,12 @@ const CommonTestAndPreview = (props) => {
3057
3073
  const handleContentUpdate = (contentToUpdate) => {
3058
3074
  // Extract tags from content (but don't set previewDataHtml - use raw content initially)
3059
3075
  // previewDataHtml will only be set when preview API call is made
3060
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
3076
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
3061
3077
  channel,
3062
3078
  formData,
3063
3079
  contentToUpdate
3064
3080
  );
3065
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
3081
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
3066
3082
  };
3067
3083
 
3068
3084
  if (isDragDrop) {
@@ -3504,12 +3520,12 @@ const CommonTestAndPreview = (props) => {
3504
3520
  }
3505
3521
 
3506
3522
  setTagsExtracted(true);
3507
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
3523
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
3508
3524
  activeChannelForActions,
3509
3525
  formData || {},
3510
3526
  contentToExtract
3511
3527
  );
3512
- actions.extractTagsRequested({ templateSubject, templateContent });
3528
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
3513
3529
  };
3514
3530
 
3515
3531
  /**
@@ -70,10 +70,11 @@ export function* searchCustomersSaga(action) {
70
70
  // Extract Tags Saga
71
71
  export function* extractTagsSaga(action) {
72
72
  try {
73
- const { templateSubject, templateContent } = action.payload;
73
+ const { templateSubject, templatePreheader, templateContent } = action.payload;
74
74
 
75
75
  const payload = {
76
76
  messageTitle: templateSubject || '',
77
+ messageSubtitle: templatePreheader || '',
77
78
  messageBody: templateContent || '',
78
79
  };
79
80
 
@@ -481,6 +481,33 @@ describe('EmailPreviewContent', () => {
481
481
 
482
482
  expect(container.querySelector('.email-content')).toBeTruthy();
483
483
  });
484
+
485
+ it('should not render a pre-header row when preheader is not provided', () => {
486
+ const { container } = render(
487
+ <TestWrapper>
488
+ <ComponentToRender {...defaultProps} />
489
+ </TestWrapper>
490
+ );
491
+
492
+ expect(container.querySelector('.email-preheader')).toBeNull();
493
+ });
494
+
495
+ it('should render the pre-header text when provided', () => {
496
+ const props = {
497
+ ...defaultProps,
498
+ preheader: 'Unlock 20% off your next order',
499
+ };
500
+
501
+ const { container } = render(
502
+ <TestWrapper>
503
+ <ComponentToRender {...props} />
504
+ </TestWrapper>
505
+ );
506
+
507
+ const preheaderElement = container.querySelector('.email-preheader');
508
+ expect(preheaderElement).toBeTruthy();
509
+ expect(preheaderElement.textContent).toBe('Unlock 20% off your next order');
510
+ });
484
511
  });
485
512
 
486
513
  describe('Device Handling', () => {
@@ -46,6 +46,7 @@ jest.mock('../../UnifiedPreview/EmailPreviewContent', () => ({
46
46
  <div data-testid="email-preview">
47
47
  <div data-testid="email-content">{props.content}</div>
48
48
  <div data-testid="email-subject">{props.subject}</div>
49
+ <div data-testid="email-preheader">{props.preheader}</div>
49
50
  <div data-testid="email-device">{props.device}</div>
50
51
  </div>
51
52
  ),
@@ -288,6 +289,45 @@ describe('UnifiedPreview', () => {
288
289
  expect(screen.getByTestId('email-subject')).toHaveTextContent('Email Subject');
289
290
  });
290
291
 
292
+ it('should pass resolvedPreheader through as the preheader prop', () => {
293
+ const props = {
294
+ ...defaultProps,
295
+ channel: CHANNELS.EMAIL,
296
+ content: {
297
+ resolvedBody: '<html>Email Content</html>',
298
+ resolvedTitle: 'Email Subject',
299
+ resolvedPreheader: 'Unlock 20% off',
300
+ },
301
+ };
302
+
303
+ render(
304
+ <TestWrapper>
305
+ <ComponentToRender {...props} />
306
+ </TestWrapper>
307
+ );
308
+
309
+ expect(screen.getByTestId('email-preheader')).toHaveTextContent('Unlock 20% off');
310
+ });
311
+
312
+ it('should default preheader to an empty string when resolvedPreheader is absent', () => {
313
+ const props = {
314
+ ...defaultProps,
315
+ channel: CHANNELS.EMAIL,
316
+ content: {
317
+ resolvedBody: '<html>Email Content</html>',
318
+ resolvedTitle: 'Email Subject',
319
+ },
320
+ };
321
+
322
+ render(
323
+ <TestWrapper>
324
+ <ComponentToRender {...props} />
325
+ </TestWrapper>
326
+ );
327
+
328
+ expect(screen.getByTestId('email-preheader')).toHaveTextContent('');
329
+ });
330
+
291
331
  it('should handle empty resolvedBody', () => {
292
332
  const props = {
293
333
  ...defaultProps,