@capillarytech/creatives-library 8.0.249 → 8.0.250-alpha.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 (136) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +18 -0
  8. package/utils/common.js +5 -0
  9. package/utils/commonUtils.js +28 -5
  10. package/utils/tests/commonUtil.test.js +224 -0
  11. package/utils/transformTemplateConfig.js +0 -10
  12. package/v2Components/CapDeviceContent/index.js +61 -56
  13. package/v2Components/CapTagList/index.js +6 -1
  14. package/v2Components/CapTagListWithInput/index.js +5 -1
  15. package/v2Components/CapTagListWithInput/messages.js +1 -1
  16. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  17. package/v2Components/ErrorInfoNote/index.js +452 -72
  18. package/v2Components/ErrorInfoNote/messages.js +22 -0
  19. package/v2Components/ErrorInfoNote/style.scss +280 -4
  20. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  21. package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
  22. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
  23. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
  24. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  25. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  26. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  27. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
  28. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
  29. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  30. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  31. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
  32. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
  33. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  34. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  35. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  36. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  37. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  38. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  39. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
  40. package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
  41. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  42. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
  43. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
  44. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
  45. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
  46. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  47. package/v2Components/HtmlEditor/constants.js +42 -20
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  52. package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
  53. package/v2Components/HtmlEditor/index.js +1 -1
  54. package/v2Components/HtmlEditor/messages.js +95 -85
  55. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
  56. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  57. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  58. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
  59. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  60. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  61. package/v2Components/MobilePushPreviewV2/index.js +32 -7
  62. package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
  63. package/v2Components/TemplatePreview/index.js +47 -32
  64. package/v2Components/TemplatePreview/messages.js +4 -0
  65. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  66. package/v2Containers/BeeEditor/index.js +172 -90
  67. package/v2Containers/BeePopupEditor/constants.js +10 -0
  68. package/v2Containers/BeePopupEditor/index.js +193 -0
  69. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  70. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  71. package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
  72. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  73. package/v2Containers/CreativesContainer/constants.js +1 -0
  74. package/v2Containers/CreativesContainer/index.js +239 -46
  75. package/v2Containers/CreativesContainer/messages.js +8 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  77. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
  79. package/v2Containers/Email/actions.js +7 -0
  80. package/v2Containers/Email/constants.js +5 -1
  81. package/v2Containers/Email/index.js +222 -27
  82. package/v2Containers/Email/messages.js +32 -0
  83. package/v2Containers/Email/reducer.js +12 -1
  84. package/v2Containers/Email/sagas.js +61 -7
  85. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  86. package/v2Containers/Email/tests/sagas.test.js +320 -29
  87. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
  88. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
  89. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  90. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
  91. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  92. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  93. package/v2Containers/EmailWrapper/constants.js +2 -0
  94. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
  95. package/v2Containers/EmailWrapper/index.js +103 -23
  96. package/v2Containers/EmailWrapper/messages.js +61 -1
  97. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
  98. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
  99. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  100. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  101. package/v2Containers/InApp/actions.js +7 -0
  102. package/v2Containers/InApp/constants.js +20 -4
  103. package/v2Containers/InApp/index.js +802 -359
  104. package/v2Containers/InApp/index.scss +4 -3
  105. package/v2Containers/InApp/messages.js +7 -3
  106. package/v2Containers/InApp/reducer.js +21 -3
  107. package/v2Containers/InApp/sagas.js +29 -9
  108. package/v2Containers/InApp/selectors.js +25 -5
  109. package/v2Containers/InApp/tests/index.test.js +154 -50
  110. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  111. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  112. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  113. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
  114. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  115. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
  116. package/v2Containers/InAppWrapper/constants.js +16 -0
  117. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  118. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  119. package/v2Containers/InAppWrapper/index.js +148 -0
  120. package/v2Containers/InAppWrapper/messages.js +49 -0
  121. package/v2Containers/InappAdvance/index.js +1099 -0
  122. package/v2Containers/InappAdvance/index.scss +10 -0
  123. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  124. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  125. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  126. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  127. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  128. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  129. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  130. package/v2Containers/TagList/index.js +62 -19
  131. package/v2Containers/Templates/_templates.scss +60 -1
  132. package/v2Containers/Templates/index.js +89 -4
  133. package/v2Containers/Templates/messages.js +4 -0
  134. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  135. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  136. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -48,13 +48,14 @@ export const CapTagListWithInput = (props) => {
48
48
  showTagList = true,
49
49
  showInput = true,
50
50
  inputProps = {},
51
+ popoverPlacement,
51
52
  } = props;
