@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
@@ -31,6 +31,7 @@ import CapHeading from '@capillarytech/cap-ui-library/CapHeading';
31
31
  import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
32
32
  import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
33
33
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
34
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
34
35
 
35
36
  import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
36
37
  import LabelHOC from '@capillarytech/cap-ui-library/assets/HOCs/ComponentWithLabelHOC';
@@ -61,7 +62,7 @@ import { convert } from 'html-to-text';
61
62
  import { OUTBOUND, ADD_LANGUAGE, UPLOAD, USE_EDITOR, COPY_PRIMARY_LANGUAGE, GLOBAL_CONVERT_OPTIONS } from './constants';
62
63
  import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
63
64
  import moment from 'moment';
64
- import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
65
+ import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS, UNSUBSCRIBE_TAG} from '../../constants/unified';
65
66
  import { REQUEST } from '../../v2Containers/Cap/constants'
66
67
  import { isEmailUnsubscribeTagOptional, isAiContentBotDisabled } from '../../utils/common';
67
68
  import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
@@ -70,6 +71,7 @@ import { getChannelData, hasPersonalizationTags, validateLiquidTemplateContent,
70
71
  const {Column} = Table;
71
72
  const {TextArea} = CapInput;
72
73
  const {CapRadioGroup} = CapRadio;
74
+ const {CapLabelInline} = CapLabel;
73
75
  // import styled from 'styled-components';
74
76
 
75
77
  const tagsTypes = {
@@ -110,21 +112,32 @@ class HighFreqInput extends React.Component {
110
112
  }
111
113
  }
112
114
 
113
- // Isolated wrapper for EMAIL template-subject: blur-only commit, same as HighFreqInput.
115
+ // EMAIL fields kept visible in BEE mode; use the blur-commit HighFreqTagInput below.
116
+ const HIGH_FREQ_EMAIL_TAG_INPUT_IDS = ['template-subject', 'template-preheader'];
117
+
118
+ // EMAIL subject/preheader wrapper: commits on blur, like HighFreqInput.
114
119
  class HighFreqTagInput extends React.Component {
115
120
  constructor(props) {
116
121
  super(props);
117
- this.state = { localInputValue: props.inputValue || '' };
122
+ this.state = { localInputValue: props.inputValue || '', errorDismissed: false };
118
123
  }
119
124
 
120
125
  componentDidUpdate(prevProps) {
121
126
  if (prevProps.inputValue !== this.props.inputValue && this.state.localInputValue !== this.props.inputValue) {
122
127
  this.setState({ localInputValue: this.props.inputValue || '' });
123
128
  }
129
+ // New errorDataRef = fresh validation (each Save): un-dismiss so a repeated error shows again.
130
+ if (this.props.errorDataRef !== undefined && prevProps.errorDataRef !== this.props.errorDataRef && this.state.errorDismissed) {
131
+ this.setState({ errorDismissed: false });
132
+ }
124
133
  }
125
134
 
126
135
  handleInputChange = (e) => {
127
136
  this.setState({ localInputValue: e.target.value });
137
+ // Hide a stale error on edit (like the HTML editor's Pre-header); re-validates only on Save.
138
+ if (this.props.errorDataRef !== undefined && !this.state.errorDismissed) {
139
+ this.setState({ errorDismissed: true });
140
+ }
128
141
  };
129
142
 
130
143
  handleBlur = () => {
@@ -132,13 +145,39 @@ class HighFreqTagInput extends React.Component {
132
145
  };
133
146
 
134
147
  render() {
135
- const { inputValue: _iv, onCommit: _oc, inputOnChange: _ic, ...rest } = this.props;
148
+ const {
149
+ inputValue: _iv, onCommit: _oc, inputOnChange: _ic, inputMaxLength, onRemove, removeLabel,
150
+ errorDataRef, inputErrorMessage, ...rest
151
+ } = this.props;
152
+ const counterSuffix = inputMaxLength ? (
153
+ <CapLabelInline type="label1" className="high-freq-tag-input-counter">
154
+ {`${this.state.localInputValue.length} / ${inputMaxLength}`}
155
+ </CapLabelInline>
156
+ ) : null;
157
+ const removeSuffix = onRemove ? (
158
+ <CapLabelInline type="label1" role="button" tabIndex={0} className="high-freq-tag-input-remove-link" onClick={onRemove}>
159
+ {removeLabel}
160
+ </CapLabelInline>
161
+ ) : null;
162
+ const combinedSuffix = (counterSuffix || removeSuffix) ? (
163
+ <CapRow align="middle" type="flex" noWrap gap={8}>
164
+ {counterSuffix}
165
+ {removeSuffix}
166
+ </CapRow>
167
+ ) : undefined;
136
168
  return (
137
169
  <CapTagListWithInput
138
170
  {...rest}
171
+ inputMaxLength={inputMaxLength}
139
172
  inputValue={this.state.localInputValue}
140
173
  inputOnChange={this.handleInputChange}
141
- inputProps={{ ...(this.props.inputProps || {}), onBlur: this.handleBlur }}
174
+ inputErrorMessage={this.state.errorDismissed ? '' : inputErrorMessage}
175
+ inputProps={{
176
+ ...(this.props.inputProps || {}),
177
+ onBlur: this.handleBlur,
178
+ ...(combinedSuffix ? { suffix: combinedSuffix } : {}),
179
+ }}
180
+ singleLineLayout
142
181
  />
143
182
  );
144
183
  }
@@ -169,6 +208,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
169
208
  STANDARD_ERROR_MSG: [],
170
209
  LIQUID_ERROR_MSG: [],
171
210
  },
211
+ // Pre-header visibility: null = show if it has content; true/false = user's Add/Remove click.
212
+ preheaderRevealed: null,
172
213
  };
173
214
  this.renderForm = this.renderForm.bind(this);
174
215
  this.updateFormData = this.updateFormData.bind(this);
@@ -1245,6 +1286,16 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1245
1286
  }
