@capillarytech/creatives-library 9.0.70 → 9.0.71-alpha.sprint-cap-211543.18

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 +119 -10
  25. package/v2Components/FormBuilder/_formBuilder.scss +21 -0
  26. package/v2Components/FormBuilder/messages.js +12 -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 +172 -8
  40. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
  41. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
  42. package/v2Containers/EmailWrapper/constants.js +5 -0
  43. package/v2Containers/EmailWrapper/messages.js +24 -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
@@ -30,7 +30,9 @@ import CapUploader from '@capillarytech/cap-ui-library/CapUploader';
30
30
  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
+ import CapTooltipWithInfo from '@capillarytech/cap-ui-library/CapTooltipWithInfo';
33
34
  import CapAskAira from '@capillarytech/cap-ui-library/CapAskAira';
35
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
34
36
 
35
37
  import { injectIntl, intlShape, FormattedMessage } from 'react-intl';
36
38
  import LabelHOC from '@capillarytech/cap-ui-library/assets/HOCs/ComponentWithLabelHOC';
@@ -61,7 +63,7 @@ import { convert } from 'html-to-text';
61
63
  import { OUTBOUND, ADD_LANGUAGE, UPLOAD, USE_EDITOR, COPY_PRIMARY_LANGUAGE, GLOBAL_CONVERT_OPTIONS } from './constants';
62
64
  import { GET_TRANSLATION_MAPPED } from '../../constants/unified';
63
65
  import moment from 'moment';
64
- import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS} from '../../constants/unified';
66
+ import { CUSTOMER_BARCODE_TAG , COPY_OF, ENTRY_TRIGGER_TAG_REGEX, SKIP_TAGS_REGEX_GROUPS, UNSUBSCRIBE_TAG} from '../../constants/unified';
65
67
  import { REQUEST } from '../../v2Containers/Cap/constants'
66
68
  import { isEmailUnsubscribeTagOptional, isAiContentBotDisabled } from '../../utils/common';
67
69
  import { isUrl } from '../../v2Containers/Line/Container/Wrapper/utils';
@@ -70,6 +72,7 @@ import { getChannelData, hasPersonalizationTags, validateLiquidTemplateContent,
70
72
  const {Column} = Table;
71
73
  const {TextArea} = CapInput;
72
74
  const {CapRadioGroup} = CapRadio;
75
+ const {CapLabelInline} = CapLabel;
73
76
  // import styled from 'styled-components';
74
77
 
75
78
  const tagsTypes = {
@@ -110,21 +113,32 @@ class HighFreqInput extends React.Component {
110
113
  }
111
114
  }
112
115
 
113
- // Isolated wrapper for EMAIL template-subject: blur-only commit, same as HighFreqInput.
116
+ // EMAIL fields kept visible in BEE mode; use the blur-commit HighFreqTagInput below.
117
+ const HIGH_FREQ_EMAIL_TAG_INPUT_IDS = ['template-subject', 'template-preheader'];
118
+
119
+ // EMAIL subject/preheader wrapper: commits on blur, like HighFreqInput.
114
120
  class HighFreqTagInput extends React.Component {
115
121
  constructor(props) {
116
122
  super(props);
117
- this.state = { localInputValue: props.inputValue || '' };
123
+ this.state = { localInputValue: props.inputValue || '', errorDismissed: false };
118
124
  }
119
125
 
120
126
  componentDidUpdate(prevProps) {
121
127
  if (prevProps.inputValue !== this.props.inputValue && this.state.localInputValue !== this.props.inputValue) {
122
128
  this.setState({ localInputValue: this.props.inputValue || '' });
123
129
  }
130
+ // New errorDataRef = fresh validation (each Save): un-dismiss so a repeated error shows again.
131
+ if (this.props.errorDataRef !== undefined && prevProps.errorDataRef !== this.props.errorDataRef && this.state.errorDismissed) {
132
+ this.setState({ errorDismissed: false });
133
+ }
124
134
  }
125
135
 
126
136
  handleInputChange = (e) => {
127
137
  this.setState({ localInputValue: e.target.value });
138
+ // Hide a stale error on edit (like the HTML editor's Pre-header); re-validates only on Save.
139
+ if (this.props.errorDataRef !== undefined && !this.state.errorDismissed) {
140
+ this.setState({ errorDismissed: true });
141
+ }
128
142
  };
129
143
 
130
144
  handleBlur = () => {
@@ -132,13 +146,39 @@ class HighFreqTagInput extends React.Component {
132
146
  };
133
147
 
134
148
  render() {
135
- const { inputValue: _iv, onCommit: _oc, inputOnChange: _ic, ...rest } = this.props;
149
+ const {
150
+ inputValue: _iv, onCommit: _oc, inputOnChange: _ic, inputMaxLength, onRemove, removeLabel,
151
+ errorDataRef, inputErrorMessage, ...rest
152
+ } = this.props;
153
+ const counterSuffix = inputMaxLength ? (
154
+ <CapLabelInline type="label1" className="high-freq-tag-input-counter">
155
+ {`${this.state.localInputValue.length} / ${inputMaxLength}`}
156
+ </CapLabelInline>
157
+ ) : null;
158
+ const removeSuffix = onRemove ? (
159
+ <CapLabelInline type="label1" role="button" tabIndex={0} className="high-freq-tag-input-remove-link" onClick={onRemove}>
160
+ {removeLabel}
161
+ </CapLabelInline>
162
+ ) : null;
163
+ const combinedSuffix = (counterSuffix || removeSuffix) ? (
164
+ <CapRow align="middle" type="flex" noWrap gap={8}>
165
+ {counterSuffix}
166
+ {removeSuffix}
167
+ </CapRow>
168
+ ) : undefined;
136
169
  return (
137
170
  <CapTagListWithInput
138
171
  {...rest}
172
+ inputMaxLength={inputMaxLength}
139
173
  inputValue={this.state.localInputValue}
140
174
  inputOnChange={this.handleInputChange}
141
- inputProps={{ ...(this.props.inputProps || {}), onBlur: this.handleBlur }}
175
+ inputErrorMessage={this.state.errorDismissed ? '' : inputErrorMessage}
176
+ inputProps={{
177
+ ...(this.props.inputProps || {}),
178
+ onBlur: this.handleBlur,
179
+ ...(combinedSuffix ? { suffix: combinedSuffix } : {}),
180
+ }}
181
+ singleLineLayout
142
182
  />
143
183
  );
144
184
  }
@@ -169,6 +209,8 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
169
209
  STANDARD_ERROR_MSG: [],
170
210
  LIQUID_ERROR_MSG: [],
171
211
  },
212
+ // Pre-header visibility: null = show if it has content; true/false = user's Add/Remove click.
213
+ preheaderRevealed: null,
172
214
  };
173
215
  this.renderForm = this.renderForm.bind(this);
174
216
  this.updateFormData = this.updateFormData.bind(this);
@@ -1245,6 +1287,16 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1245
1287
  }