52
53
 
53
54
  const { formatMessage } = intl;
54
55
 
55
56
  return (
56
57
  <CapColumn style={containerStyle}>
57
- <CapRow style={{display: 'flex', flexDirection: 'row'}}>
58
+ <CapRow align="middle" type="flex">
58
59
  {showHeading && headingText && (
59
60
  <CapHeading type={headingType} style={headingStyle}>
60
61
  {headingText}
@@ -76,6 +77,7 @@ export const CapTagListWithInput = (props) => {
76
77
  selectedOfferDetails={selectedOfferDetails}
77
78
  eventContextTags={eventContextTags}
78
79
  style={tagListStyle}
80
+ popoverPlacement={popoverPlacement}
79
81
  />
80
82
  )}
81
83
  </CapRow>
@@ -136,6 +138,7 @@ CapTagListWithInput.propTypes = {
136
138
  showHeading: PropTypes.bool,
137
139
  showTagList: PropTypes.bool,
138
140
  showInput: PropTypes.bool,
141
+ popoverPlacement: PropTypes.string,
139
142
  };
140
143
 
141
144
  CapTagListWithInput.defaultProps = {
@@ -164,6 +167,7 @@ CapTagListWithInput.defaultProps = {
164
167
  showTagList: true,
165
168
  showInput: true,
166
169
  inputProps: {},
170
+ popoverPlacement: undefined,
167
171
  };
168
172
 
169
173
  export default injectIntl(CapTagListWithInput);
@@ -5,6 +5,6 @@ const prefix = 'creatives.componentsV2.CapTagListWithInput';
5
5
  export default defineMessages({
6
6
  addLabels: {
7
7
  id: `${prefix}.addLabels`,
8
- defaultMessage: 'Add labels',
8
+ defaultMessage: 'Add label',
9
9
  },
10
10
  });
@@ -4,6 +4,11 @@ import '@testing-library/jest-dom';
4
4
  import { render, screen, fireEvent } from '../../../utils/test-utils';
5
5
  import { CapWhatsappCTA } from '../index';
6
6
 
7
+ // Mock the missing reducer
8
+ jest.mock('@capillarytech/cap-ui-library/CapCollapsibleLeftNavigation/reducer', () => {
9
+ return (state = {}) => state;
10
+ }, { virtual: true });
11
+
7
12
  const updateHandler = jest.fn();
8
13
  const deleteHandler = jest.fn();
9
14
  const initializeComponent = (
@@ -1,21 +1,440 @@
1
- import React from "react";
2
- import PropTypes from "prop-types";
3
- import CapRow from "@capillarytech/cap-ui-library/CapRow";
4
- import CapButton from "@capillarytech/cap-ui-library/CapButton";
5
- import CapIcon from "@capillarytech/cap-ui-library/CapIcon";
6
- import CapLabel from "@capillarytech/cap-ui-library/CapLabel";
7
- import CapList from "@capillarytech/cap-ui-library/CapList";
1
+ import React, { useState, useMemo } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
4
+ import CapButton from '@capillarytech/cap-ui-library/CapButton';
5
+ import CapIcon from '@capillarytech/cap-ui-library/CapIcon';
6
+ import CapLabel from '@capillarytech/cap-ui-library/CapLabel';
7
+ import CapTab from '@capillarytech/cap-ui-library/CapTab';
8
+ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
8
9
  import {
9
10
  FormattedMessage,
10
- FormattedNumber,
11
11
  injectIntl,
12
- } from "react-intl";
13
- import "./style.scss";
14
- import messages from "./messages";
15
- import { processErrors } from "./utils";
16
- import ErrorTypeRenderer from "./ErrorTypeRenderer";
17
- import { ANDROID, GENERIC, IOS } from "../../v2Containers/CreativesContainer/constants";
12
+ } from 'react-intl';
13
+ import './style.scss';
14
+ import messages from './messages';
15
+ import { processErrors } from './utils';
16
+ import ErrorTypeRenderer from './ErrorTypeRenderer';
17
+ import { ANDROID, GENERIC, IOS } from '../../v2Containers/CreativesContainer/constants';
18
18
 
19
+ // Label issue patterns - syntax errors related to tags
20
+ const LABEL_ISSUE_PATTERNS = [
21
+ 'tag must be paired',
22
+ 'open tag match failed',
23
+ 'closed tag match failed',
24
+ 'unclosed',
25
+ 'missing required',
26
+ 'tag-pair',
27
+ 'attr-value-not-empty',
28
+ 'attr-no-duplication',
29
+ 'tag-self-close',
30
+ 'spec-char-escape',
31
+ '</html>',
32
+ '<html>',
33
+ '</div>',
34
+ '<div>',
35
+ '{{',
36
+ '}}',
37
+ ];
38
+
39
+ /**
40
+ * Categorize error messages into HTML, Label, and Liquid categories
41
+ */
42
+ const categorizeErrorMessages = (standardErrors, liquidErrors) => {
43
+ const htmlIssues = [];
44
+ const labelIssues = [];
45
+ const liquidIssues = [];
46
+
47
+ // Process standard errors
48
+ (standardErrors || []).forEach((error, index) => {
49
+ const errorLower = (error || '').toLowerCase();
50
+
51
+ // Check if it's a Label (tag syntax) issue
52
+ const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
53
+ (pattern) => errorLower.includes(pattern.toLowerCase()),
54
+ );
55
+
56
+ if (isLabelIssue) {
57
+ labelIssues.push({
58
+ message: error,
59
+ severity: 'error',
60
+ index,
61
+ source: 'label',
62
+ });
63
+ } else {
64
+ htmlIssues.push({
65
+ message: error,
66
+ severity: 'error',
67
+ index,
68
+ source: 'html',
69
+ });
70
+ }
71
+ });
72
+
73
+ // Process liquid errors
74
+ (liquidErrors || []).forEach((error, index) => {
75
+ liquidIssues.push({
76
+ message: error,
77
+ severity: 'error',
78
+ index,
79
+ source: 'liquid',
80
+ });
81
+ });
82
+
83
+ return { htmlIssues, labelIssues, liquidIssues };
84
+ };
85
+
86
+ /**
87
+ * Get icon based on severity
88
+ */
89
+ const getSeverityIcon = (severity) => {
90
+ if (severity === 'warning') {
91
+ return <CapIcon type="alert-warning" className="error-info-note__icon error-info-note__icon--warning" />;
92
+ }
93
+ return <CapIcon type="warning-circle" className="error-info-note__icon error-info-note__icon--error" />;
94
+ };
95
+
96
+ /**
97
+ * Tab content component
98
+ */
99
+ const TabContent = ({ issues, onErrorClick, intl }) => {
100
+ if (!issues || issues.length === 0) {
101
+ return null;
102
+ }
103
+
104
+ const handleNavigateClick = (issue, e) => {
105
+ e.stopPropagation();
106
+ if (onErrorClick) {
107
+ onErrorClick(issue);
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div className="error-info-note__content">
113
+ {issues.map((issue, index) => {
114
+ const { severity, message } = issue;
115
+ const key = `${message}-${index}`;
116
+
117
+ // Parse line and char from message if present (format: "... Line X, Char Y.")
118
+ const lineMatch = message.match(/Line\s+(\d+)/i);
119
+ const charMatch = message.match(/Char\s+(\d+)/i);
120
+ const line = lineMatch ? parseInt(lineMatch[1], 10) : null;
121
+ const char = charMatch ? parseInt(charMatch[1], 10) : null;
122
+
123
+ // Extract rule from message (format: "... • rule-name")
124
+ const ruleMatch = message.match(/•\s*([a-z-]+)$/i);
125
+ const rule = ruleMatch ? ruleMatch[1] : null;
126
+
127
+ // Clean message (remove line/char and rule parts for display)
128
+ let displayMessage = message;
129
+ if (lineMatch) {
130
+ displayMessage = displayMessage.replace(/Line\s+\d+,?\s*/gi, '');
131
+ }
132
+ if (charMatch) {
133
+ displayMessage = displayMessage.replace(/Char\s+\d+\.?\s*/gi, '');
134
+ }
135
+ if (ruleMatch) {
136
+ displayMessage = displayMessage.replace(/•\s*[a-z-]+$/i, '');
137
+ }
138
+ displayMessage = displayMessage.trim();
139
+
140
+ return (
141
+ <div
142
+ key={key}
143
+ className={`error-info-note__item error-info-note__item--${severity || 'error'}`}
144
+ >
145
+ <div className="error-info-note__item-icon">
146
+ {getSeverityIcon(severity)}
147
+ </div>
148
+ <div className="error-info-note__item-content">
149
+ <span className="error-info-note__item-message">
150
+ {displayMessage}
151
+ </span>
152
+ {line && (
153
+ <span className="error-info-note__item-location">
154
+ Line
155
+ {' '}
156
+ {line}
157
+ {char ? `, Char ${char}` : ''}
158
+ .
159
+ </span>
160
+ )}
161
+ {rule && (
162
+ <span className="error-info-note__item-rule">
163
+
164
+ {' '}
165
+ {rule}
166
+ </span>
167
+ )}
168
+ </div>
169
+ {onErrorClick && (
170
+ <CapTooltip title={intl?.formatMessage ? intl.formatMessage({ id: 'errorInfoNote.navigateToError', defaultMessage: 'Go to error location' }) : 'Go to error location'}>
171
+ <button
172
+ type="button"
173
+ className="error-info-note__item-navigate"
174
+ onClick={(e) => handleNavigateClick({ ...issue, line, column: char }, e)}
175
+ aria-label="Go to error location"
176
+ >
177
+ <CapIcon type="redirection" />
178
+ </button>
179
+ </CapTooltip>
180
+ )}
181
+ </div>
182
+ );
183
+ })}
184
+ </div>
185
+ );
186
+ };
187
+
188
+ TabContent.propTypes = {
189
+ issues: PropTypes.array,
190
+ onErrorClick: PropTypes.func,
191
+ intl: PropTypes.object,
192
+ };
193
+
194
+ TabContent.defaultProps = {
195
+ issues: [],
196
+ onErrorClick: null,
197
+ intl: null,
198
+ };
199
+
200
+ /**
201
+ * ErrorInfoNote Component with Tabbed Interface
202
+ * @param {boolean} useLegacyDisplay - If true, uses simple list display instead of tabbed interface (for BEE Editor)
203
+ */
204
+ export const ErrorInfoNote = (props) => {
205
+ const {
206
+ errorMessages,
207
+ onErrorClick,
208
+ onClose,
209
+ isLiquidEnabled = true,
210
+ intl,
211
+ useLegacyDisplay = false, // Use simple list display instead of tabs (for BEE Editor)
212
+ } = props;
213
+
214
+ const [isDismissed, setIsDismissed] = useState(false);
215
+ const [activeKey, setActiveKey] = useState(null);
216
+
217
+ const {
218
+ LIQUID_ERROR_MSG: rawLiquidErrors = [],
219
+ STANDARD_ERROR_MSG: rawStandardErrors = [],
220
+ } = errorMessages || {};
221
+
222
+ // Detect if platform-specific (ANDROID/IOS) or GENERIC
223
+ const isObject = typeof rawStandardErrors === 'object' && rawStandardErrors !== null;
224
+ const isNotArray = !Array.isArray(rawLiquidErrors);
225
+ const hasPlatformKeys = isObject && isNotArray && [ANDROID, IOS, GENERIC].some((key) => key in rawLiquidErrors);
226
+
227
+ // For platform-specific errors or when useLegacyDisplay is true, use the legacy renderer
228
+ if (hasPlatformKeys) {
229
+ const androidErrors = {
230
+ STANDARD: processErrors(rawStandardErrors, 'standard', ANDROID, messages),
231
+ LIQUID: processErrors(rawLiquidErrors, 'liquid', ANDROID, messages),
232
+ };
233
+ const iosErrors = {
234
+ STANDARD: processErrors(rawStandardErrors, 'standard', IOS, messages),
235
+ LIQUID: processErrors(rawLiquidErrors, 'liquid', IOS, messages),
236
+ };
237
+ return (
238
+ <ErrorTypeRenderer
239
+ genericErrors={null}
240
+ androidErrors={androidErrors}
241
+ iosErrors={iosErrors}
242
+ ErrorSectionComponent={ErrorSection}
243
+ />
244
+ );
245
+ }
246
+
247
+ // For BEE Editor (useLegacyDisplay=true), use simple list display without tabs
248
+ if (useLegacyDisplay) {
249
+ const standardErrors = Array.isArray(rawStandardErrors) ? rawStandardErrors : [];
250
+ const liquidErrors = Array.isArray(rawLiquidErrors) ? rawLiquidErrors : [];
251
+ const hasStandardErrors = standardErrors.length > 0;
252
+ const hasLiquidErrors = liquidErrors.length > 0 && isLiquidEnabled;
253
+
254
+ if (!hasStandardErrors && !hasLiquidErrors) {
255
+ return null;
256
+ }
257
+
258
+ return (
259
+ <div className="error-container error-container--legacy">
260
+ {hasStandardErrors && (
261
+ <ErrorSection
262
+ title={<FormattedMessage {...messages.standardErrorHeader} />}
263
+ errors={standardErrors}
264
+ liquidError={false}
265
+ />
266
+ )}
267
+ {hasLiquidErrors && (
268
+ <ErrorSection
269
+ title={<FormattedMessage {...messages.dynamicErrorHeader} />}
270
+ errors={liquidErrors}
271
+ liquidError
272
+ />
273
+ )}
274
+ </div>
275
+ );
276
+ }
277
+
278
+ // Categorize errors for tabbed interface
279
+ const { htmlIssues, labelIssues, liquidIssues } = useMemo(() => categorizeErrorMessages(
280
+ Array.isArray(rawStandardErrors) ? rawStandardErrors : [],
281
+ Array.isArray(rawLiquidErrors) ? rawLiquidErrors : [],
282
+ ), [rawStandardErrors, rawLiquidErrors]);
283
+
284
+ // Calculate counts
285
+ const htmlCount = htmlIssues.length;
286
+ const labelCount = labelIssues.length;
287
+ const liquidCount = liquidIssues.length;
288
+ // Include liquid issues in total count even when liquid is disabled
289
+ // This ensures liquid errors are shown when liquid content is detected but feature is disabled
290
+ const totalCount = htmlCount + labelCount + liquidCount;
291
+
292
+ // Set default active key
293
+ useMemo(() => {
294
+ if (!activeKey) {
295
+ if (htmlCount > 0) {
296
+ setActiveKey('html');
297
+ } else if (labelCount > 0) {
298
+ setActiveKey('label');
299
+ } else if (liquidCount > 0) {
300
+ // Show liquid tab even when liquid is disabled if liquid content is detected
301
+ setActiveKey('liquid');
302
+ }
303
+ }
304
+ }, [htmlCount, labelCount, liquidCount, activeKey]);
305
+
306
+ // Handle close
307
+ const handleClose = () => {
308
+ setIsDismissed(true);
309
+ if (onClose) {
310
+ onClose();
311
+ }
312
+ };
313
+
314
+ // Handle liquid documentation click
315
+ const handleLiquidDocClick = () => {
316
+ window.open(
317
+ 'https://docs.capillarytech.com/docs/liquid-language-in-messages',
318
+ '_blank',
319
+ );
320
+ };
321
+
322
+ // Don't render if no issues or dismissed
323
+ if (totalCount === 0 || isDismissed) {
324
+ return null;
325
+ }
326
+
327
+ // Build tab panes (CapTab uses 'panes' with 'tab' and 'content' properties)
328
+ const tabPanes = [];
329
+
330
+ if (htmlCount > 0) {
331
+ tabPanes.push({
332
+ key: 'html',
333
+ tab: (
334
+ <span className="error-info-note__tab-label">
335
+ <FormattedMessage {...messages.htmlIssues} />
336
+ <span className="error-info-note__tab-count">
337
+ (
338
+ {htmlCount}
339
+ )
340
+ </span>
341
+ </span>
342
+ ),
343
+ content: (
344
+ <TabContent
345
+ issues={htmlIssues}
346
+ onErrorClick={onErrorClick}
347
+ intl={intl}
348
+ />
349
+ ),
350
+ });
351
+ }
352
+
353
+ if (labelCount > 0) {
354
+ tabPanes.push({
355
+ key: 'label',
356
+ tab: (
357
+ <span className="error-info-note__tab-label">
358
+ <FormattedMessage {...messages.labelIssues} />
359
+ <span className="error-info-note__tab-count">
360
+ (
361
+ {labelCount}
362
+ )
363
+ </span>
364
+ </span>
365
+ ),
366
+ content: (
367
+ <TabContent
368
+ issues={labelIssues}
369
+ onErrorClick={onErrorClick}
370
+ intl={intl}
371
+ />
372
+ ),
373
+ });
374
+ }
375
+
376
+ // Show liquid issues tab even when liquid is disabled if liquid content is detected
377
+ // This allows users to see errors when they add liquid content but liquid feature is not enabled
378
+ if (liquidCount > 0) {
379
+ tabPanes.push({
380
+ key: 'liquid',
381
+ tab: (
382
+ <span className="error-info-note__tab-label">
383
+ <FormattedMessage {...messages.liquidIssues} />
384
+ <span className="error-info-note__tab-count">
385
+ (
386
+ {liquidCount}
387
+ )
388
+ </span>
389
+ </span>
390
+ ),
391
+ content: (
392
+ <TabContent
393
+ issues={liquidIssues}
394
+ onErrorClick={onErrorClick}
395
+ intl={intl}
396
+ />
397
+ ),
398
+ });
399
+ }
400
+
401
+ return (
402
+ <div className="error-container error-container--tabs">
403
+ <CapRow className="error-info-note__header">
404
+ <CapTab
405
+ activeKey={activeKey || (tabPanes[0]?.key)}
406
+ onChange={setActiveKey}
407
+ panes={tabPanes}
408
+ className="error-info-note__tabs"
409
+ />
410
+ <CapRow className="error-info-note__actions">
411
+ {activeKey === 'liquid' && isLiquidEnabled && (
412
+ <CapButton
413
+ type="flat"
414
+ className="error-info-note__liquid-doc"
415
+ onClick={handleLiquidDocClick}
416
+ >
417
+ <FormattedMessage {...messages.liquidDoc} />
418
+ <CapIcon size="s" type="launch" />
419
+ </CapButton>
420
+ )}
421
+ <CapTooltip title={intl?.formatMessage ? intl.formatMessage({ id: 'errorInfoNote.closePanel', defaultMessage: 'Close validation panel' }) : 'Close validation panel'}>
422
+ <button
423
+ type="button"
424
+ className="error-info-note__close"
425
+ onClick={handleClose}
426
+ aria-label="Close validation panel"
427
+ >
428
+ <CapIcon type="close" />
429
+ </button>
430
+ </CapTooltip>
431
+ </CapRow>
432
+ </CapRow>
433
+ </div>
434
+ );
435
+ };
436
+
437
+ // Legacy ErrorSection component for platform-specific errors (backwards compatibility)
19
438
  const ErrorSection = ({
20
439
  title,
21
440
  errors,
@@ -26,7 +445,7 @@ const ErrorSection = ({
26
445
  {title && (
27
446
  <CapRow
28
447
  className={`error-header ${
29
- !liquidError ? "standard-error-header" : ""
448
+ !liquidError ? 'standard-error-header' : ''
30
449
  }`}
31
450
  >
32
451
  <>
@@ -38,8 +457,7 @@ const ErrorSection = ({
38
457
  <CapButton
39
458
  type="flat"
40
459
  className="add-btn"
41
- onClick={() => window.open("https://docs.capillarytech.com/docs/liquid-language-in-messages", "_blank")
42
- }
460
+ onClick={() => window.open('https://docs.capillarytech.com/docs/liquid-language-in-messages', '_blank')}
43
461
  >
44
462
  <FormattedMessage {...messages.liquidDoc} />
45
463
  <CapIcon size="s" type="launch" />
@@ -53,21 +471,15 @@ const ErrorSection = ({
53
471
  <CapLabel type="label2">{platformLabel}</CapLabel>
54
472
  </CapRow>
55
473
  )}
56
- <CapList
57
- className="error-list"
58
- size="small"
59
- dataSource={errors}
60
- renderItem={(error, index) => (
61
- <CapList.Item>
474
+ <div className="error-list-legacy">
475
+ {(errors || []).map((error) => (
476
+ <div key={`${error}`} className="error-list-legacy__item">
62
477
  <CapLabel type="label2" className="cap-list-v2-error-item">
63
- <CapLabel type="label2">
64
- <FormattedNumber value={index + 1} />.
65
- </CapLabel>
66
478
  <CapLabel type="label2">{error}</CapLabel>
67
479
  </CapLabel>
68
- </CapList.Item>
69
- )}
70
- />
480
+ </div>
481
+ ))}
482
+ </div>
71
483
  </>
72
484
  );
73
485
 
@@ -84,54 +496,16 @@ ErrorSection.defaultProps = {
84
496
  platformLabel: null,
85
497
  };
86
498
 
87
- export const ErrorInfoNote = (props) => {
88
- const { errorMessages } = props;
89
-
90
- const {
91
- LIQUID_ERROR_MSG: rawLiquidErrors = [],
92
- STANDARD_ERROR_MSG: rawStandardErrors = [],
93
- } = errorMessages || {};
94
-
95
- // Detect if platform-specific (ANDROID/IOS) or GENERIC
96
- const isObject = typeof rawStandardErrors === 'object' && rawStandardErrors !== null;
97
- const isNotArray = !Array.isArray(rawLiquidErrors);
98
- const hasPlatformKeys = isObject && isNotArray && [ANDROID, IOS, GENERIC].some((key) => key in rawLiquidErrors);
99
-
100
- if (hasPlatformKeys) {
101
- // Platform-specific
102
- const androidErrors = {
103
- STANDARD: processErrors(rawStandardErrors, 'standard', ANDROID, messages),
104
- LIQUID: processErrors(rawLiquidErrors, 'liquid', ANDROID, messages),
105
- };
106
- const iosErrors = {
107
- STANDARD: processErrors(rawStandardErrors, 'standard', IOS, messages),
108
- LIQUID: processErrors(rawLiquidErrors, 'liquid', IOS, messages),
109
- };
110
- return (
111
- <ErrorTypeRenderer
112
- genericErrors={null}
113
- androidErrors={androidErrors}
114
- iosErrors={iosErrors}
115
- ErrorSectionComponent={ErrorSection}
116
- />
117
- );
118
- }
119
- // GENERIC (not platform-specific)
120
- const genericStandard = processErrors(rawStandardErrors, 'standard', null, messages);
121
- const genericLiquid = processErrors(rawLiquidErrors, 'liquid', null, messages);
122
- return (
123
- <ErrorTypeRenderer
124
- genericErrors={{ standard: genericStandard, liquid: genericLiquid }}
125
- ErrorSectionComponent={ErrorSection}
126
- />
127
- );
128
- };
129
-
130
499
  ErrorInfoNote.defaultProps = {
131
500
  errorMessages: {
132
501
  LIQUID_ERROR_MSG: [],
133
502
  STANDARD_ERROR_MSG: [],
134
503
  },
504
+ onErrorClick: null,
505
+ onClose: null,
506
+ isLiquidEnabled: true,
507
+ intl: null,
508
+ useLegacyDisplay: false, // Use simple list display for BEE Editor
135
509
  };
136
510
 
137
511
  ErrorInfoNote.propTypes = {
@@ -153,5 +527,11 @@ ErrorInfoNote.propTypes = {
153
527
  }),
154
528
  ]),
155
529
  }),
530
+ onErrorClick: PropTypes.func,
531
+ onClose: PropTypes.func,
532
+ isLiquidEnabled: PropTypes.bool,
533
+ intl: PropTypes.object,
534
+ useLegacyDisplay: PropTypes.bool, // Use simple list display for BEE Editor
156
535
  };
536
+
157
537
  export default injectIntl(ErrorInfoNote);
@@ -6,6 +6,28 @@
6
6
  import { defineMessages } from "react-intl";
7
7
  const scope = "creatives.componentsV2.ErrorInfoNote";
8
8
  export default defineMessages({
9
+ // Tab labels for new tabbed interface
10
+ htmlIssues: {
11
+ id: `${scope}.htmlIssues`,
12
+ defaultMessage: "HTML issues",
13
+ },
14
+ labelIssues: {
15
+ id: `${scope}.labelIssues`,
16
+ defaultMessage: "Label issues",
17
+ },
18
+ liquidIssues: {
19
+ id: `${scope}.liquidIssues`,
20
+ defaultMessage: "Liquid issues",
21
+ },
22
+ navigateToError: {
23
+ id: `${scope}.navigateToError`,
24
+ defaultMessage: "Go to error location",
25
+ },
26
+ closePanel: {
27
+ id: `${scope}.closePanel`,
28
+ defaultMessage: "Close validation panel",
29
+ },
30
+ // Legacy messages (kept for backwards compatibility)
9
31
  dynamicErrorHeader: {
10
32
  id: `${scope}.dynamicErrorHeader`,
11
33
  defaultMessage: