@capillarytech/creatives-library 9.0.73-alpha.sprint-cap-212983.19 → 9.1.0

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 (54) hide show
  1. package/package.json +1 -1
  2. package/v2Components/CapTagListWithInput/__tests__/CapTagListWithInput.test.js +58 -1
  3. package/v2Components/CapTagListWithInput/index.js +64 -35
  4. package/v2Components/CapTagListWithInput/index.scss +9 -0
  5. package/v2Components/CommonTestAndPreview/UnifiedPreview/EmailPreviewContent.js +8 -0
  6. package/v2Components/CommonTestAndPreview/UnifiedPreview/_unifiedPreview.scss +7 -0
  7. package/v2Components/CommonTestAndPreview/UnifiedPreview/index.js +1 -0
  8. package/v2Components/CommonTestAndPreview/actions.js +1 -1
  9. package/v2Components/CommonTestAndPreview/index.js +29 -13
  10. package/v2Components/CommonTestAndPreview/sagas.js +2 -1
  11. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/EmailPreviewContent.test.js +27 -0
  12. package/v2Components/CommonTestAndPreview/tests/UnifiedPreview/index.test.js +40 -0
  13. package/v2Components/CommonTestAndPreview/tests/index.test.js +95 -0
  14. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
  15. package/v2Components/EmailMobilePreview/index.js +8 -1
  16. package/v2Components/EmailMobilePreview/index.scss +12 -3
  17. package/v2Components/EmailMobilePreview/tests/index.test.js +31 -0
  18. package/v2Components/EmailPreviewV2/_emailPreviewV2.scss +37 -5
  19. package/v2Components/EmailPreviewV2/index.js +18 -6
  20. package/v2Components/EmailPreviewV2/messages.js +4 -0
  21. package/v2Components/EmailPreviewV2/tests/index.test.js +43 -0
  22. package/v2Components/FormBuilder/Classic.js +134 -14
  23. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +9 -0
  24. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +11 -12
  25. package/v2Components/FormBuilder/Functional/tests/mpush.singlePlatform.test.js +144 -0
  26. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +17 -5
  27. package/v2Components/FormBuilder/_formBuilder.scss +21 -0
  28. package/v2Components/FormBuilder/messages.js +12 -0
  29. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +45 -0
  30. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
  31. package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
  32. package/v2Containers/CommunicationFlow/constants.js +1 -0
  33. package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
  34. package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
  35. package/v2Containers/CreativesContainer/index.js +7 -1
  36. package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
  37. package/v2Containers/Email/index.js +31 -4
  38. package/v2Containers/Email/initialSchema.js +30 -0
  39. package/v2Containers/Email/messages.js +8 -0
  40. package/v2Containers/Email/tests/index.test.js +107 -0
  41. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +172 -8
  42. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
  43. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
  44. package/v2Containers/EmailWrapper/constants.js +5 -0
  45. package/v2Containers/EmailWrapper/messages.js +24 -0
  46. package/v2Containers/MobilePush/Create/constants.js +1 -0
  47. package/v2Containers/MobilePush/Create/index.js +2 -2
  48. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +58 -0
  49. package/v2Containers/MobilePush/Edit/index.js +2 -2
  50. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +17 -0
  51. package/v2Containers/MobilePush/commonMethods.js +7 -8
  52. package/v2Containers/TagList/index.js +7 -0
  53. package/v2Containers/TagList/tests/TagList.test.js +42 -0
  54. package/v2Containers/Viber/index.scss +0 -31
