@capillarytech/creatives-library 9.0.73-alpha.sprint-testpreview.22 → 9.1.1

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 (66) 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 +36 -75
  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 +92 -97
  14. package/v2Components/CommonTestAndPreview/tests/sagas.test.js +24 -0
  15. package/v2Components/CommonTestAndPreview/tests/utils.test.js +0 -66
  16. package/v2Components/CommonTestAndPreview/utils.js +0 -14
  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 +37 -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 +134 -14
  25. package/v2Components/FormBuilder/Functional/FormBuilderShell.js +9 -0
  26. package/v2Components/FormBuilder/Functional/layout/TabsContainer.js +11 -12
  27. package/v2Components/FormBuilder/Functional/tests/mpush.singlePlatform.test.js +144 -0
  28. package/v2Components/FormBuilder/Functional/tests/tabsContainer.test.js +17 -5
  29. package/v2Components/FormBuilder/_formBuilder.scss +21 -0
  30. package/v2Components/FormBuilder/messages.js +12 -0
  31. package/v2Components/FormBuilder/tests/mpush.characterization.test.js +45 -0
  32. package/v2Containers/CommunicationFlow/Tests/CommunicationFlow.test.js +2 -1
  33. package/v2Containers/CommunicationFlow/Tests/constants.test.js +4 -4
  34. package/v2Containers/CommunicationFlow/constants.js +1 -0
  35. package/v2Containers/CommunicationFlow/utils/Tests/mapCommDefinitionToStepData.test.js +2 -1
  36. package/v2Containers/CommunicationFlow/utils/mapCommDefinitionToStepData.js +1 -0
  37. package/v2Containers/CreativesContainer/SlideBoxContent.js +0 -2
  38. package/v2Containers/CreativesContainer/index.js +7 -1
  39. package/v2Containers/CreativesContainer/tests/index.test.js +81 -0
  40. package/v2Containers/Email/index.js +31 -4
  41. package/v2Containers/Email/initialSchema.js +30 -0
  42. package/v2Containers/Email/messages.js +8 -0
  43. package/v2Containers/Email/tests/index.test.js +107 -0
  44. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +172 -8
  45. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +264 -5
  46. package/v2Containers/EmailWrapper/components/_emailHTMLEditor.scss +29 -0
  47. package/v2Containers/EmailWrapper/constants.js +5 -0
  48. package/v2Containers/EmailWrapper/messages.js +24 -0
  49. package/v2Containers/MobilePush/Create/constants.js +1 -0
  50. package/v2Containers/MobilePush/Create/index.js +2 -2
  51. package/v2Containers/MobilePush/Create/test/contentValidity.test.js +58 -0
  52. package/v2Containers/MobilePush/Edit/index.js +2 -2
  53. package/v2Containers/MobilePush/Edit/test/contentValidity.test.js +17 -0
  54. package/v2Containers/MobilePush/commonMethods.js +7 -8
  55. package/v2Containers/Rcs/index.js +1 -27
  56. package/v2Containers/TagList/index.js +7 -0
  57. package/v2Containers/TagList/tests/TagList.test.js +42 -0
  58. package/v2Containers/Templates/index.js +4 -9
  59. package/v2Containers/Templates/tests/index.test.js +35 -107
  60. package/v2Containers/Viber/index.scss +31 -0
  61. package/v2Containers/Whatsapp/index.js +1 -18
  62. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +2 -2
  63. package/v2Containers/Zalo/index.js +0 -18
  64. package/v2Containers/Rcs/tests/testAndPreviewFromListing.test.js +0 -132
  65. package/v2Containers/Whatsapp/tests/testAndPreviewFromListing.test.js +0 -91
  66. package/v2Containers/Zalo/tests/testAndPreviewFromListing.test.js +0 -98
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "9.0.73-alpha.sprint-testpreview.22",
4
+ "version": "9.1.1",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -5,6 +5,7 @@ import { IntlProvider } from 'react-intl';
5
5
  import CapTagListWithInput from '../index';
6
6
 
7
7
  const capturedTagListProps = { current: null };
8
+ const capturedCapRowProps = { current: null };
8
9
 