1246
1287
  }
1247
1288
 
1289
+ // Pre-header must not contain the unsubscribe tag (checked on Save, not per keystroke).
1290
+ if (index === 'template-preheader') {
1291
+ if (hasUnsubscribeTag(data)) {
1292
+ errorData[index] = true;
1293
+ isValid = false;
1294
+ } else {
1295
+ errorData[index] = false;
1296
+ }
1297
+ }
1298
+
1248
1299
  // Check if Template name present in full mode
1249
1300
  if(type !== 'embedded' && index === 'template-name' && data === '') {
1250
1301
  errorData[index] = true;
@@ -3447,6 +3498,18 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3447
3498
  let contentSections = [];
3448
3499
  let popoverSections = [];
3449
3500
  const self = this;
3501
+ // Pre-header visibility: null = show if it has content; true/false = user's Add/Remove click.
3502
+ const preheaderValueForRenderSection = this.state.formData['template-preheader'] || '';
3503
+ const isPreheaderRevealed = this.state.preheaderRevealed !== null
3504
+ ? this.state.preheaderRevealed
3505
+ : !!preheaderValueForRenderSection;
3506
+ // Hidden Pre-header section renders no fields by design — not the "not ready" spinner case below.
3507
+ const isHiddenPreheaderOnlySection = !isPreheaderRevealed
3508
+ && section.inputFields
3509
+ && section.inputFields.length === 1
3510
+ && section.inputFields[0].cols
3511
+ && section.inputFields[0].cols.length === 1
3512
+ && section.inputFields[0].cols[0].id === 'template-preheader';
3450
3513
  _.forEach(section.inputFields, (field) => {
3451
3514
  const columns = [];
3452
3515
  _.forEach(field.cols, (val) => {
@@ -3807,14 +3870,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3807
3870
  const isBEEAppEnableForCapTagList = this.checkBeeEditorAllowedForLibrary();
3808
3871
  ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
3809
3872
 
3810
- // Always render Subject CapTagListWithInput even when Bee editor is active for EMAIL channel
3873
+ // Pre-header hidden behind "+ Pre-header" unless it has content or the user toggled it.
3874
+ if (val.id === 'template-preheader' && !isPreheaderRevealed) {
3875
+ break;
3876
+ }
3877
+
3878
+ // Subject/Pre-header render even in BEE mode
3811
3879
  if (
3812
- val.id === 'template-subject' ||
3880
+ HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id) ||
3813
3881
  !(_.get(this.state, `formData[${this.state.currentTab - 1}][${currentLangForCapTagList}].is_drag_drop`, false)) ||
3814
3882
  isBEEAppEnableForCapTagList === false ||
3815
3883
  channelForCapTagList !== 'EMAIL'
3816
3884
  ) {
3817
- const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && val.id === 'template-subject';
3885
+ const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id);
3818
3886
  const tagListProps = {
3819
3887
  key: `input-${val.id}`,
3820
3888
  inputId: val.id,
@@ -3823,9 +3891,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3823
3891
  inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
3824
3892
  inputRequired: val.required || false,
3825
3893
  inputDisabled: val.disabled || false,
3894
+ inputMaxLength: val.maxLength,
3826
3895
  headingText: val.label || '',
3827
3896
  headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
3828
3897
  headingType: "h4",
3898
+ // Label is an inline input prefix (see inputProps), so no heading here.
3899
+ showHeading: !isEmailStandaloneSubject,
3829
3900
  onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
3830
3901
  onContextChange: this.props.onContextChange,
3831
3902
  location: this.props.location,
@@ -3838,10 +3909,42 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3838
3909
  waitEventContextTags: this.props?.waitEventContextTags,
3839
3910
  moduleFilterEnabled: moduleFilterEnabledForCapTagList,
3840
3911
  containerStyle: val.style || {},
3841
- inputProps: val.inputProps || {},
3912
+ inputProps: isEmailStandaloneSubject
3913
+ ? {
3914
+ ...(val.inputProps || {}),
3915
+ prefix: (
3916
+ <CapLabelInline type="label2" className="high-freq-tag-input-prefix-label">
3917
+ {val.label || ''}
3918
+ </CapLabelInline>
3919
+ ),
3920
+ }
3921
+ : (val.inputProps || {}),
3842
3922
  showInput: val.showInput !== false,
3843
3923
  showTagList: val.showTagList !== false,
3844
3924
  restrictPersonalization: this.props.restrictPersonalization,
3925
+ ...(val.id === 'template-subject' && !isPreheaderRevealed ? {
3926
+ extraContent: (
3927
+ <CapButton
3928
+ type="dashed"
3929
+ size="small"
3930
+ style={{ whiteSpace: 'nowrap' }}
3931
+ onClick={() => this.setState({ preheaderRevealed: true })}
3932
+ >
3933
+ {this.props.intl.formatMessage(messages.addPreheader)}
3934
+ </CapButton>
3935
+ ),
3936
+ } : {}),
3937
+ ...(val.id === 'template-preheader' ? {
3938
+ onRemove: () => {
3939
+ this.performFormDataUpdate('', val);
3940
+ this.setState({ preheaderRevealed: false });
3941
+ },
3942
+ removeLabel: this.props.intl.formatMessage(messages.removePreheader),
3943
+ // Unsubscribe is not a valid Pre-header tag — hide it from the Add Label dropdown.
3944
+ excludeTags: [UNSUBSCRIBE_TAG],
3945
+ // New ref per Save/Done click; lets HighFreqTagInput hide stale errors while editing.
3946
+ errorDataRef: this.state.errorData,
3947
+ } : {}),
3845
3948
  };
3846
3949
  columns.push(
3847
3950
  <CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
@@ -4249,7 +4352,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4249
4352
  fields.push(row);
4250
4353
  }
4251
4354
  });
4252
- if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length) {
4355
+ if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length && !isHiddenPreheaderOnlySection) {
4253
4356
  return (
4254
4357
  <div style={{ textAlign: 'center' }}>
4255
4358
  <CapSpin spinning />
@@ -136,4 +136,22 @@
136
136
  }
137
137
  .error-boundary{
138
138
  border: $CAP_SPACE_02 solid $CAP_RED;
139
+ }
140
+
141
+ // Suffix controls (character counter, Remove link) rendered alongside EMAIL's
142
+ // Subject/Pre-header inputs by HighFreqTagInput (Classic.js).
143
+ .high-freq-tag-input-counter {
144
+ white-space: nowrap;
145
+ }
146
+
147
+ .high-freq-tag-input-remove-link {
148
+ white-space: nowrap;
149
+ cursor: pointer;
150
+ }
151
+
152
+ .high-freq-tag-input-prefix-label {
153
+ font-weight: 600;
154
+ padding-right: $CAP_SPACE_08;
155
+ margin-right: $CAP_SPACE_08;
156
+ border-right: 1px solid $CAP_COLOR_16;
139
157
  }
@@ -98,4 +98,12 @@ export default defineMessages({
98
98
  id: `creatives.componentsV2.FormBuilder.personalizationTagsErrorMessage`,
99
99
  defaultMessage: 'Personalization tags are not supported for anonymous customers, please remove the tags.',
100
100
  },
101
+ addPreheader: {
102
+ id: 'creatives.componentsV2.FormBuilder.addPreheader',
103
+ defaultMessage: '+ Pre-header',
104
+ },
105
+ removePreheader: {
106
+ id: 'creatives.componentsV2.FormBuilder.removePreheader',
107
+ defaultMessage: 'Remove',
108
+ },
101
109
  });
@@ -8,6 +8,7 @@ import { shallowWithIntl } from '../../../helpers/intl-enzym-test-helpers';
8
8
  const { match, location, capGlobal, language } = mockData;
9
9
 
10
10
  jest.mock('@capillarytech/cap-ui-utils', () => ({
11
+ getEnvVariable: jest.fn(),
11
12
  GA: {
12
13
  initialize: jest.fn(),
13
14
  timeTracker: {
@@ -479,7 +479,7 @@ describe('handleSave — CCS flow', () => {
479
479
  initialData: {
480
480
  contentItems: [{
481
481
  channel: 'EMAIL',
482
- templateData: { emailSubject: 'Order Confirmed', emailBody: '<html>Hi</html>' },
482
+ templateData: { emailSubject: 'Order Confirmed', emailBody: '<html>Hi</html>', emailPreHeader: 'Your order is on its way' },
483
483
  }],
484
484
  },
485
485
  onSave,
@@ -497,6 +497,7 @@ describe('handleSave — CCS flow', () => {
497
497
  channel: 'EMAIL',
498
498
  messageSubject: 'Order Confirmed',
499
499
  messageBody: '<html>Hi</html>',
500
+ messagePreHeader: 'Your order is on its way',
500
501
  },
501
502
  }),
502
503
  }),
@@ -10,20 +10,20 @@ describe('CCS_CONTENT_TRANSFORMS', () => {
10
10
  it('prefers emailBody over emailHtml when both are present', () => {
11
11
  expect(
12
12
  CCS_CONTENT_TRANSFORMS.EMAIL({
13
- emailSubject: 'Subject', emailBody: '<p>Body</p>', emailHtml: '<p>Html</p>',
13
+ emailSubject: 'Subject', emailBody: '<p>Body</p>', emailHtml: '<p>Html</p>', emailPreHeader: 'Preheader',
14
14
  }),
15
- ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Body</p>' });
15
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Body</p>', messagePreHeader: 'Preheader' });
16
16
  });
17
17
 
18
18
  it('falls back to emailHtml when emailBody is absent', () => {
19
19
  expect(
20
20
  CCS_CONTENT_TRANSFORMS.EMAIL({ emailSubject: 'Subject', emailHtml: '<p>Html</p>' }),
21
- ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Html</p>' });
21
+ ).toEqual({ messageSubject: 'Subject', messageBody: '<p>Html</p>', messagePreHeader: undefined });
22
22
  });
23
23
 
24
24
  it('defaults templateData to {} when called with no argument', () => {
25
25
  expect(CCS_CONTENT_TRANSFORMS.EMAIL()).toEqual({
26
- messageSubject: undefined, messageBody: undefined,
26
+ messageSubject: undefined, messageBody: undefined, messagePreHeader: undefined,
27
27
  });
28
28
  });
29
29
  });
@@ -65,6 +65,7 @@ export const CCS_CONTENT_TRANSFORMS = {
65
65
  EMAIL: (templateData = {}) => ({
66
66
  messageSubject: templateData.emailSubject,
67
67
  messageBody: templateData.emailBody || templateData.emailHtml,
68
+ messagePreHeader: templateData.emailPreHeader,
68
69
  }),
69
70
  SMS: (templateData = {}) => ({
70
71
  message: templateData.messageBody || '',
@@ -101,7 +101,7 @@ describe('mapCommDefinitionToStepData', () => {
101
101
  singleChannelStrategy: {
102
102
  variant: {
103
103
  channel: 'EMAIL',
104
- emailMessageContent: { messageSubject: 'Subject', messageBody: '<p>Body</p>' },
104
+ emailMessageContent: { messageSubject: 'Subject', messageBody: '<p>Body</p>', messagePreHeader: 'Preheader' },
105
105
  },
106
106
  },
107
107
  };
@@ -111,6 +111,7 @@ describe('mapCommDefinitionToStepData', () => {
111
111
  expect(result.contentItems[0].templateData).toEqual({
112
112
  emailSubject: 'Subject',
113
113
  emailBody: '<p>Body</p>',
114
+ emailPreHeader: 'Preheader',
114
115
  channel: 'EMAIL',
115
116
  });
116
117
  });
@@ -27,6 +27,7 @@ const CCS_CONTENT_REVERSE_TRANSFORMS = {
27
27
  EMAIL: (payload = {}) => ({
28
28
  emailSubject: payload.messageSubject,
29
29
  emailBody: payload.messageBody,
30
+ emailPreHeader: payload.messagePreHeader,
30
31
  }),
31
32
  SMS: (payload = {}) => ({
32
33
  messageBody: payload.message || '',
@@ -320,7 +320,7 @@ export class Creatives extends React.Component {
320
320
  };
321
321
 
322
322
  onEditTemplate = () => {
323
- if (commonUtil.hasCreativesArchivalEnabled() && this.props.templateData?.isArchived) {
323
+ if (this.props.templateData?.isArchived) {
324
324
  CapNotification.error({ message: this.props.intl.formatMessage(messages.cannotEditArchivedTemplate) });
325
325
  return;
326
326
  }
@@ -503,6 +503,7 @@ export class Creatives extends React.Component {
503
503
  case constants.EMAIL: {
504
504
  const formData = {
505
505
  subject: templateData.emailSubject,
506
+ preheader: templateData.emailPreHeader,
506
507
  html_content: templateData.emailBody,
507
508
  is_drag_drop: templateData.is_drag_drop,
508
509
  lang_id: templateData.lang_id,
@@ -1092,10 +1093,15 @@ export class Creatives extends React.Component {
1092
1093
  }
1093
1094
  const newHtmlContent = await updateImagesInHtml(html_content);
1094
1095
  templateData = {
1095
- ...templateData, ...emailBase, emailBody: newHtmlContent, emailSubject: (emailBase && emailBase.subject) ? emailBase.subject : '',
1096
+ ...templateData,
1097
+ ...emailBase,
1098
+ emailBody: newHtmlContent,
1099
+ emailSubject: (emailBase && emailBase.subject) ? emailBase.subject : '',
1100
+ emailPreHeader: (emailBase && emailBase.preheader) ? emailBase.preheader : '',
1096
1101
  };
1097
1102
  delete templateData.html_content;
1098
1103
  delete templateData.subject;
1104
+ delete templateData.preheader;
1099
1105
  }
1100
1106
  break;
1101
1107
  case constants.MOBILE_PUSH:
@@ -2472,7 +2478,7 @@ export class Creatives extends React.Component {
2472
2478
  onSave={this.saveMessage}
2473
2479
  onDiscard={this.discardMessage}
2474
2480
  onEditTemplate={this.onEditTemplate}
2475
- isTemplateArchived={!!(commonUtil.hasCreativesArchivalEnabled() && this.props.templateData && this.props.templateData.isArchived)}
2481
+ isTemplateArchived={!!(this.props.templateData && this.props.templateData.isArchived)}
2476
2482
  slidBoxContent={slidBoxContent}
2477
2483
  onCreateNextStep={this.onCreateNextStep}
2478
2484
  currentChannel={currentChannel.toUpperCase()}
@@ -376,4 +376,85 @@ describe('Test SlideBoxContent container', () => {
376
376
  expect(getCreativesData).toHaveBeenCalledTimes(1);
377
377
  expect(getCreativesData).toHaveBeenCalledWith({ channel: 'EMAIL'});
378
378
  });
379
+
380
+ it('EMAIL getFormData maps preheader to emailPreHeader for campaigns, alongside subject/body', async () => {
381
+ renderFunction('EMAIL', 'editTemplate', emailTemplates, emailEditTemplateData);
382
+ renderedComponent.instance().onEditTemplate();
383
+
384
+ const mockValue = {
385
+ validity: true,
386
+ value: {
387
+ name: 'email_creative_name',
388
+ type: 'EMAIL',
389
+ base: {
390
+ subject: 'Test subject',
391
+ preheader: 'Test preheader',
392
+ html_content: '<p>hello</p>',
393
+ },
394
+ },
395
+ };
396
+
397
+ renderedComponent.instance().getFormData(mockValue);
398
+ await tick();
399
+
400
+ expect(getCreativesData).toHaveBeenCalledTimes(1);
401
+ const sentData = getCreativesData.mock.calls[0][0];
402
+ expect(sentData.emailSubject).toBe('Test subject');
403
+ expect(sentData.emailPreHeader).toBe('Test preheader');
404
+ expect(sentData.emailBody).toBe('<p>hello</p>');
405
+ expect(sentData.subject).toBeUndefined();
406
+ expect(sentData.preheader).toBeUndefined();
407
+ expect(sentData.html_content).toBeUndefined();
408
+ });
409
+
410
+ it('EMAIL getFormData maps emailPreHeader to an empty string when not present', async () => {
411
+ renderFunction('EMAIL', 'editTemplate', emailTemplates, emailEditTemplateData);
412
+ renderedComponent.instance().onEditTemplate();
413
+
414
+ const mockValue = {
415
+ validity: true,
416
+ value: {
417
+ name: 'email_creative_name',
418
+ type: 'EMAIL',
419
+ base: {
420
+ subject: 'Test subject',
421
+ html_content: '<p>hello</p>',
422
+ },
423
+ },
424
+ };
425
+
426
+ renderedComponent.instance().getFormData(mockValue);
427
+ await tick();
428
+
429
+ expect(getCreativesData).toHaveBeenCalledTimes(1);
430
+ const sentData = getCreativesData.mock.calls[0][0];
431
+ expect(sentData.emailPreHeader).toBe('');
432
+ });
433
+
434
+ it('EMAIL getTemplateData maps emailPreHeader back to preheader when reopening a saved campaign message', () => {
435
+ renderedComponent = shallowWithIntl(
436
+ <Creatives
437
+ loyaltyMetaData={loyaltyMetaData}
438
+ Templates={emailTemplates}
439
+ channel="EMAIL"
440
+ slidBoxContent="editTemplate"
441
+ templateData={{
442
+ channel: 'EMAIL',
443
+ emailSubject: 'Reopened subject',
444
+ emailPreHeader: 'Reopened preheader',
445
+ emailBody: '<p>hello</p>',
446
+ }}
447
+ handleCloseCreatives={handleCloseCreatives}
448
+ getCreativesData={getCreativesData}
449
+ isFullMode={false}
450
+ templateActions={{
451
+ getCdnTransformationConfig,
452
+ }}
453
+ />,
454
+ );
455
+
456
+ const { templateData } = renderedComponent.instance().state;
457
+ expect(templateData.base.subject).toBe('Reopened subject');
458
+ expect(templateData.base.preheader).toBe('Reopened preheader');
459
+ });
379
460
  });
@@ -119,6 +119,10 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
119
119
  "template-subject": {
120
120
  onTagSelect: this.onTagSelect,
121
121
  },
122
+ // unified pre-header field with input and tag selection
123
+ "template-preheader": {
124
+ onTagSelect: this.onTagSelect,
125
+ },
122
126
  "template-version": {
123
127
  onSelect: this.handleVersionSelect,
124
128
  addVersion: this.addVersion,
@@ -645,6 +649,14 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
645
649
  formData['template-subject'] = subjectFromEditData;
646
650
  }
647
651
  }
652
+
653
+ // Ensure template-preheader is preserved at the top level
654
+ if (formData['template-preheader'] === undefined || formData['template-preheader'] === '') {
655
+ const preheaderFromEditData = _.get(nextProps, 'Email.templateDetails.versions.base.preheader', '');
656
+ if (preheaderFromEditData) {
657
+ formData['template-preheader'] = preheaderFromEditData;
658
+ }
659
+ }
648
660
 
649
661
  if (langIndex !== undefined && langIndex !== -1 && temp && temp.panes) {
650
662
  _.forEach(temp.panes, (pane, index) => {
@@ -903,11 +915,11 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
903
915
  }
904
916
 
905
917
  onTagSelect = (data, currentTab, sourceVal) => {
906
- // If tag is selected from Subject's TagList or unified subject field, insert into subject input
907
- if (sourceVal && (sourceVal.id === 'title-tagList' || sourceVal.id === 'template-subject')) {
918
+ // If tag is selected from Subject's TagList, unified subject field, or the pre-header field, insert into the relevant input
919
+ if (sourceVal && (sourceVal.id === 'title-tagList' || sourceVal.id === 'template-subject' || sourceVal.id === 'template-preheader')) {
908
920
  const tagToInsert = `{{${data}}}`;
909
921
  const formData = _.cloneDeep(this.state.formData);
910
- const fieldId = 'template-subject';
922
+ const fieldId = sourceVal.id === 'template-preheader' ? 'template-preheader' : 'template-subject';
911
923
  const input = document.getElementById(fieldId) || document.querySelector(`#${fieldId} input`);
912
924
  let subjectValue = formData[fieldId] || '';
913
925
  try {
@@ -1184,6 +1196,8 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1184
1196
  formData['template-name'] = editData.name;
1185
1197
  const subject = _.get(editData, 'versions.base.subject', '');
1186
1198
  formData['template-subject'] = subject;
1199
+ const preheader = _.get(editData, 'versions.base.preheader', '');
1200
+ formData['template-preheader'] = preheader;
1187
1201
  formData.base = editData.versions.base;
1188
1202
 
1189
1203
  _.forEach(editData.versions.history, (data1, index) => {
@@ -1686,6 +1700,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
1686
1700
  newFormData.base = _.cloneDeep(tmpData);
1687
1701
  newFormData.secondary_templates.push({template_data: tmpData});
1688
1702
  newFormData.base.subject = formData['template-subject'];
1703
+ newFormData.base.preheader = formData['template-preheader'] || '';
1689
1704
  return newFormData;
1690
1705
  }
1691
1706
 
@@ -2531,7 +2546,14 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2531
2546
  newSchema.standalone.sections = [];
2532
2547
  if (this.props.location.query.type === 'embedded' && (this.props.location.query.module === "loyalty" || this.props.location.query.module === "dvs" || this.props.location.query.module === "coupon_expiry" || this.props.location.query.module === "timeline" ||
2533
2548
  this.props.location.query.module === "library")) {
2534
- newSchema.standalone.sections.push(_.cloneDeep(schema.standalone.sections[0]));
2549
+ // Keep only Subject/Pre-header (matched by field id; template-name stays hidden).
2550
+ const standAloneFieldIdsToKeep = ['template-subject', 'template-preheader'];
2551
+ _.forEach(_.get(schema, 'standalone.sections', []), (section) => {
2552
+ const hasKeepableField = _.some(section.inputFields, (inputField) => _.some(inputField.cols, (col) => standAloneFieldIdsToKeep.includes(col.id)));
2553
+ if (hasKeepableField) {
2554
+ newSchema.standalone.sections.push(_.cloneDeep(section));
2555
+ }
2556
+ });
2535
2557
  }
2536
2558
 
2537
2559
  _.forEach(newSchema.containers, (container) => {
@@ -2590,6 +2612,7 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2590
2612
  if ((this.props.location.query.isLanguageSupport === undefined || this.props.location.query.isLanguageSupport === 'false')) {
2591
2613
  // If formData['template-subject'] is empty, assign an empty string; otherwise, retain its current value or use data.base.subject.
2592
2614
  formData['template-subject'] = formData?.['template-subject'] === '' ? '' : (data?.base?.subject || '');
2615
+ formData['template-preheader'] = formData?.['template-preheader'] === '' ? '' : (data?.base?.preheader || '');
2593
2616
  const baseLanguage = this.props.currentOrgDetails.basic_details.base_language;
2594
2617
  let languageIndex = _.findIndex(this.supportedLanguages, {iso_code: baseLanguage});
2595
2618
 
@@ -2923,12 +2946,16 @@ export class Email extends React.Component { // eslint-disable-line react/prefer
2923
2946
  if (index === "template-subject" && obj?.versions?.history?.[0]) {
2924
2947
  obj.versions.history[0].subject = newdata;
2925
2948
  }
2949
+ if (index === "template-preheader" && obj?.versions?.history?.[0]) {
2950
+ obj.versions.history[0].preheader = newdata;
2951
+ }
2926
2952
  });
2927
2953
  //const data = formData[`${this.state.currentTab - 1}`];
2928
2954
  obj.name = formData['template-name'];
2929
2955
  obj.versions.base = formData.base;
2930
2956
  obj.versions.base.tabKey = baseKey;
2931
2957
  obj.versions.base.subject = formData['template-subject'];
2958
+ obj.versions.base.preheader = formData['template-preheader'] || '';
2932
2959
  obj.versions.base.activeTab = formData?.base?.activeTab || baseLanguage;
2933
2960
  _.forEach(obj.versions.base.selectedLanguages, (language) => {
2934
2961
  if (obj.versions.base[language].is_drag_drop) {
@@ -42,6 +42,36 @@ export const response = {
42
42
  },
43
43
  ],
44
44
  },
45
+ {
46
+ type: "multicols",
47
+ inputFields: [
48
+ {
49
+ cols: [
50
+ {
51
+ id: "template-preheader",
52
+ placeholder: "Enter Pre-header Text",
53
+ type: "cap-tag-list-with-input",
54
+ metaType: "text",
55
+ datatype: "string",
56
+ required: false,
57
+ label: "Pre-header",
58
+ styling: "semantic",
59
+ order: 1,
60
+ width: 16,
61
+ customComponent: true,
62
+ standalone: true,
63
+ onlyDisplay: false,
64
+ maxLength: 150,
65
+ errorMessage: "Unsubscribe tag is not allowed in Pre-header.",
66
+ supportedEvents: [
67
+ "onChange",
68
+ "onTagSelect",
69
+ ],
70
+ },
71
+ ],
72
+ },
73
+ ],
74
+ },
45
75
  {
46
76
  type: "multicols",
47
77
  inputFields: [
@@ -154,6 +154,14 @@ export default defineMessages({
154
154
  id: 'creatives.containersV2.Email.emailSubjectNotEmplty',
155
155
  defaultMessage: 'Email subject cannot be empty.',
156
156
  },
157
+ "Enter Pre-header Text": {
158
+ id: 'creatives.containersV2.Email.emailPreheader',
159
+ defaultMessage: 'Enter pre-header text',
160
+ },
161
+ "Pre-header": {
162
+ id: 'creatives.containersV2.Email.Preheader',
163
+ defaultMessage: 'Pre-header',
164
+ },
157
165
  "Enter template name": {
158
166
  id: 'creatives.containersV2.Email.templateNamePlaceholder',
159
167
  defaultMessage: 'Enter template name',