@@ -224,6 +224,7 @@ describe('CommonTestAndPreview Sagas', () => {
224
224
  expect(generator.next().value).toEqual(
225
225
  call(Api.extractTagsWithMetaData, {
226
226
  messageTitle: 'Hello {{name}}',
227
+ messageSubtitle: '',
227
228
  messageBody: 'Welcome {{email}}',
228
229
  })
229
230
  );
@@ -238,6 +239,25 @@ describe('CommonTestAndPreview Sagas', () => {
238
239
  expect(generator.next().done).toBe(true);
239
240
  });
240
241
 
242
+ it('should pass templatePreheader through as messageSubtitle', () => {
243
+ const action = {
244
+ payload: {
245
+ templateSubject: 'Hello {{name}}',
246
+ templatePreheader: 'Enjoy {{discount}}',
247
+ templateContent: 'Welcome {{email}}',
248
+ },
249
+ };
250
+ const generator = extractTagsSaga(action);
251
+
252
+ expect(generator.next().value).toEqual(
253
+ call(Api.extractTagsWithMetaData, {
254
+ messageTitle: 'Hello {{name}}',
255
+ messageSubtitle: 'Enjoy {{discount}}',
256
+ messageBody: 'Welcome {{email}}',
257
+ })
258
+ );
259
+ });
260
+
241
261
  it('should handle empty templateSubject', () => {
242
262
  const action = {
243
263
  payload: {
@@ -250,6 +270,7 @@ describe('CommonTestAndPreview Sagas', () => {
250
270
  expect(generator.next().value).toEqual(
251
271
  call(Api.extractTagsWithMetaData, {
252
272
  messageTitle: '',
273
+ messageSubtitle: '',
253
274
  messageBody: 'Welcome {{email}}',
254
275
  })
255
276
  );
@@ -268,6 +289,7 @@ describe('CommonTestAndPreview Sagas', () => {
268
289
  expect(generator.next().value).toEqual(
269
290
  call(Api.extractTagsWithMetaData, {
270
291
  messageTitle: 'Hello {{name}}',
292
+ messageSubtitle: '',
271
293
  messageBody: '',
272
294
  })
273
295
  );
@@ -284,6 +306,7 @@ describe('CommonTestAndPreview Sagas', () => {
284
306
  expect(generator.next().value).toEqual(
285
307
  call(Api.extractTagsWithMetaData, {
286
308
  messageTitle: '',
309
+ messageSubtitle: '',
287
310
  messageBody: 'Welcome {{email}}',
288
311
  })
289
312
  );
@@ -301,6 +324,7 @@ describe('CommonTestAndPreview Sagas', () => {
301
324
  expect(generator.next().value).toEqual(
302
325
  call(Api.extractTagsWithMetaData, {
303
326
  messageTitle: 'Hello {{name}}',
327
+ messageSubtitle: '',
304
328
  messageBody: '',
305
329
  })
306
330
  );
@@ -29,7 +29,7 @@ const deviceAspectRatio = {
29
29
  },
30
30
  };
31
31
 
32
- class EmailMobilePreview extends React.Component {
32
+ export class EmailMobilePreview extends React.Component {
33
33
  static getDerivedStateFromProps(props) {
34
34
  if (props.showAllDevices) {
35
35
  return {
@@ -66,6 +66,7 @@ class EmailMobilePreview extends React.Component {
66
66
  templateContent,
67
67
  enableShowAllDevices,
68
68
  templateSubject,
69
+ templatePreHeader,
69
70
  } = this.props;
70
71
  const copyContent = templateContent && removeLinksFromHtmlContent(templateContent);
71
72
  const updateCopyContent = templateContent && iframePreviewAdjustWidth(copyContent);
@@ -76,6 +77,10 @@ class EmailMobilePreview extends React.Component {
76
77
  <CapLabel className="email-subject-mobile" type="label2">
77
78
  <FormattedMessage {...messages.subject} /> {templateSubject}</CapLabel>
78
79
  );
80
+ const emailPreHeaderLabel = templatePreHeader && (
81
+ <CapLabel className="email-preheader-mobile" type="label2">
82
+ <FormattedMessage {...messages.preheader} /> {templatePreHeader}</CapLabel>
83
+ );
79
84
 
80
85
  return (
81
86
  <div className="email-mobile-preview-container">
@@ -100,6 +105,7 @@ class EmailMobilePreview extends React.Component {
100
105
  <img src={mobileBody} alt="capillary" width="386" />
101
106
  <div className={`${messageContainerName} ${emailSubjectLabel ? 'with-subject' : ''}`}>
102
107
  {emailSubjectLabel}
108
+ {emailPreHeaderLabel}
103
109
  <iframe srcDoc={updateCopyContent} {...deviceAspectRatio.mobile} >
104
110
  <p><FormattedMessage {...messages.noIframeSupport}/></p>
105
111
  </iframe>
@@ -127,6 +133,7 @@ class EmailMobilePreview extends React.Component {
127
133
  EmailMobilePreview.propTypes = {
128
134
  templateContent: PropTypes.string,
129
135
  templateSubject: PropTypes.string,
136
+ templatePreHeader: PropTypes.string,
130
137
  templateData: PropTypes.object,
131
138
  templateActions: PropTypes.object,
132
139
  onClickShowInAllDevices: PropTypes.func,
@@ -44,10 +44,19 @@ $classPrefix: email-mobile-preview-container;
44
44
  justify-content: center;
45
45
  }
46
46
  .email-subject-mobile {
47
- width: 227px;
47
+ width: 14.1875rem;
48
48
  text-align: left;
49
- margin: 8px 0;
50
- margin-left: 4px;
49
+ margin: 0.5rem 0.25rem 0;
50
+ padding-bottom: 0.5rem;
51
+ border-bottom: 0.0625rem solid #d9d9d9;
52
+ }
53
+ .email-preheader-mobile {
54
+ width: 14.1875rem;
55
+ text-align: left;
56
+ margin: 0.5rem 0.25rem 0;
57
+ padding-bottom: 0.5rem;
58
+ border-bottom: 0.0625rem solid #d9d9d9;
59
+ color: #8c8c8c;
51
60
  }
52
61
  // .mobile-message-container {
53
62
  // position: absolute;
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { shallow } from 'enzyme';
3
+ import { EmailMobilePreview } from '../index';
4
+
5
+ describe('<EmailMobilePreview />', () => {
6
+ const baseProps = {
7
+ templateContent: '<p>Body</p>',
8
+ templateActions: {
9
+ getTemplateDetails: jest.fn(),
10
+ },
11
+ };
12
+
13
+ it('does not render a pre-header label when templatePreHeader is not provided', () => {
14
+ const wrapper = shallow(<EmailMobilePreview {...baseProps} templateSubject="Hello" />);
15
+ expect(wrapper.find('.email-preheader-mobile')).toHaveLength(0);
16
+ });
17
+
18
+ it('renders the pre-header label when templatePreHeader is provided', () => {
19
+ const wrapper = shallow(
20
+ <EmailMobilePreview {...baseProps} templateSubject="Hello" templatePreHeader="Enjoy 20% off" />
21
+ );
22
+ const label = wrapper.find('.email-preheader-mobile');
23
+ expect(label).toHaveLength(1);
24
+ expect(label.text()).toContain('Enjoy 20% off');
25
+ });
26
+
27
+ it('renders the pre-header label even when subject is absent', () => {
28
+ const wrapper = shallow(<EmailMobilePreview {...baseProps} templatePreHeader="Enjoy 20% off" />);
29
+ expect(wrapper.find('.email-preheader-mobile')).toHaveLength(1);
30
+ });
31
+ });
@@ -1,4 +1,8 @@
1
+ @import '~@capillarytech/cap-ui-library/styles/_variables';
2
+
1
3
  $classPrefix: email-preview-v2;
4
+ $preheaderMutedColor: #8c8c8c;
5
+
2
6
  .#{$classPrefix}:not(.email-mobile-preview-container){
3
7
  #emailPreivew-container {
4
8
  position: relative;
@@ -54,19 +58,47 @@ $classPrefix: email-preview-v2;
54
58
  .email-subject-mobile {
55
59
  width: 310px;
56
60
  text-align: left;
57
- margin: 8px 0;
58
- margin-left: 4px;
61
+ margin: 0.5rem 0.25rem 0;
62
+ padding-bottom: 0.5rem;
63
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
59
64
  }
60
65
  .email-subject-tablet {
61
66
  width: 450px;
62
67
  text-align: left;
63
- margin: 8px 0;
64
- margin-left: 4px;
68
+ margin: 0.5rem 0.25rem 0;
69
+ padding-bottom: 0.5rem;
70
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
65
71
  }
66
72
  .email-subject-desktop {
67
73
  text-align: left;
68
74
  margin-left: 6%;
69
- margin-bottom: 8px;
75
+ margin-right: 6%;
76
+ padding-bottom: 0.5rem;
77
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
78
+ }
79
+ .email-preheader-mobile {
80
+ width: 19.375rem;
81
+ text-align: left;
82
+ margin: 0.5rem 0.25rem 0;
83
+ padding-bottom: 0.5rem;
84
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
85
+ color: $preheaderMutedColor;
86
+ }
87
+ .email-preheader-tablet {
88
+ width: 28.125rem;
89
+ text-align: left;
90
+ margin: 0.5rem 0.25rem 0;
91
+ padding-bottom: 0.5rem;
92
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
93
+ color: $preheaderMutedColor;
94
+ }
95
+ .email-preheader-desktop {
96
+ text-align: left;
97
+ margin-left: 6%;
98
+ margin-right: 6%;
99
+ padding-bottom: 0.5rem;
100
+ border-bottom: 0.0625rem solid $CAP_COLOR_16;
101
+ color: $preheaderMutedColor;
70
102
  }
71
103
  .tablet-message-container {
72
104
  position: absolute;
@@ -30,12 +30,12 @@ const deviceAspectRatio = {
30
30
  height: "500",
31
31
  width: "1024",
32
32
  }),
33
- tablet: (emailSubject) => ({
34
- height: emailSubject ? "570" : "600",
33
+ tablet: (emailSubject, emailPreHeader) => ({
34
+ height: emailSubject ? (emailPreHeader ? "545" : "570") : "600",
35
35
  width: "450",
36
36
  }),
37
- mobile: (emailSubject) => ({
38
- height: emailSubject ? "541" : "511",
37
+ mobile: (emailSubject, emailPreHeader) => ({
38
+ height: emailSubject ? (emailPreHeader ? "516" : "541") : "511",
39
39
  width: "340",
40
40
  }),
41
41
  };
@@ -72,25 +72,36 @@ export class EmailPreviewV2 extends React.Component {
72
72
  render() {
73
73
  let messageContainerName = '';
74
74
  const {currentDevice} = this.state;
75
- const { templateContent, templateData = {}, templateSubject } = this.props;
75
+ const {
76
+ templateContent, templateData = {}, templateSubject, templatePreHeader,
77
+ } = this.props;
76
78
  const copyContent = templateContent && removeLinksFromHtmlContent(templateContent);
77
79
  const updateCopyContent = templateContent && iframePreviewAdjustWidth(copyContent);
78
80
  let subjectClassName = 'email-subject-desktop';
81
+ let preheaderClassName = 'email-preheader-desktop';
79
82
  if (currentDevice === devices.tablet) {
80
83
  messageContainerName = 'tablet-message-container';
81
84
  subjectClassName = "email-subject-tablet";
85
+ preheaderClassName = "email-preheader-tablet";
82
86
  } else if (currentDevice === devices.mobile) {
83
87
  messageContainerName = 'mobile-message-container';
84
88
  subjectClassName = "email-subject-mobile";
89
+ preheaderClassName = "email-preheader-mobile";
85
90
  }
86
91
  const emailSubject = get(templateData, 'versions.base.subject') || templateSubject;
92
+ const emailPreHeader = get(templateData, 'versions.base.preheader') || templatePreHeader;
87
93
  const emailSubjectLabel = emailSubject && (
88
94
  <CapLabel className={subjectClassName} type="label2">
89
95
  <FormattedMessage {...messages.subject} /> {emailSubject}</CapLabel>
90
96
  );
97
+ const emailPreHeaderLabel = emailPreHeader && (
98
+ <CapLabel className={preheaderClassName} type="label2">
99
+ <FormattedMessage {...messages.preheader} /> {emailPreHeader}</CapLabel>
100
+ );
91
101
  const iframeDiv = (<div className={`${messageContainerName}`}>
92
102
  {emailSubjectLabel}
93
- <iframe srcDoc={updateCopyContent} {...deviceAspectRatio[currentDevice](emailSubject)} >
103
+ {emailPreHeaderLabel}
104
+ <iframe srcDoc={updateCopyContent} {...deviceAspectRatio[currentDevice](emailSubject, emailPreHeader)} >
94
105
  <p><FormattedMessage {...messages.browserDoesntSupportIframe} /></p>
95
106
  </iframe>
96
107
  </div>);
@@ -171,6 +182,7 @@ export class EmailPreviewV2 extends React.Component {
171
182
  EmailPreviewV2.propTypes = {
172
183
  templateContent: PropTypes.string,
173
184
  templateSubject: PropTypes.string,
185
+ templatePreHeader: PropTypes.string,
174
186
  templateData: PropTypes.object,
175
187
  templateActions: PropTypes.object,
176
188
  device: PropTypes.string,
@@ -42,6 +42,10 @@ export default defineMessages({
42
42
  id: 'creatives.componentsV2.EmailPreviewV2.subject',
43
43
  defaultMessage: 'Subject:',
44
44
  },
45
+ preheader: {
46
+ id: 'creatives.componentsV2.EmailPreviewV2.preheader',
47
+ defaultMessage: 'Pre-header:',
48
+ },
45
49
  browserDoesntSupportIframe: {
46
50
  id: 'creatives.componentsV2.EmailPreviewV2.browserDoesntSupportIframe',
47
51
  defaultMessage: 'Your browser does not support iframes.',
@@ -13,4 +13,47 @@ describe('<EmailPreviewV2 />', () => {
13
13
  />);
14
14
  expect(compoent).toMatchSnapshot();
15
15
  });
16
+
17
+ const baseProps = {
18
+ templateData: {},
19
+ templateActions: {
20
+ getTemplateDetails: jest.fn(),
21
+ resetTemplateData: jest.fn(),
22
+ },
23
+ };
24
+
25
+ // The desktop pane's content is passed as a prop to CapTab (not a rendered child), so
26
+ // shallow() won't reach into it via .find() directly - render that pane's content JSX
27
+ // on its own to inspect it.
28
+ const getDesktopPaneWrapper = (wrapper) => {
29
+ const panes = wrapper.find('CapTab').prop('panes');
30
+ const desktopPane = panes.find((pane) => pane.key === 'desktop');
31
+ return shallow(desktopPane.content);
32
+ };
33
+
34
+ it('does not render a pre-header label when templatePreHeader is not provided', () => {
35
+ const wrapper = shallow(<EmailPreviewV2 {...baseProps} templateSubject="Hello" />);
36
+ expect(getDesktopPaneWrapper(wrapper).find('.email-preheader-desktop')).toHaveLength(0);
37
+ });
38
+
39
+ it('renders the pre-header label for the current (desktop) device by default', () => {
40
+ const wrapper = shallow(
41
+ <EmailPreviewV2 {...baseProps} templateSubject="Hello" templatePreHeader="Enjoy 20% off" />
42
+ );
43
+ const label = getDesktopPaneWrapper(wrapper).find('.email-preheader-desktop');
44
+ expect(label).toHaveLength(1);
45
+ expect(label.text()).toContain('Enjoy 20% off');
46
+ });
47
+
48
+ it('prefers templateData.versions.base.preheader over the templatePreHeader prop', () => {
49
+ const wrapper = shallow(
50
+ <EmailPreviewV2
51
+ {...baseProps}
52
+ templateData={{ versions: { base: { preheader: 'From template data' } } }}
53
+ templatePreHeader="From prop"
54
+ />
55
+ );
56
+ const label = getDesktopPaneWrapper(wrapper).find('.email-preheader-desktop');
57
+ expect(label.text()).toContain('From template data');
58
+ });
16
59
  });
@@ -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);
@@ -596,6 +638,13 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
596
638
  if (!_.isEqual(prevProps.isTransactional, this.props.isTransactional)) {
597
639
  this.validateForm(this.props.tags, this.props.injectedTags);
598
640
  }
641
+ const panes = _.get(this.props, 'schema.containers[0].panes', []);
642
+ if (_.toUpper(_.get(this.props, 'schema.channel')) === MOBILE_PUSH && _.get(panes, `[${this.state.currentTab - 1}].isSupported`) === false) {
643
+ const visiblePaneIndex = _.findIndex(panes, (pane) => pane?.isSupported !== false);
644
+ if (visiblePaneIndex !== -1) {
645
+ this.setState({ currentTab: visiblePaneIndex + 1 });
646
+ }
647
+ }
599
648
  }
600
649
 
601
650
  onEdit() {
@@ -1245,6 +1294,16 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
1245
1294
  }
1246
1295
  }
1247
1296
 
1297
+ // Pre-header must not contain the unsubscribe tag (checked on Save, not per keystroke).
1298
+ if (index === 'template-preheader') {
1299
+ if (hasUnsubscribeTag(data)) {
1300
+ errorData[index] = true;
1301
+ isValid = false;
1302
+ } else {
1303
+ errorData[index] = false;
1304
+ }
1305
+ }
1306
+
1248
1307
  // Check if Template name present in full mode
1249
1308
  if(type !== 'embedded' && index === 'template-name' && data === '') {
1250
1309
  errorData[index] = true;
@@ -3447,6 +3506,18 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3447
3506
  let contentSections = [];
3448
3507
  let popoverSections = [];
3449
3508
  const self = this;
3509
+ // Pre-header visibility: null = show if it has content; true/false = user's Add/Remove click.
3510
+ const preheaderValueForRenderSection = this.state.formData['template-preheader'] || '';
3511
+ const isPreheaderRevealed = this.state.preheaderRevealed !== null
3512
+ ? this.state.preheaderRevealed
3513
+ : !!preheaderValueForRenderSection;
3514
+ // Hidden Pre-header section renders no fields by design — not the "not ready" spinner case below.
3515
+ const isHiddenPreheaderOnlySection = !isPreheaderRevealed
3516
+ && section.inputFields
3517
+ && section.inputFields.length === 1
3518
+ && section.inputFields[0].cols
3519
+ && section.inputFields[0].cols.length === 1
3520
+ && section.inputFields[0].cols[0].id === 'template-preheader';
3450
3521
  _.forEach(section.inputFields, (field) => {
3451
3522
  const columns = [];
3452
3523
  _.forEach(field.cols, (val) => {
@@ -3807,14 +3878,19 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3807
3878
  const isBEEAppEnableForCapTagList = this.checkBeeEditorAllowedForLibrary();
3808
3879
  ifError = this.state.checkValidation && (isVersionEnable ? this.state.errorData[`${this.state.currentTab - 1}`][val.id] : this.state.errorData[val.id]);
3809
3880
 
3810
- // Always render Subject CapTagListWithInput even when Bee editor is active for EMAIL channel
3881
+ // Pre-header hidden behind "+ Pre-header" unless it has content or the user toggled it.
3882
+ if (val.id === 'template-preheader' && !isPreheaderRevealed) {
3883
+ break;
3884
+ }
3885
+
3886
+ // Subject/Pre-header render even in BEE mode
3811
3887
  if (
3812
- val.id === 'template-subject' ||
3888
+ HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id) ||
3813
3889
  !(_.get(this.state, `formData[${this.state.currentTab - 1}][${currentLangForCapTagList}].is_drag_drop`, false)) ||
3814
3890
  isBEEAppEnableForCapTagList === false ||
3815
3891
  channelForCapTagList !== 'EMAIL'
3816
3892
  ) {
3817
- const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && val.id === 'template-subject';
3893
+ const isEmailStandaloneSubject = val.standalone && channelForCapTagList === EMAIL && HIGH_FREQ_EMAIL_TAG_INPUT_IDS.includes(val.id);
3818
3894
  const tagListProps = {
3819
3895
  key: `input-${val.id}`,
3820
3896
  inputId: val.id,
@@ -3823,9 +3899,12 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3823
3899
  inputErrorMessage: val.errorMessage && ifError ? val.errorMessage : '',
3824
3900
  inputRequired: val.required || false,
3825
3901
  inputDisabled: val.disabled || false,
3902
+ inputMaxLength: val.maxLength,
3826
3903
  headingText: val.label || '',
3827
3904
  headingStyle: val.headingStyle || { marginTop: '3%', marginRight: '79%' },
3828
3905
  headingType: "h4",
3906
+ // Label is an inline input prefix (see inputProps), so no heading here.
3907
+ showHeading: !isEmailStandaloneSubject,
3829
3908
  onTagSelect: (data) => this.callChildEvent(data, val, 'onTagSelect'),
3830
3909
  onContextChange: this.props.onContextChange,
3831
3910
  location: this.props.location,
@@ -3838,10 +3917,47 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
3838
3917
  waitEventContextTags: this.props?.waitEventContextTags,
3839
3918
  moduleFilterEnabled: moduleFilterEnabledForCapTagList,
3840
3919
  containerStyle: val.style || {},
3841
- inputProps: val.inputProps || {},
3920
+ inputProps: isEmailStandaloneSubject
3921
+ ? {
3922
+ ...(val.inputProps || {}),
3923
+ prefix: (
3924
+ <CapRow align="middle" type="flex" noWrap gap={4} className="high-freq-tag-input-prefix">
3925
+ <CapLabelInline type="label2" className="high-freq-tag-input-prefix-label">
3926
+ {val.label || ''}
3927
+ </CapLabelInline>
3928
+ {val.id === 'template-preheader' && (
3929
+ <CapTooltipWithInfo title={this.props.intl.formatMessage(messages.preheaderInfoTooltip)} />
3930
+ )}
3931
+ </CapRow>
3932
+ ),
3933
+ }
3934
+ : (val.inputProps || {}),
3842
3935
  showInput: val.showInput !== false,
3843
3936
  showTagList: val.showTagList !== false,
3844
3937
  restrictPersonalization: this.props.restrictPersonalization,
3938
+ ...(val.id === 'template-subject' && !isPreheaderRevealed ? {
3939
+ extraContent: (
3940
+ <CapButton
3941
+ type="dashed"
3942
+ size="small"
3943
+ style={{ whiteSpace: 'nowrap' }}
3944
+ onClick={() => this.setState({ preheaderRevealed: true })}
3945
+ >
3946
+ {this.props.intl.formatMessage(messages.addPreheader)}
3947
+ </CapButton>
3948
+ ),
3949
+ } : {}),
3950
+ ...(val.id === 'template-preheader' ? {
3951
+ onRemove: () => {
3952
+ this.performFormDataUpdate('', val);
3953
+ this.setState({ preheaderRevealed: false });
3954
+ },
3955
+ removeLabel: this.props.intl.formatMessage(messages.removePreheader),
3956
+ // Unsubscribe is not a valid Pre-header tag — hide it from the Add Label dropdown.
3957
+ excludeTags: [UNSUBSCRIBE_TAG],
3958
+ // New ref per Save/Done click; lets HighFreqTagInput hide stale errors while editing.
3959
+ errorDataRef: this.state.errorData,
3960
+ } : {}),
3845
3961
  };
3846
3962
  columns.push(
3847
3963
  <CapColumn key={`input-${val.id}`} offset={val.offset} span={val.width ? val.width : ''} style={val.style ? val.style : {marginBottom: '16px'}}>
@@ -4249,7 +4365,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4249
4365
  fields.push(row);
4250
4366
  }
4251
4367
  });
4252
- if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length) {
4368
+ if (!this.props.isEmailLoading && this.props.schema.channel.toUpperCase() === EMAIL && !fields.length && !isHiddenPreheaderOnlySection) {
4253
4369
  return (
4254
4370
  <div style={{ textAlign: 'center' }}>
4255
4371
  <CapSpin spinning />
@@ -4281,7 +4397,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4281
4397
  const renderedPanes = [];
4282
4398
  let renderedSections = [];
4283
4399
  let renderedContentSections = [];
4284
- let paneIndex = 1;
4285
4400
  let panes = [];
4286
4401
  let result = [];
4287
4402
  let extraActionSections = [];
@@ -4292,6 +4407,7 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4292
4407
  case "tabs":
4293
4408
  panes = container.panes;
4294
4409
  _.forEach(panes, (pane, index) => {
4410
+ const paneIndex = index + 1;
4295
4411
  renderedSections = pane.sections.map((section) => {
4296
4412
  result = this.renderSection(section, paneIndex);
4297
4413
  return result;
@@ -4341,7 +4457,6 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4341
4457
  })
4342
4458
  ),
4343
4459
  });
4344
- paneIndex += 1;
4345
4460
  }
4346
4461
  });
4347
4462
  extraActionSections = container.tabBarExtraContent && container.tabBarExtraContent.sections ? container.tabBarExtraContent.sections.map((section) => {
@@ -4352,11 +4467,16 @@ class FormBuilder extends React.Component { // eslint-disable-line react/prefer-
4352
4467
  if (stateTabKey && stateTabKey !== '') {
4353
4468
  activeTabKey = stateTabKey;
4354
4469
  }
4470
+ activeTabKey = activeTabKey && activeTabKey !== '' ? `${activeTabKey}` : '1';
4471
+ if (renderedPanes?.length && !_.some(renderedPanes, (renderedPane) => `${renderedPane?.key}` === activeTabKey)
4472
+ && _.some(panes, (pane) => pane?.isSupported === false)) {
4473
+ activeTabKey = `${renderedPanes[0]?.key}`;
4474
+ }
4355
4475
  return (
4356
4476
  <CapTab
4357
4477
  className={`cap-tabs-${container.id}`}
4358
- activeKey={activeTabKey && activeTabKey !== '' ? `${activeTabKey}` : '1'}
4359
- defaultActiveKey={activeTabKey && activeTabKey !== '' ? `${activeTabKey}` : '1'}
4478
+ activeKey={activeTabKey}
4479
+ defaultActiveKey={activeTabKey}
4360
4480
  onChange={(data, e) => { this.callChildEvent(data, container, 'onTabChange', e); }}
4361
4481
  onEdit={(data, e) => this.onEdit(data, e)}
4362
4482
  style={{width: '100%'}}
@@ -303,6 +303,15 @@ const FormBuilderShell = (props) => {
303
303
  dispatch(setActiveTab(currentTabProp - 1));
304
304
  }, [currentTabProp, hasTabs]);
305
305
 
306
+ // Field writes target the active tab, so it must never be a hidden (isSupported: false) pane.
307
+ useEffect(() => {
308
+ if (!hasTabs) return;
309
+ const panes = schema?.containers?.find((container) => container?.type === 'tabs')?.panes || [];
310
+ if (panes[activeTabIndex]?.isSupported !== false) return;
311
+ const visiblePaneIndex = panes.findIndex((pane) => pane?.isSupported !== false);
312
+ if (visiblePaneIndex !== -1) dispatch(setActiveTab(visiblePaneIndex));
313
+ }, [activeTabIndex, schema, hasTabs]);
314
+
306
315
  // ---- container-driven modal visibility (mirrors Classic's props->state modal sync) ----
307
316
  useEffect(() => {
308
317
  setModalVisible(Boolean(showModal));