@capillarytech/creatives-library 8.0.262-alpha.0 → 8.0.262
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.
- package/package.json +1 -1
- package/v2Components/ErrorInfoNote/index.js +57 -112
- package/v2Components/ErrorInfoNote/messages.js +8 -12
- package/v2Components/ErrorInfoNote/style.scss +0 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +46 -182
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +11 -15
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +7 -8
- package/v2Components/HtmlEditor/_htmlEditor.scss +6 -6
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +1 -1
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +4 -4
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +17 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +15 -28
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +0 -4
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +7 -31
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +53 -26
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +74 -144
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +14 -14
- package/v2Components/HtmlEditor/hooks/useValidation.js +23 -3
- package/v2Components/HtmlEditor/utils/validationConstants.js +3 -4
- package/v2Containers/CreativesContainer/index.js +1 -3
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +6 -9
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +5 -49
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +23 -38
- package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ValidationTabs Component
|
|
3
3
|
*
|
|
4
|
-
* Displays validation
|
|
5
|
-
* -
|
|
6
|
-
* -
|
|
7
|
-
* - Liquid issues: Liquid expression errors (shown when liquid content is detected, even if liquid feature is disabled)
|
|
4
|
+
* Displays validation issues in a tabbed interface with 2 categories:
|
|
5
|
+
* - Errors: Blocking issues (API errors, Rule Group #1, client-side Liquid errors)
|
|
6
|
+
* - Warnings: Non-blocking issues
|
|
8
7
|
*/
|
|
9
8
|
|
|
10
|
-
import React, { useState, useMemo } from 'react';
|
|
9
|
+
import React, { useState, useMemo, useEffect } from 'react';
|
|
11
10
|
import PropTypes from 'prop-types';
|
|
12
11
|
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
|
|
13
12
|
|
|
@@ -19,48 +18,27 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
|
19
18
|
// Messages
|
|
20
19
|
import messages from './messages';
|
|
21
20
|
import { BLOCKING_ERROR_RULE_IDS } from '../../constants';
|
|
22
|
-
import { ISSUE_SOURCES, LABEL_ISSUE_PATTERNS } from '../../utils/validationConstants';
|
|
23
21
|
|
|
24
22
|
// Styles
|
|
25
23
|
import './_validationTabs.scss';
|
|
26
|
-
import {StyledCapTab} from '../../../../v2Containers/MobilePushNew/style';
|
|
24
|
+
import { StyledCapTab } from '../../../../v2Containers/MobilePushNew/style';
|
|
27
25
|
|
|
28
26
|
/**
|
|
29
|
-
*
|
|
27
|
+
* Group issues into Errors (blocking) and Warnings (non-blocking)
|
|
30
28
|
*/
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const liquidIssues = [];
|
|
29
|
+
const groupByErrorsAndWarnings = (allIssues) => {
|
|
30
|
+
const errors = [];
|
|
31
|
+
const warnings = [];
|
|
35
32
|
|
|
36
33
|
allIssues.forEach((issue) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// Check if it's a Liquid issue - ONLY by source, not by message content
|
|
42
|
-
// This prevents false positives where HTML errors mention liquid syntax
|
|
43
|
-
if (source === ISSUE_SOURCES.LIQUID) {
|
|
44
|
-
liquidIssues.push(issue);
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Check if it's a Label (tag syntax) issue
|
|
49
|
-
const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
|
|
50
|
-
(pattern) => messageLower.includes(pattern.toLowerCase())
|
|
51
|
-
|| ruleLower.includes(pattern.toLowerCase()),
|
|
52
|
-
);
|
|
53
|
-
|
|
54
|
-
if (isLabelIssue) {
|
|
55
|
-
labelIssues.push(issue);
|
|
56
|
-
return;
|
|
34
|
+
if (isBlockingError(issue)) {
|
|
35
|
+
errors.push(issue);
|
|
36
|
+
} else {
|
|
37
|
+
warnings.push(issue);
|
|
57
38
|
}
|
|
58
|
-
|
|
59
|
-
// Default to HTML issues
|
|
60
|
-
htmlIssues.push(issue);
|
|
61
39
|
});
|
|
62
40
|
|
|
63
|
-
return {
|
|
41
|
+
return { errors, warnings };
|
|
64
42
|
};
|
|
65
43
|
|
|
66
44
|
/**
|
|
@@ -83,24 +61,13 @@ const isBlockingError = (issue) => {
|
|
|
83
61
|
return false;
|
|
84
62
|
};
|
|
85
63
|
|
|
86
|
-
/**
|
|
87
|
-
* Get icon based on whether issue is blocking error or warning
|
|
88
|
-
* Blocking errors use error-icon, warnings use alert-warning
|
|
89
|
-
*/
|
|
90
|
-
const getSeverityIcon = (issue) => {
|
|
91
|
-
if (isBlockingError(issue)) {
|
|
92
|
-
return <CapIcon type="error-icon" className="validation-tabs__icon validation-tabs__icon--error" />;
|
|
93
|
-
}
|
|
94
|
-
// All other issues (warnings, non-blocking) use warning icon
|
|
95
|
-
return <CapIcon type="alert-warning" className="validation-tabs__icon validation-tabs__icon--warning" />;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
64
|
/**
|
|
99
65
|
* ValidationTabContent - Renders the content for each tab
|
|
100
66
|
*/
|
|
101
67
|
const ValidationTabContent = ({
|
|
102
68
|
issues,
|
|
103
69
|
onErrorClick,
|
|
70
|
+
onExpand,
|
|
104
71
|
}) => {
|
|
105
72
|
if (!issues || issues.length === 0) {
|
|
106
73
|
return null;
|
|
@@ -108,6 +75,10 @@ const ValidationTabContent = ({
|
|
|
108
75
|
|
|
109
76
|
const handleNavigateClick = (issue, e) => {
|
|
110
77
|
e.stopPropagation();
|
|
78
|
+
// Expand panel when redirection is clicked so it does not stay stuck to footer
|
|
79
|
+
if (onExpand) {
|
|
80
|
+
onExpand();
|
|
81
|
+
}
|
|
111
82
|
if (onErrorClick) {
|
|
112
83
|
// Always call onErrorClick to acknowledge the error (enables buttons)
|
|
113
84
|
// If line number exists, navigate to it; otherwise just acknowledge and focus editor
|
|
@@ -135,9 +106,6 @@ const ValidationTabContent = ({
|
|
|
135
106
|
key={key}
|
|
136
107
|
className={`validation-tabs__item validation-tabs__item--${displaySeverity}`}
|
|
137
108
|
>
|
|
138
|
-
<div className="validation-tabs__item-icon">
|
|
139
|
-
{getSeverityIcon(issue)}
|
|
140
|
-
</div>
|
|
141
109
|
<div className="validation-tabs__item-content">
|
|
142
110
|
<span className="validation-tabs__item-message">
|
|
143
111
|
{message}
|
|
@@ -173,11 +141,13 @@ const ValidationTabContent = ({
|
|
|
173
141
|
ValidationTabContent.propTypes = {
|
|
174
142
|
issues: PropTypes.array,
|
|
175
143
|
onErrorClick: PropTypes.func,
|
|
144
|
+
onExpand: PropTypes.func,
|
|
176
145
|
};
|
|
177
146
|
|
|
178
147
|
ValidationTabContent.defaultProps = {
|
|
179
148
|
issues: [],
|
|
180
149
|
onErrorClick: null,
|
|
150
|
+
onExpand: null,
|
|
181
151
|
};
|
|
182
152
|
|
|
183
153
|
/**
|
|
@@ -187,137 +157,95 @@ const ValidationTabs = ({
|
|
|
187
157
|
intl,
|
|
188
158
|
validation,
|
|
189
159
|
onErrorClick,
|
|
190
|
-
|
|
160
|
+
isCollapsed = false,
|
|
161
|
+
onToggleCollapse,
|
|
162
|
+
onExpand,
|
|
191
163
|
className,
|
|
192
164
|
}) => {
|
|
193
|
-
const [activeKey, setActiveKey] = useState(
|
|
165
|
+
const [activeKey, setActiveKey] = useState('errors');
|
|
194
166
|
|
|
195
|
-
//
|
|
196
|
-
const {
|
|
167
|
+
// Group issues into Errors (blocking) and Warnings (non-blocking)
|
|
168
|
+
const { errors, warnings } = useMemo(() => {
|
|
197
169
|
if (!validation) {
|
|
198
|
-
return {
|
|
170
|
+
return { errors: [], warnings: [] };
|
|
199
171
|
}
|
|
200
|
-
|
|
201
|
-
// Get all issues from validation
|
|
202
172
|
const allIssues = validation.getAllIssues ? validation.getAllIssues() : [];
|
|
203
|
-
|
|
204
|
-
return categorized;
|
|
173
|
+
return groupByErrorsAndWarnings(allIssues);
|
|
205
174
|
}, [validation]);
|
|
206
175
|
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
const
|
|
210
|
-
const liquidCount = liquidIssues.length;
|
|
211
|
-
// Include liquid issues in total count even when liquid is disabled
|
|
212
|
-
// This ensures liquid errors are shown when liquid content is detected but feature is disabled
|
|
213
|
-
const totalCount = htmlCount + labelCount + liquidCount;
|
|
176
|
+
const errorsCount = errors.length;
|
|
177
|
+
const warningsCount = warnings.length;
|
|
178
|
+
const totalCount = errorsCount + warningsCount;
|
|
214
179
|
|
|
215
|
-
//
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
setActiveKey('
|
|
219
|
-
} else if (
|
|
220
|
-
setActiveKey('
|
|
221
|
-
} else if (liquidCount > 0 && !activeKey) {
|
|
222
|
-
// Show liquid tab even when liquid is disabled if liquid content is detected
|
|
223
|
-
setActiveKey('liquid');
|
|
180
|
+
// Default active tab: errors if any, else warnings
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (errorsCount > 0) {
|
|
183
|
+
setActiveKey('errors');
|
|
184
|
+
} else if (warningsCount > 0) {
|
|
185
|
+
setActiveKey('warnings');
|
|
224
186
|
}
|
|
225
|
-
}, [
|
|
187
|
+
}, [errorsCount, warningsCount]);
|
|
226
188
|
|
|
227
|
-
// Don't render if no issues
|
|
228
189
|
if (totalCount === 0) {
|
|
229
190
|
return null;
|
|
230
191
|
}
|
|
231
192
|
|
|
232
|
-
// Build tab panes (CapTab uses 'panes' with 'tab' and 'content' properties)
|
|
233
193
|
const tabPanes = [];
|
|
234
194
|
|
|
235
|
-
if (
|
|
195
|
+
if (errorsCount > 0) {
|
|
236
196
|
tabPanes.push({
|
|
237
|
-
key: '
|
|
197
|
+
key: 'errors',
|
|
238
198
|
tab: (
|
|
239
|
-
<CapTooltip title={`${intl.formatMessage(messages.
|
|
240
|
-
<span className="validation-tabs__tab-label">
|
|
241
|
-
<FormattedMessage {...messages.
|
|
242
|
-
<span className="validation-tabs__tab-count">
|
|
243
|
-
(
|
|
244
|
-
{htmlCount}
|
|
245
|
-
)
|
|
246
|
-
</span>
|
|
199
|
+
<CapTooltip title={`${intl.formatMessage(messages.errors)} (${errorsCount})`}>
|
|
200
|
+
<span className="validation-tabs__tab-label validation-tabs__tab-label--errors">
|
|
201
|
+
<FormattedMessage {...messages.errors} />
|
|
202
|
+
<span className="validation-tabs__tab-count">({errorsCount})</span>
|
|
247
203
|
</span>
|
|
248
204
|
</CapTooltip>
|
|
249
205
|
),
|
|
250
206
|
content: (
|
|
251
207
|
<ValidationTabContent
|
|
252
|
-
issues={
|
|
208
|
+
issues={errors}
|
|
253
209
|
onErrorClick={onErrorClick}
|
|
254
|
-
|
|
210
|
+
onExpand={onExpand}
|
|
255
211
|
/>
|
|
256
212
|
),
|
|
257
213
|
});
|
|
258
214
|
}
|
|
259
215
|
|
|
260
|
-
if (
|
|
216
|
+
if (warningsCount > 0) {
|
|
261
217
|
tabPanes.push({
|
|
262
|
-
key: '
|
|
218
|
+
key: 'warnings',
|
|
263
219
|
tab: (
|
|
264
|
-
<CapTooltip title={`${intl.formatMessage(messages.
|
|
265
|
-
<span className="validation-tabs__tab-label">
|
|
266
|
-
<FormattedMessage {...messages.
|
|
267
|
-
<span className="validation-tabs__tab-count">
|
|
268
|
-
(
|
|
269
|
-
{labelCount}
|
|
270
|
-
)
|
|
271
|
-
</span>
|
|
220
|
+
<CapTooltip title={`${intl.formatMessage(messages.warnings)} (${warningsCount})`}>
|
|
221
|
+
<span className="validation-tabs__tab-label validation-tabs__tab-label--warnings">
|
|
222
|
+
<FormattedMessage {...messages.warnings} />
|
|
223
|
+
<span className="validation-tabs__tab-count">({warningsCount})</span>
|
|
272
224
|
</span>
|
|
273
225
|
</CapTooltip>
|
|
274
226
|
),
|
|
275
227
|
content: (
|
|
276
228
|
<ValidationTabContent
|
|
277
|
-
issues={
|
|
229
|
+
issues={warnings}
|
|
278
230
|
onErrorClick={onErrorClick}
|
|
279
|
-
|
|
231
|
+
onExpand={onExpand}
|
|
280
232
|
/>
|
|
281
233
|
),
|
|
282
234
|
});
|
|
283
235
|
}
|
|
284
236
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
tabPanes.push({
|
|
289
|
-
key: 'liquid',
|
|
290
|
-
tab: (
|
|
291
|
-
<CapTooltip title={`${intl.formatMessage(messages.liquidIssues)} (${liquidCount})`}>
|
|
292
|
-
<span className="validation-tabs__tab-label">
|
|
293
|
-
<FormattedMessage {...messages.liquidIssues} />
|
|
294
|
-
<span className="validation-tabs__tab-count">
|
|
295
|
-
(
|
|
296
|
-
{liquidCount}
|
|
297
|
-
)
|
|
298
|
-
</span>
|
|
299
|
-
</span>
|
|
300
|
-
</CapTooltip>
|
|
301
|
-
),
|
|
302
|
-
content: (
|
|
303
|
-
<ValidationTabContent
|
|
304
|
-
issues={liquidIssues}
|
|
305
|
-
onErrorClick={onErrorClick}
|
|
306
|
-
intl={intl}
|
|
307
|
-
/>
|
|
308
|
-
),
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Handle close
|
|
313
|
-
const handleClose = () => {
|
|
314
|
-
if (onClose) {
|
|
315
|
-
onClose();
|
|
237
|
+
const handleToggleCollapse = () => {
|
|
238
|
+
if (onToggleCollapse) {
|
|
239
|
+
onToggleCollapse();
|
|
316
240
|
}
|
|
317
241
|
};
|
|
318
242
|
|
|
243
|
+
const collapseLabel = isCollapsed
|
|
244
|
+
? intl.formatMessage(messages.expandPanel)
|
|
245
|
+
: intl.formatMessage(messages.collapsePanel);
|
|
246
|
+
|
|
319
247
|
return (
|
|
320
|
-
<div className={`validation-tabs ${className || ''}`}>
|
|
248
|
+
<div className={`validation-tabs ${isCollapsed ? 'validation-tabs--collapsed' : ''} ${className || ''}`}>
|
|
321
249
|
<CapRow className="validation-tabs__header">
|
|
322
250
|
<StyledCapTab
|
|
323
251
|
className="validation-tabs__tabs"
|
|
@@ -326,14 +254,14 @@ const ValidationTabs = ({
|
|
|
326
254
|
panes={tabPanes}
|
|
327
255
|
/>
|
|
328
256
|
<CapRow className="validation-tabs__actions">
|
|
329
|
-
<CapTooltip title={
|
|
257
|
+
<CapTooltip title={collapseLabel}>
|
|
330
258
|
<button
|
|
331
259
|
type="button"
|
|
332
|
-
className="validation-
|
|
333
|
-
onClick={
|
|
334
|
-
aria-label={
|
|
260
|
+
className="validation-tabs__collapse-toggle"
|
|
261
|
+
onClick={handleToggleCollapse}
|
|
262
|
+
aria-label={collapseLabel}
|
|
335
263
|
>
|
|
336
|
-
<CapIcon type=
|
|
264
|
+
<CapIcon type={isCollapsed ? 'chevron-up' : 'chevron-down'} />
|
|
337
265
|
</button>
|
|
338
266
|
</CapTooltip>
|
|
339
267
|
</CapRow>
|
|
@@ -348,16 +276,18 @@ ValidationTabs.propTypes = {
|
|
|
348
276
|
getAllIssues: PropTypes.func,
|
|
349
277
|
}),
|
|
350
278
|
onErrorClick: PropTypes.func,
|
|
351
|
-
|
|
352
|
-
|
|
279
|
+
isCollapsed: PropTypes.bool,
|
|
280
|
+
onToggleCollapse: PropTypes.func,
|
|
281
|
+
onExpand: PropTypes.func,
|
|
353
282
|
className: PropTypes.string,
|
|
354
283
|
};
|
|
355
284
|
|
|
356
285
|
ValidationTabs.defaultProps = {
|
|
357
286
|
validation: null,
|
|
358
287
|
onErrorClick: null,
|
|
359
|
-
|
|
360
|
-
|
|
288
|
+
isCollapsed: false,
|
|
289
|
+
onToggleCollapse: null,
|
|
290
|
+
onExpand: null,
|
|
361
291
|
className: '',
|
|
362
292
|
};
|
|
363
293
|
|
|
@@ -9,18 +9,14 @@ import { defineMessages } from 'react-intl';
|
|
|
9
9
|
const scope = 'app.components.HtmlEditor.ValidationTabs';
|
|
10
10
|
|
|
11
11
|
export default defineMessages({
|
|
12
|
-
// Tab labels
|
|
13
|
-
|
|
14
|
-
id: `${scope}.
|
|
15
|
-
defaultMessage: '
|
|
12
|
+
// Tab labels (Errors = blocking, Warnings = non-blocking)
|
|
13
|
+
errors: {
|
|
14
|
+
id: `${scope}.errors`,
|
|
15
|
+
defaultMessage: 'Errors',
|
|
16
16
|
},
|
|
17
|
-
|
|
18
|
-
id: `${scope}.
|
|
19
|
-
defaultMessage: '
|
|
20
|
-
},
|
|
21
|
-
liquidIssues: {
|
|
22
|
-
id: `${scope}.liquidIssues`,
|
|
23
|
-
defaultMessage: 'Liquid issues',
|
|
17
|
+
warnings: {
|
|
18
|
+
id: `${scope}.warnings`,
|
|
19
|
+
defaultMessage: 'Warnings',
|
|
24
20
|
},
|
|
25
21
|
|
|
26
22
|
// Error item labels
|
|
@@ -38,9 +34,13 @@ export default defineMessages({
|
|
|
38
34
|
id: `${scope}.navigateToError`,
|
|
39
35
|
defaultMessage: 'Go to error location',
|
|
40
36
|
},
|
|
41
|
-
|
|
42
|
-
id: `${scope}.
|
|
43
|
-
defaultMessage: '
|
|
37
|
+
collapsePanel: {
|
|
38
|
+
id: `${scope}.collapsePanel`,
|
|
39
|
+
defaultMessage: 'Collapse validation panel',
|
|
40
|
+
},
|
|
41
|
+
expandPanel: {
|
|
42
|
+
id: `${scope}.expandPanel`,
|
|
43
|
+
defaultMessage: 'Expand validation panel',
|
|
44
44
|
},
|
|
45
45
|
|
|
46
46
|
// Liquid documentation
|
|
@@ -29,6 +29,21 @@ const getLineNumberFromPosition = (text, position) => {
|
|
|
29
29
|
return text.substring(0, position).split('\n').length;
|
|
30
30
|
};
|
|
31
31
|
|
|
32
|
+
/**
|
|
33
|
+
* Get 1-based line and column from a character position in text
|
|
34
|
+
*/
|
|
35
|
+
const getLineAndColumnFromPosition = (text, position) => {
|
|
36
|
+
if (!text || position === undefined || position < 0) {
|
|
37
|
+
return { line: 1, column: 1 };
|
|
38
|
+
}
|
|
39
|
+
const before = text.substring(0, position);
|
|
40
|
+
const lines = before.split('\n');
|
|
41
|
+
const line = lines.length;
|
|
42
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
43
|
+
const column = lastLine.length + 1;
|
|
44
|
+
return { line, column };
|
|
45
|
+
};
|
|
46
|
+
|
|
32
47
|
/**
|
|
33
48
|
* Find line number for a tag or pattern in content
|
|
34
49
|
* Used for API errors to locate where the error occurs
|
|
@@ -350,14 +365,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
350
365
|
});
|
|
351
366
|
|
|
352
367
|
// Security: protocol types are Rule Group #1 (blocking); others are warnings
|
|
368
|
+
// Use issue.position (from findUnsafeContent) to show real line/column when available
|
|
353
369
|
const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
|
|
370
|
+
const contentStr = typeof content === 'string' ? content : '';
|
|
354
371
|
const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
|
|
355
372
|
const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
|
|
373
|
+
const { line, column } = (issue?.position !== undefined && contentStr)
|
|
374
|
+
? getLineAndColumnFromPosition(contentStr, issue.position)
|
|
375
|
+
: { line: 1, column: 1 };
|
|
356
376
|
return {
|
|
357
377
|
type: isBlocking ? 'error' : 'warning',
|
|
358
378
|
message: `Security issue: ${issue.type}`,
|
|
359
|
-
line
|
|
360
|
-
column
|
|
379
|
+
line,
|
|
380
|
+
column,
|
|
361
381
|
rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
|
|
362
382
|
severity: isBlocking ? 'error' : 'warning',
|
|
363
383
|
source: 'security',
|
|
@@ -399,7 +419,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
399
419
|
|
|
400
420
|
// Ensure we always return an array
|
|
401
421
|
return Array.isArray(allIssues) ? allIssues : [];
|
|
402
|
-
}, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
|
|
422
|
+
}, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
|
|
403
423
|
|
|
404
424
|
/**
|
|
405
425
|
* Check if validation is clean (no errors or warnings)
|
|
@@ -31,9 +31,8 @@ export const LABEL_ISSUE_PATTERNS = [
|
|
|
31
31
|
'alt-require',
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
-
// Tab keys
|
|
34
|
+
// Tab keys: Errors = blocking, Warnings = non-blocking
|
|
35
35
|
export const ERROR_TAB_KEYS = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
LIQUID: 'liquid',
|
|
36
|
+
ERRORS: 'errors',
|
|
37
|
+
WARNINGS: 'warnings',
|
|
39
38
|
};
|
|
@@ -126,9 +126,7 @@ export class Creatives extends React.Component {
|
|
|
126
126
|
// HTML Editor validation state (for email channel)
|
|
127
127
|
htmlEditorValidationState: {
|
|
128
128
|
isContentEmpty: true,
|
|
129
|
-
issueCounts: {
|
|
130
|
-
html: 0, label: 0, liquid: 0, total: 0,
|
|
131
|
-
},
|
|
129
|
+
issueCounts: { errors: 0, warnings: 0, total: 0 },
|
|
132
130
|
validationComplete: false, // Flag to track if validation has completed
|
|
133
131
|
errorsAcknowledged: false, // Flag to track if user has acknowledged errors by clicking redirection icon
|
|
134
132
|
},
|
|
@@ -262,10 +262,9 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
|
|
|
262
262
|
"errorsAcknowledged": false,
|
|
263
263
|
"isContentEmpty": true,
|
|
264
264
|
"issueCounts": Object {
|
|
265
|
-
"
|
|
266
|
-
"label": 0,
|
|
267
|
-
"liquid": 0,
|
|
265
|
+
"errors": 0,
|
|
268
266
|
"total": 0,
|
|
267
|
+
"warnings": 0,
|
|
269
268
|
},
|
|
270
269
|
"validationComplete": false,
|
|
271
270
|
}
|
|
@@ -392,10 +391,9 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
|
|
|
392
391
|
"errorsAcknowledged": false,
|
|
393
392
|
"isContentEmpty": true,
|
|
394
393
|
"issueCounts": Object {
|
|
395
|
-
"
|
|
396
|
-
"label": 0,
|
|
397
|
-
"liquid": 0,
|
|
394
|
+
"errors": 0,
|
|
398
395
|
"total": 0,
|
|
396
|
+
"warnings": 0,
|
|
399
397
|
},
|
|
400
398
|
"validationComplete": false,
|
|
401
399
|
}
|
|
@@ -522,10 +520,9 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
|
|
|
522
520
|
"errorsAcknowledged": false,
|
|
523
521
|
"isContentEmpty": true,
|
|
524
522
|
"issueCounts": Object {
|
|
525
|
-
"
|
|
526
|
-
"label": 0,
|
|
527
|
-
"liquid": 0,
|
|
523
|
+
"errors": 0,
|
|
528
524
|
"total": 0,
|
|
525
|
+
"warnings": 0,
|
|
529
526
|
},
|
|
530
527
|
"validationComplete": false,
|
|
531
528
|
}
|
|
@@ -651,55 +651,11 @@ const EmailHTMLEditor = (props) => {
|
|
|
651
651
|
setSubjectError('');
|
|
652
652
|
}
|
|
653
653
|
|
|
654
|
-
// 1.5. Check for
|
|
655
|
-
//
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
};
|
|
660
|
-
let allIssues = [];
|
|
661
|
-
if (htmlEditorRef.current && typeof htmlEditorRef.current.getAllIssues === 'function') {
|
|
662
|
-
allIssues = htmlEditorRef.current.getAllIssues();
|
|
663
|
-
// Filter out API validation errors - they're validated separately via API call
|
|
664
|
-
const clientSideIssues = allIssues.filter((issue) => issue.rule !== 'liquid-api-validation' && issue.rule !== 'standard-api-validation');
|
|
665
|
-
|
|
666
|
-
// Count only client-side errors
|
|
667
|
-
issueCounts = {
|
|
668
|
-
html: 0,
|
|
669
|
-
label: 0,
|
|
670
|
-
liquid: 0,
|
|
671
|
-
total: clientSideIssues.length,
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
clientSideIssues.forEach((issue) => {
|
|
675
|
-
const { source, rule, message } = issue;
|
|
676
|
-
const messageLower = (message || '').toLowerCase();
|
|
677
|
-
const ruleLower = (rule || '').toLowerCase();
|
|
678
|
-
|
|
679
|
-
// Check if it's a liquid error (client-side only, not API)
|
|
680
|
-
if (source === 'liquid-validator' && rule !== 'liquid-api-validation') {
|
|
681
|
-
issueCounts.liquid++;
|
|
682
|
-
} else if (
|
|
683
|
-
messageLower.includes('tag must be paired')
|
|
684
|
-
|| messageLower.includes('open tag match failed')
|
|
685
|
-
|| messageLower.includes('closed tag match failed')
|
|
686
|
-
|| messageLower.includes('unclosed')
|
|
687
|
-
|| messageLower.includes('missing required')
|
|
688
|
-
|| ruleLower.includes('tag-pair')
|
|
689
|
-
|| ruleLower.includes('attr-value-not-empty')
|
|
690
|
-
|| ruleLower.includes('attr-no-duplication')
|
|
691
|
-
|| ruleLower.includes('tag-self-close')
|
|
692
|
-
|| ruleLower.includes('spec-char-escape')
|
|
693
|
-
|| ruleLower.includes('tagname-lowercase')
|
|
694
|
-
|| ruleLower.includes('attr-lowercase')
|
|
695
|
-
|| ruleLower.includes('src-not-empty')
|
|
696
|
-
|| ruleLower.includes('alt-require')
|
|
697
|
-
) {
|
|
698
|
-
issueCounts.label++;
|
|
699
|
-
} else {
|
|
700
|
-
issueCounts.html++;
|
|
701
|
-
}
|
|
702
|
-
});
|
|
654
|
+
// 1.5. Check for validation errors (Errors = blocking, Warnings = non-blocking)
|
|
655
|
+
// Get issue counts from ref or stored state
|
|
656
|
+
let issueCounts = { errors: 0, warnings: 0, total: 0 };
|
|
657
|
+
if (htmlEditorRef.current && typeof htmlEditorRef.current.getIssueCounts === 'function') {
|
|
658
|
+
issueCounts = htmlEditorRef.current.getIssueCounts();
|
|
703
659
|
} else if (lastValidationStateRef.current?.issueCounts) {
|
|
704
660
|
issueCounts = lastValidationStateRef.current.issueCounts;
|
|
705
661
|
}
|