9
10
  jest.mock('../../../v2Containers/TagList', () => {
10
11
  const React = require('react');
@@ -15,7 +16,14 @@ jest.mock('../../../v2Containers/TagList', () => {
15
16
  return Mock;
16
17
  });
17
18
 
18
- jest.mock('@capillarytech/cap-ui-library/CapRow', () => ({ children }) => <div>{children}</div>);
19
+ jest.mock('@capillarytech/cap-ui-library/CapRow', () => {
20
+ const React = require('react');
21
+ const Mock = ({ children, ...rest }) => {
22
+ capturedCapRowProps.current = rest;
23
+ return <div>{children}</div>;
24
+ };
25
+ return Mock;
26
+ });
19
27
  jest.mock('@capillarytech/cap-ui-library/CapColumn', () => ({ children }) => <div>{children}</div>);
20
28
  jest.mock('@capillarytech/cap-ui-library/CapHeading', () => () => null);
21
29
  jest.mock('@capillarytech/cap-ui-library/CapInput', () => () => <input data-testid="cap-input" />);
@@ -27,6 +35,7 @@ const waitMap = {
27
35
  describe('CapTagListWithInput', () => {
28
36
  beforeEach(() => {
29
37
  capturedTagListProps.current = null;
38
+ capturedCapRowProps.current = null;
30
39
  });
31
40
 
32
41
  it('forwards waitEventContextTags to TagList', () => {
@@ -60,4 +69,52 @@ describe('CapTagListWithInput', () => {
60
69
 
61
70
  expect(capturedTagListProps.current.waitEventContextTags).toEqual({});
62
71
  });
72
+
73
+ it('forwards excludeTags to TagList', () => {
74
+ render(
75
+ <IntlProvider locale="en" messages={{}}>
76
+ <CapTagListWithInput
77
+ inputId="template-preheader"
78
+ inputOnChange={jest.fn()}
79
+ excludeTags={['unsubscribe']}
80
+ onTagSelect={jest.fn()}
81
+ onContextChange={jest.fn()}
82
+ />
83
+ </IntlProvider>
84
+ );
85
+
86
+ expect(capturedTagListProps.current.excludeTags).toEqual(['unsubscribe']);
87
+ });
88
+
89
+ it('uses default empty array for excludeTags when omitted', () => {
90
+ render(
91
+ <IntlProvider locale="en" messages={{}}>
92
+ <CapTagListWithInput
93
+ inputId="test-url"
94
+ inputOnChange={jest.fn()}
95
+ onTagSelect={jest.fn()}
96
+ onContextChange={jest.fn()}
97
+ />
98
+ </IntlProvider>
99
+ );
100
+
101
+ expect(capturedTagListProps.current.excludeTags).toEqual([]);
102
+ });
103
+
104
+ it('top-aligns the singleLineLayout row so an error message growing the input does not shift the Add Label button', () => {
105
+ render(
106
+ <IntlProvider locale="en" messages={{}}>
107
+ <CapTagListWithInput
108
+ inputId="template-preheader"
109
+ inputOnChange={jest.fn()}
110
+ inputErrorMessage="Unsubscribe tag is not allowed in Pre-header."
111
+ singleLineLayout
112
+ onTagSelect={jest.fn()}
113
+ onContextChange={jest.fn()}
114
+ />
115
+ </IntlProvider>
116
+ );
117
+
118
+ expect(capturedCapRowProps.current.align).toBe('top');
119
+ });
63
120
  });
@@ -7,6 +7,7 @@ import CapInput from '@capillarytech/cap-ui-library/CapInput';
7
7
  import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
8
8
  import TagList from '../../v2Containers/TagList';
9
9
  import messages from './messages';
10
+ import './index.scss';
10
11
 
11
12
  /**
12
13
  * CapTagListWithInput - A reusable component that combines TagList and CapInput
@@ -18,6 +19,7 @@ export const CapTagListWithInput = (props) => {
18
19
  // TagList props
19
20
  tags = [],
20
21
  injectedTags = {},
22
+ excludeTags = [],
21
23
  location = {},
22
24
  selectedOfferDetails = [],
23
25
  onTagSelect,
@@ -51,10 +53,65 @@ export const CapTagListWithInput = (props) => {
51
53
  showInput = true,
52
54
  inputProps = {},
53
55
  popoverPlacement,
56
+ // Tag-insert trigger inline with the input (not in a row above)
57
+ singleLineLayout = false,
58
+ // Content between input and tag trigger (singleLineLayout only)
59
+ extraContent = null,
54
60
  } = props;
55
61
 
56
62
  const { formatMessage } = intl;
57
63
 
64
+ const tagListElement = showTagList && (
65
+ <TagList
66
+ key={`${inputId}_tags`}
67
+ className={className}
68
+ moduleFilterEnabled={moduleFilterEnabled}
69
+ label={formatMessage(messages.addLabels)}
70
+ onTagSelect={onTagSelect}
71
+ onContextChange={onContextChange}
72
+ location={location}
73
+ tags={tags}
74
+ injectedTags={injectedTags}
75
+ excludeTags={excludeTags}
76
+ userLocale={userLocale}
77
+ selectedOfferDetails={selectedOfferDetails}
78
+ eventContextTags={eventContextTags}
79
+ waitEventContextTags={waitEventContextTags}
80
+ style={tagListStyle}
81
+ popoverPlacement={popoverPlacement}
82
+ restrictPersonalization={restrictPersonalization}
83
+ />
84
+ );
85
+
86
+ const inputElement = showInput && (
87
+ <CapInput
88
+ id={inputId}
89
+ onChange={inputOnChange}
90
+ placeholder={inputPlaceholder}
91
+ value={inputValue}
92
+ size={inputSize}
93
+ maxLength={inputMaxLength}
94
+ errorMessage={inputErrorMessage}
95
+ isRequired={inputRequired}
96
+ disabled={inputDisabled}
97
+ style={inputStyle}
98
+ {...inputProps}
99
+ />
100
+ );
101
+
102
+ if (singleLineLayout) {
103
+ return (
104
+ <CapColumn style={containerStyle}>
105
+ {/* "top", not "middle": keeps the buttons in place when an error grows the input */}
106
+ <CapRow align="top" type="flex" noWrap gap={8} className="cap-tag-list-with-input__single-line-row">
107
+ <CapColumn className="cap-tag-list-with-input__input-column">{inputElement}</CapColumn>
108
+ {extraContent}
109
+ {tagListElement}
110
+ </CapRow>
111
+ </CapColumn>
112
+ );
113
+ }
114
+
58
115
  return (
59
116
  <CapColumn style={containerStyle}>
60
117
  <CapRow align="middle" type="flex" noWrap>
@@ -64,42 +121,9 @@ export const CapTagListWithInput = (props) => {
64
121
  </CapHeading>
65
122
  )}
66
123
 
67
- {showTagList && (
68
- <TagList
69
- key={`${inputId}_tags`}
70
- className={className}
71
- moduleFilterEnabled={moduleFilterEnabled}
72
- label={formatMessage(messages.addLabels)}
73
- onTagSelect={onTagSelect}
74
- onContextChange={onContextChange}
75
- location={location}
76
- tags={tags}
77
- injectedTags={injectedTags}
78
- userLocale={userLocale}
79
- selectedOfferDetails={selectedOfferDetails}
80
- eventContextTags={eventContextTags}
81
- waitEventContextTags={waitEventContextTags}
82
- style={tagListStyle}
83
- popoverPlacement={popoverPlacement}
84
- restrictPersonalization={restrictPersonalization}
85
- />
86
- )}
124
+ {tagListElement}
87
125
  </CapRow>
88
- {showInput && (
89
- <CapInput
90
- id={inputId}
91
- onChange={inputOnChange}
92
- placeholder={inputPlaceholder}
93
- value={inputValue}
94
- size={inputSize}
95
- maxLength={inputMaxLength}
96
- errorMessage={inputErrorMessage}
97
- isRequired={inputRequired}
98
- disabled={inputDisabled}
99
- style={inputStyle}
100
- {...inputProps}
101
- />
102
- )}
126
+ {inputElement}
103
127
  </CapColumn>
104
128
  );
105
129
  };
@@ -109,6 +133,7 @@ CapTagListWithInput.propTypes = {
109
133
  // TagList props
110
134
  tags: PropTypes.array,
111
135
  injectedTags: PropTypes.object,
136
+ excludeTags: PropTypes.array,
112
137
  location: PropTypes.object,
113
138
  selectedOfferDetails: PropTypes.array,
114
139
  onTagSelect: PropTypes.func.isRequired,
@@ -145,6 +170,8 @@ CapTagListWithInput.propTypes = {
145
170
  showTagList: PropTypes.bool,
146
171
  showInput: PropTypes.bool,
147
172
  popoverPlacement: PropTypes.string,
173
+ singleLineLayout: PropTypes.bool,
174
+ extraContent: PropTypes.node,
148
175
  };
149
176
 
150
177
  CapTagListWithInput.defaultProps = {
@@ -176,6 +203,8 @@ CapTagListWithInput.defaultProps = {
176
203
  showInput: true,
177
204
  inputProps: {},
178
205
  popoverPlacement: undefined,
206
+ singleLineLayout: false,
207
+ extraContent: null,
179
208
  };
180
209
 
181
210
  export default injectIntl(CapTagListWithInput);
@@ -0,0 +1,9 @@
1
+ .cap-tag-list-with-input__single-line-row {
2
+ width: 100%;
3
+ }
4
+
5
+ // Input takes the remaining width and can shrink below its content size
6
+ .cap-tag-list-with-input__input-column {
7
+ flex: 1;
8
+ min-width: 0;
9
+ }
@@ -22,6 +22,7 @@ import {
22
22
  const EmailPreviewContent = ({
23
23
  content,
24
24
  subject,
25
+ preheader,
25
26
  device,
26
27
  isUpdating,
27
28
  error,
@@ -75,6 +76,11 @@ const EmailPreviewContent = ({
75
76
  </CapRow>
76
77
  </CapRow>
77
78
 
79
+ {/* Email Pre-header */}
80
+ {preheader && (
81
+ <CapRow useLegacy className="email-preheader">{preheader}</CapRow>
82
+ )}
83
+
78
84
  {/* Email Content - Varies based on state */}
79
85
  <CapRow useLegacy className="email-content">
80
86
  {contentSection}
@@ -131,6 +137,7 @@ const EmailPreviewContent = ({
131
137
  EmailPreviewContent.propTypes = {
132
138
  content: PropTypes.string,
133
139
  subject: PropTypes.string,
140
+ preheader: PropTypes.string,
134
141
  device: PropTypes.oneOf([DESKTOP, MOBILE, TABLET]),
135
142
  isUpdating: PropTypes.bool,
136
143
  error: PropTypes.string,
@@ -140,6 +147,7 @@ EmailPreviewContent.propTypes = {
140
147
  EmailPreviewContent.defaultProps = {
141
148
  content: '',
142
149
  subject: '',
150
+ preheader: '',
143
151
  device: DESKTOP,
144
152
  isUpdating: false,
145
153
  error: null,
@@ -520,6 +520,13 @@
520
520
  }
521
521
  }
522
522
 
523
+ // Email Pre-header
524
+ .email-preheader {
525
+ padding: $CAP_SPACE_12 $CAP_SPACE_16;
526
+ font-size: $FONT_SIZE_M;
527
+ color: #8c8c8c;
528
+ }
529
+
523
530
  // Email Content
524
531
  .email-content {
525
532
  border-top: $CAP_SPACE_01 solid $CAP_G12;
@@ -87,6 +87,7 @@ const UnifiedPreview = ({
87
87
  <EmailPreviewContent
88
88
  content={content?.resolvedBody || ''}
89
89
  subject={content?.resolvedTitle || ''}
90
+ preheader={content?.resolvedPreheader || ''}
90
91
  device={device}
91
92
  isUpdating={isUpdating}
92
93
  error={error}
@@ -43,7 +43,7 @@ export const searchCustomersRequested = (query) => ({
43
43
  */
44
44
  export const extractTagsRequested = (payload) => ({
45
45
  type: EXTRACT_TAGS_REQUESTED,
46
- payload, // { templateSubject, templateContent }
46
+ payload, // { templateSubject, templatePreheader, templateContent }
47
47
  });
48
48
 
49
49
  /**
@@ -39,8 +39,6 @@ import {
39
39
  mapRcsSuggestionForTestMeta,
40
40
  getRcsPrimaryTagExtractionText,
41
41
  mergeSyntheticMustacheTags,
42
- getWhatsappTagExtractionText,
43
- mergeZaloUnfilledParamTags,
44
42
  } from './utils';
45
43
  import AddTestCustomerButton from './AddTestCustomer';
46
44
  import ExistingCustomerModal from './ExistingCustomerModal';
@@ -237,8 +235,6 @@ const CommonTestAndPreview = (props) => {
237
235
  const [isExtractingSmsFallbackTags, setIsExtractingSmsFallbackTags] = useState(false);
238
236
  const [customValues, setCustomValues] = useState({});
239
237
  const previewCustomValuesRef = useRef({});
240
- // Values last applied to the preview (Update / Discard / customer prefill); drives client-side fill-in.
241
- const [previewTagValues, setPreviewTagValues] = useState(null);
242
238
  const [showJSON, setShowJSON] = useState(false);
243
239
  const [tagsExtracted, setTagsExtracted] = useState(false);
244
240
 
@@ -599,12 +595,6 @@ const CommonTestAndPreview = (props) => {
599
595
  if (channel === CHANNELS.RCS) {
600
596
  return mergeSyntheticMustacheTags(extractedTags, getRcsPrimaryTagExtractionText(formData));
601
597
  }
602
- if (channel === CHANNELS.WHATSAPP) {
603
- return mergeSyntheticMustacheTags(extractedTags, getWhatsappTagExtractionText(formData));
604
- }
605
- if (channel === CHANNELS.ZALO) {
606
- return mergeZaloUnfilledParamTags(extractedTags, formData);
607
- }
608
598
  return extractedTags ?? [];
609
599
  }, [
610
600
  channel,
@@ -690,8 +680,7 @@ const CommonTestAndPreview = (props) => {
690
680
  const tagName = tagPath.split('.').pop(); // Get the actual tag name from the path
691
681
  // Escape regex metacharacters in tagName (arbitrary JSON key) so it can't break the pattern.
692
682
  const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
693
- // Braces escaped: unescaped `{{1}}` parses `{1}` as a quantifier and never matches numbered slots.
694
- const tagRegex = new RegExp(`\\{\\{${escapedTagName}\\}\\}`, 'g');
683
+ const tagRegex = new RegExp(`{{${escapedTagName}}}`, 'g');
695
684
  const rawValue = tagValues[tagPath];
696
685
  // Preserve legitimate falsy values (0, false); only an unset/blank slot keeps the placeholder.
697
686
  const hasValue = rawValue !== null && rawValue !== undefined && rawValue !== '';
@@ -958,6 +947,7 @@ const CommonTestAndPreview = (props) => {
958
947
  return {
959
948
  ...basePayload,
960
949
  messageTitle: resolveTagsInText(formDataObj['template-subject'], customValuesObj),
950
+ messageSubtitle: resolveTagsInText(formDataObj['template-preheader'], customValuesObj),
961
951
  messageBody: contentStr,
962
952
  };
963
953
 
@@ -1080,6 +1070,7 @@ const CommonTestAndPreview = (props) => {
1080
1070
  case CHANNELS.EMAIL:
1081
1071
  return {
1082
1072
  templateSubject: formDataObj['template-subject'] || '',
1073
+ templatePreheader: formDataObj['template-preheader'] || '',
1083
1074
  templateContent: contentStr,
1084
1075
  };
1085
1076
 
@@ -1421,13 +1412,22 @@ const CommonTestAndPreview = (props) => {
1421
1412
  || formDataObj?.base?.['template-subject']
1422
1413
  || '';
1423
1414
 
1415
+ // Extract pre-header from formData (same shapes as subject)
1416
+ const preheader = formDataObj?.['template-preheader']
1417
+ || formDataObj?.[0]?.['template-preheader']
1418
+ || formDataObj?.base?.['template-preheader']
1419
+ || '';
1420
+
1424
1421
  // Extract body from previewData (resolved) or content
1425
1422
  const messageBody = previewDataObj?.resolvedBody || contentStr || '';
1426
1423
 
1427
- // Resolve tags in subject if custom values are provided
1424
+ // Resolve tags in subject/pre-header if custom values are provided
1428
1425
  const resolvedSubject = customValuesObj && Object.keys(customValuesObj).length > 0
1429
1426
  ? resolveTagsInText(subject, customValuesObj)
1430
1427
  : subject;
1428
+ const resolvedPreheader = customValuesObj && Object.keys(customValuesObj).length > 0
1429
+ ? resolveTagsInText(preheader, customValuesObj)
1430
+ : preheader;
1431
1431
 
1432
1432
  const emailChannelSettings = {
1433
1433
  channel: CHANNELS.EMAIL,
@@ -1453,6 +1453,7 @@ const CommonTestAndPreview = (props) => {
1453
1453
  channel: CHANNELS.EMAIL,
1454
1454
  messageBody,
1455
1455
  messageSubject: resolvedSubject,
1456
+ messagePreHeader: resolvedPreheader,
1456
1457
  },
1457
1458
  };
1458
1459
  }
@@ -1557,7 +1558,7 @@ const CommonTestAndPreview = (props) => {
1557
1558
 
1558
1559
  // Build whatsappMedia structure
1559
1560
  const whatsappMedia = {
1560
- header: resolveTagsInText(formDataObj?.templateHeader || formDataObj?.header || '', customValuesObj),
1561
+ header: formDataObj?.templateHeader || formDataObj?.header || '',
1561
1562
  footer: formDataObj?.templateFooter || formDataObj?.footer || '',
1562
1563
  headerVarMapped: formDataObj?.headerVarMapped || formDataObj?.headerVarMappedData || {},
1563
1564
  headerTemplate: formDataObj?.headerTemplate || formDataObj?.templateHeader || '',
@@ -2425,10 +2426,7 @@ const CommonTestAndPreview = (props) => {
2425
2426
  const resolvedVarMapped = {};
2426
2427
  Object.keys(templateConfigs.varMapped).forEach((key) => {
2427
2428
  const value = templateConfigs.varMapped[key];
2428
- // Param left empty on Overview → use the value entered in Test & Preview.
2429
- resolvedVarMapped[key] = value
2430
- ? resolveTagsInText(value, customValuesObj)
2431
- : (customValuesObj[key] ?? '');
2429
+ resolvedVarMapped[key] = resolveTagsInText(value, customValuesObj);
2432
2430
  });
2433
2431
  templateConfigs.varMapped = resolvedVarMapped;
2434
2432
  }
@@ -2459,9 +2457,7 @@ const CommonTestAndPreview = (props) => {
2459
2457
  const resolvedVarMapped = {};
2460
2458
  Object.keys(varMapped).forEach((key) => {
2461
2459
  const value = varMapped[key];
2462
- resolvedVarMapped[key] = value
2463
- ? resolveTagsInText(value, customValuesObj)
2464
- : (customValuesObj[key] ?? '');
2460
+ resolvedVarMapped[key] = resolveTagsInText(value, customValuesObj);
2465
2461
  });
2466
2462
  varMapped = resolvedVarMapped;
2467
2463
  }
@@ -2551,27 +2547,10 @@ const CommonTestAndPreview = (props) => {
2551
2547
  *
2552
2548
  * IMPORTANT: Use raw content/formData initially, only use previewDataHtml if preview call was made
2553
2549
  */
2554
- // Fills last-applied test values into raw template text when the preview API reply isn't usable.
2555
- const resolvePreviewText = (text) => (
2556
- typeof text === 'string' && previewTagValues ? resolveTagsInText(text, previewTagValues) : text
2557
- );
2558
-
2559
2550
  // Memoized RCS carousel content: only recomputes when the API response or raw template changes,
2560
2551
  // NOT on every customValues keystroke. Uses previewCustomValuesRef captured at Update Preview time.
2561
2552
  const rcsCarouselContentObj = useMemo(() => {
2562
2553
  if (channel !== CHANNELS.RCS) return null;
2563
- // Standalone card: fill entered values client-side; the preview API leaves numbered {{N}} unresolved.
2564
- const fillStandaloneCard = (card) => (
2565
- hasPreviewCallBeenMade && previewTagValues && card && typeof card === 'object' && !Array.isArray(card.carouselData)
2566
- ? {
2567
- ...card,
2568
- templateHeader: resolvePreviewText(card.templateHeader),
2569
- templateMessage: resolvePreviewText(card.templateMessage),
2570
- rcsTitle: resolvePreviewText(card.rcsTitle),
2571
- rcsDesc: resolvePreviewText(card.rcsDesc),
2572
- }
2573
- : card
2574
- );
2575
2554
  let resolvedContent = null;
2576
2555
  if (hasPreviewCallBeenMade && previewDataHtml?.resolvedBody) {
2577
2556
  resolvedContent = previewDataHtml.resolvedBody;
@@ -2579,7 +2558,7 @@ const CommonTestAndPreview = (props) => {
2579
2558
  try { resolvedContent = JSON.parse(resolvedContent); } catch (e) { resolvedContent = null; }
2580
2559
  }
2581
2560
  }
2582
- if (resolvedContent && typeof resolvedContent === 'object') return fillStandaloneCard(resolvedContent);
2561
+ if (resolvedContent && typeof resolvedContent === 'object') return resolvedContent;
2583
2562
 
2584
2563
  let rawRcs = content;
2585
2564
  if (typeof rawRcs === 'string') {
@@ -2604,9 +2583,9 @@ const CommonTestAndPreview = (props) => {
2604
2583
  }),
2605
2584
  };
2606
2585
  }
2607
- return fillStandaloneCard(rawRcs) || {};
2586
+ return rawRcs || {};
2608
2587
  // eslint-disable-next-line react-hooks/exhaustive-deps
2609
- }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData, previewTagValues]);
2588
+ }, [channel, content, hasPreviewCallBeenMade, previewDataHtml, formData]);
2610
2589
 
2611
2590
  const prepareUnifiedPreviewProps = () => {
2612
2591
  // Prepare content based on channel
@@ -2620,6 +2599,10 @@ const CommonTestAndPreview = (props) => {
2620
2599
  resolvedTitle: hasPreviewCallBeenMade && previewDataHtml?.resolvedTitle
2621
2600
  ? previewDataHtml.resolvedTitle
2622
2601
  : formData?.['template-subject'] || '',
2602
+ // The liquid preview API returns the resolved pre-header as `resolvedSubtitle`.
2603
+ resolvedPreheader: hasPreviewCallBeenMade && previewDataHtml?.resolvedSubtitle
2604
+ ? previewDataHtml.resolvedSubtitle
2605
+ : formData?.['template-preheader'] || '',
2623
2606
  };
2624
2607
  } else if (channel === CHANNELS.SMS) {
2625
2608
  // For SMS, content is the text message (string)
@@ -2668,14 +2651,6 @@ const CommonTestAndPreview = (props) => {
2668
2651
  // Use raw content if no preview call was made or resolvedContent is not available
2669
2652
  contentObj = parsedContent || {};
2670
2653
  }
2671
- // WhatsApp: fill entered values client-side; the preview API leaves numbered {{N}} unresolved.
2672
- if (channel === CHANNELS.WHATSAPP && hasPreviewCallBeenMade) {
2673
- contentObj = {
2674
- ...contentObj,
2675
- templateHeader: resolvePreviewText(contentObj.templateHeader),
2676
- templateMsg: resolvePreviewText(contentObj.templateMsg),
2677
- };
2678
- }
2679
2654
  } else if (channel === CHANNELS.RCS) {
2680
2655
  // rcsCarouselContentObj is a useMemo that handles both the API-response path and the
2681
2656
  // client-side fallback. It is stable across keystrokes (only recomputes when
@@ -2972,12 +2947,12 @@ const CommonTestAndPreview = (props) => {
2972
2947
  });
2973
2948
 
2974
2949
  // Always extract tags when content changes
2975
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
2950
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
2976
2951
  channel,
2977
2952
  formData,
2978
2953
  htmlFile,
2979
2954
  );
2980
- actions.extractTagsRequested({ templateSubject, templateContent });
2955
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
2981
2956
  }
2982
2957
 
2983
2958
  // Restore original handler
@@ -2992,32 +2967,32 @@ const CommonTestAndPreview = (props) => {
2992
2967
  const templateContent = currentTabData?.[activeTab]?.['template-content'];
2993
2968
  if (templateContent) {
2994
2969
  // Extract tags when showing (but don't set previewDataHtml - use raw content initially)
2995
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
2970
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
2996
2971
  channel,
2997
2972
  formData,
2998
2973
  templateContent
2999
2974
  );
3000
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
2975
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
3001
2976
  } else {
3002
2977
  // Fallback to content prop if no template content
3003
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
2978
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
3004
2979
  channel,
3005
2980
  formData,
3006
2981
  getCurrentContent
3007
2982
  );
3008
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
2983
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
3009
2984
  }
3010
2985
  }
3011
2986
  } else {
3012
2987
  // For non-email channels, extract tags from content
3013
2988
  // Use getCurrentContent for SMS (extracts from formData) or content prop for others
3014
2989
  const contentToConvert = getCurrentContent;
3015
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
2990
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
3016
2991
  channel,
3017
2992
  formData || {},
3018
2993
  contentToConvert
3019
2994
  );
3020
- actions.extractTagsRequested({ templateSubject, templateContent });
2995
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
3021
2996
  }
3022
2997
 
3023
2998
  // Fetch test customers and groups if enabled
@@ -3098,12 +3073,12 @@ const CommonTestAndPreview = (props) => {
3098
3073
  const handleContentUpdate = (contentToUpdate) => {
3099
3074
  // Extract tags from content (but don't set previewDataHtml - use raw content initially)
3100
3075
  // previewDataHtml will only be set when preview API call is made
3101
- const { templateSubject, templateContent: extractContent } = prepareTagExtractionPayload(
3076
+ const { templateSubject, templatePreheader, templateContent: extractContent } = prepareTagExtractionPayload(
3102
3077
  channel,
3103
3078
  formData,
3104
3079
  contentToUpdate
3105
3080
  );
3106
- actions.extractTagsRequested({ templateSubject, templateContent: extractContent });
3081
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent: extractContent });
3107
3082
  };
3108
3083
 
3109
3084
  if (isDragDrop) {
@@ -3129,7 +3104,6 @@ const CommonTestAndPreview = (props) => {
3129
3104
  previousBeeContentRef.current = '';
3130
3105
  setBeeContent('');
3131
3106
  setPreviewDataHtml('');
3132
- setPreviewTagValues(null);
3133
3107
  setSelectedCustomer(null);
3134
3108
  setRequiredTags([]);
3135
3109
  setOptionalTags([]);
@@ -3219,7 +3193,6 @@ const CommonTestAndPreview = (props) => {
3219
3193
  updatedValues,
3220
3194
  selectedCustomer
3221
3195
  );
3222
- setPreviewTagValues(updatedValues);
3223
3196
  actions.updatePreviewRequested(payload);
3224
3197
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
3225
3198
  void syncSmsFallbackPreview(updatedValues, selectedCustomer);
@@ -3289,7 +3262,6 @@ const CommonTestAndPreview = (props) => {
3289
3262
  setTagsExtracted(false);
3290
3263
  setPreviewDevice(DESKTOP);
3291
3264
  setPreviewDataHtml('');
3292
- setPreviewTagValues(null);
3293
3265
  setHasPreviewCallBeenMade(false); // Reset preview call flag
3294
3266
  setViberPreviewTagValues(null);
3295
3267
  setSelectedTestEntities([]);
@@ -3328,7 +3300,6 @@ const CommonTestAndPreview = (props) => {
3328
3300
  */
3329
3301
  const handleClearSelection = () => {
3330
3302
  setSelectedCustomer(null);
3331
- setPreviewTagValues(null);
3332
3303
  setHasPreviewCallBeenMade(false); // Reset flag when customer is cleared
3333
3304
  setViberPreviewTagValues(null);
3334
3305
 
@@ -3392,8 +3363,6 @@ const CommonTestAndPreview = (props) => {
3392
3363
  if (channel === CHANNELS.VIBER) {
3393
3364
  setViberPreviewTagValues({ ...emptyValues });
3394
3365
  }
3395
- previewCustomValuesRef.current = emptyValues;
3396
- setPreviewTagValues(emptyValues);
3397
3366
  actions.updatePreviewRequested(payload);
3398
3367
  setHasPreviewCallBeenMade(true); // Mark that preview call was made
3399
3368
  };
@@ -3407,7 +3376,6 @@ const CommonTestAndPreview = (props) => {
3407
3376
  // Capture customValues at click time so the carousel preview only updates here,
3408
3377
  // not on every subsequent keystroke.
3409
3378
  previewCustomValuesRef.current = customValues;
3410
- setPreviewTagValues(customValues);
3411
3379
  // For RCS: always dispatch with RCS channel/content so previewDataHtml stays RCS-shaped,
3412
3380
  // even when the user triggers update from the SMS fallback tab.
3413
3381
  // SMS fallback preview is kept in sync by syncSmsFallbackPreview via smsFallbackPreviewText.
@@ -3507,13 +3475,6 @@ const CommonTestAndPreview = (props) => {
3507
3475
  if (channel === CHANNELS.SMS && !smsTemplateHasMustacheTags(smsEditorBody)) {
3508
3476
  smsTagSource = [];
3509
3477
  }
3510
- // Unmapped variables become fillable test values, like RCS.
3511
- if (channel === CHANNELS.WHATSAPP) {
3512
- smsTagSource = mergeSyntheticMustacheTags(smsTagSource, getWhatsappTagExtractionText(formData));
3513
- }
3514
- if (channel === CHANNELS.ZALO) {
3515
- smsTagSource = mergeZaloUnfilledParamTags(smsTagSource, formData);
3516
- }
3517
3478
  const { required, optional, hasPersonalizationTags } = categorizeTags(smsTagSource);
3518
3479
  setRequiredTags(required);
3519
3480
  setOptionalTags(optional);
@@ -3559,12 +3520,12 @@ const CommonTestAndPreview = (props) => {
3559
3520
  }
3560
3521
 
3561
3522
  setTagsExtracted(true);
3562
- const { templateSubject, templateContent } = prepareTagExtractionPayload(
3523
+ const { templateSubject, templatePreheader, templateContent } = prepareTagExtractionPayload(
3563
3524
  activeChannelForActions,
3564
3525
  formData || {},
3565
3526
  contentToExtract
3566
3527
  );
3567
- actions.extractTagsRequested({ templateSubject, templateContent });
3528
+ actions.extractTagsRequested({ templateSubject, templatePreheader, templateContent });
3568
3529
  };
3569
3530
 
3570
3531
  /**
@@ -70,10 +70,11 @@ export function* searchCustomersSaga(action) {
70
70
  // Extract Tags Saga
71
71
  export function* extractTagsSaga(action) {
72
72
  try {
73
- const { templateSubject, templateContent } = action.payload;
73
+ const { templateSubject, templatePreheader, templateContent } = action.payload;
74
74
 
75
75
  const payload = {
76
76
  messageTitle: templateSubject || '',
77
+ messageSubtitle: templatePreheader || '',
77
78
  messageBody: templateContent || '',
78
79
  };
79
80