1246
1288
  }
1247
1289
 
1290
+ // Pre-header must not contain the unsubscribe tag (checked on Save, not per keystroke).
1291
+ if (index === 'template-preheader') {
1292
+ if (hasUnsubscribeTag(data)) {
1293
+ errorData[index] = true;
1294
+ isValid = false;
1295
+ } else {
1296
+ errorData[index] = false;
1297
+ }
1298
+ }
1299
+
1248
1300
  // Check if Template name present in full mode
1249
1301
  if(type !== 'embedded' && index === 'template-name' && data === '') {
1250
1302
  errorData[index] = true;
@@ -3447,6 +3499,18 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3447
3499
  let contentSections = [];
3448
3500
  let popoverSections = [];
3449
3501
  const self = this;
3502
+ // Pre-header visibility: null = show if it has content; true/false = user's Add/Remove click.
3503
+ const preheaderValueForRenderSection = this.state.formData['template-preheader'] || '';
3504
+ const isPreheaderRevealed = this.state.preheaderRevealed !== null
3505
+ ? this.state.preheaderRevealed
3506
+ : !!preheaderValueForRenderSection;
3507
+ // Hidden Pre-header section renders no fields by design — not the "not ready" spinner case below.
3508
+ const isHiddenPreheaderOnlySection = !isPreheaderRevealed
3509
+ && section.inputFields
3510
+ && section.inputFields.length === 1
3511
+ && section.inputFields[0].cols
3512
+ && section.inputFields[0].cols.length === 1
3513
+ && section.inputFields[0].cols[0].id === 'template-preheader';
3450
3514
  _.forEach(section.inputFields, (field) => {
3451
3515
  const columns = [];
3452
3516
  _.forEach(field.cols, (val) => {
@@ -3807,14 +3871,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3807
3871
  const isBEEAppEnableForCapTagList = this.checkBeeEditorAllowedForLibrary();
3808
3872
  ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
3809
3873
 
3810
- // Always render Subject CapTagListWithInput even when Bee editor is active for EMAIL channel
3874
+ // Pre-header hidden behind "+ Pre-header" unless it has content or the user toggled it.
3875
+ if (val.id === 'template-preheader' && !isPreheaderRevealed) {
3876
+ break;
3877
+ }
3878
+
3879
+ // Subject/Pre-header render even in BEE mode
3811
3880
  if (
3812
- val.id === 'template-subject' ||
3881
+ HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id) ||
3813
3882
  !(_.get(this.state, `formData[${this.state.currentTab - 1}][${currentLangForCapTagList}].is_drag_drop`, false)) ||
3814
3883
  isBEEAppEnableForCapTagList === false ||
3815
3884
  channelForCapTagList !== 'EMAIL'
3816
3885
  ) {
3817
- const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && val.id === 'template-subject';
3886
+ const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id);
3818
3887
  const tagListProps = {
3819
3888
  key: `input-${val.id}`,
3820
3889
  inputId: val.id,
@@ -3823,9 +3892,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3823
3892
  inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
3824
3893
  inputRequired: val.required || false,
3825
3894
  inputDisabled: val.disabled || false,
3895
+ inputMaxLength: val.maxLength,
3826
3896
  headingText: val.label || '',
3827
3897
  headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
3828
3898
  headingType: "h4",
3899
+ // Label is an inline input prefix (see inputProps), so no heading here.
3900
+ showHeading: !isEmailStandaloneSubject,
3829
3901
  onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
3830
3902
  onContextChange: this.props.onContextChange,
3831
3903
  location: this.props.location,
@@ -3838,10 +3910,47 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3838
3910
  waitEventContextTags: this.props?.waitEventContextTags,
3839
3911
  moduleFilterEnabled: moduleFilterEnabledForCapTagList,
3840
3912
  containerStyle: val.style || {},
3841
- inputProps: val.inputProps || {},
3913
+ inputProps: isEmailStandaloneSubject
3914
+ ? {
3915
+ ...(val.inputProps || {}),
3916
+ prefix: (
3917
+ <CapRow align="middle" type="flex" noWrap gap={4} className="high-freq-tag-input-prefix">
3918
+ <CapLabelInline type="label2" className="high-freq-tag-input-prefix-label">
3919
+ {val.label || ''}
3920
+ </CapLabelInline>
3921
+ {val.id === 'template-preheader' && (
3922
+ <CapTooltipWithInfo title={this.props.intl.formatMessage(messages.preheaderInfoTooltip)} />
3923
+ )}
3924
+ </CapRow>
3925
+ ),
3926
+ }
3927
+ : (val.inputProps || {}),
3842
3928
  showInput: val.showInput !== false,
3843
3929
  showTagList: val.showTagList !== false,
3844
3930
  restrictPersonalization: this.props.restrictPersonalization,
3931
+ ...(val.id === 'template-subject' && !isPreheaderRevealed ? {
3932
+ extraContent: (
3933
+ <CapButton
3934
+ type="dashed"
3935
+ size="small"
3936
+ style={{ whiteSpace: 'nowrap' }}
3937
+ onClick={() => this.setState({ preheaderRevealed: true })}
3938
+ >
3939
+ {this.props.intl.formatMessage(messages.addPreheader)}
3940
+ </CapButton>
3941
+ ),
3942
+ } : {}),
3943
+ ...(val.id === 'template-preheader' ? {
3944
+ onRemove: () => {
3945
+ this.performFormDataUpdate('', val);
3946
+ this.setState({ preheaderRevealed: false });
3947
+ },
3948
+ removeLabel: this.props.intl.formatMessage(messages.removePreheader),
3949
+ // Unsubscribe is not a valid Pre-header tag — hide it from the Add Label dropdown.
3950
+ excludeTags: [UNSUBSCRIBE_TAG],
3951
+ // New ref per Save/Done click; lets HighFreqTagInput hide stale errors while editing.
3952
+ errorDataRef: this.state.errorData,
3953
+ } : {}),
3845
3954
  };
3846
3955
  columns.push(
3847
3956
  <CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
@@ -4249,7 +4358,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4249
4358
  fields.push(row);
4250
4359
  }
4251
4360
  });
4252
- if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length) {
4361
+ if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length && !isHiddenPreheaderOnlySection) {
4253
4362
  return (
4254
4363
  <div style={{ textAlign: 'center' }}>
4255
4364
  <CapSpin spinning />
@@ -136,4 +136,25 @@
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 {
153
+ padding-right: $CAP_SPACE_08;
154
+ margin-right: $CAP_SPACE_08;
155
+ border-right: 1px solid $CAP_COLOR_16;
156
+ }
157
+
158
+ .high-freq-tag-input-prefix-label {
159
+ font-weight: 600;
139
160
  }
@@ -98,4 +98,16 @@ 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
+ },
109
+ preheaderInfoTooltip: {
110
+ id: 'creatives.componentsV2.FormBuilder.preheaderInfoTooltip',
111
+ defaultMessage: 'Shows next to the subject line in the inbox, use it to add context and improve open rates. Keep it to 40–85 characters.',
112
+ },
101
113
  });
@@ -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?.subject || '',
1100
+ emailPreHeader: 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 ([undefined, ''].includes(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: "e.g. Get 20% off your next order",
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: 85,
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',