@capillarytech/creatives-library 8.0.262-alpha.1 → 8.0.263
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 +58 -113
- package/v2Components/ErrorInfoNote/messages.js +8 -12
- package/v2Components/HtmlEditor/HTMLEditor.js +48 -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/EditorToolbar/_editorToolbar.scss +0 -1
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +27 -2
- 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/index.js +10 -33
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +48 -13
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +77 -146
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +14 -14
- package/v2Components/HtmlEditor/constants.js +3 -0
- package/v2Components/HtmlEditor/hooks/useValidation.js +34 -13
- package/v2Components/HtmlEditor/messages.js +10 -0
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +38 -36
- package/v2Components/HtmlEditor/utils/validationConstants.js +3 -4
- package/v2Components/MobilePushPreviewV2/constants.js +6 -0
- package/v2Components/MobilePushPreviewV2/index.js +4 -3
- 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
|
|
|
@@ -18,49 +17,29 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
|
|
|
18
17
|
|
|
19
18
|
// Messages
|
|
20
19
|
import messages from './messages';
|
|
21
|
-
import { BLOCKING_ERROR_RULE_IDS } from '../../constants';
|
|
22
|
-
import { ISSUE_SOURCES, LABEL_ISSUE_PATTERNS } from '../../utils/validationConstants';
|
|
20
|
+
import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../../constants';
|
|
23
21
|
|
|
24
22
|
// Styles
|
|
25
23
|
import './_validationTabs.scss';
|
|
26
|
-
import {StyledCapTab} from '../../../../v2Containers/MobilePushNew/style';
|
|
24
|
+
import { StyledCapTab } from '../../../../v2Containers/MobilePushNew/style';
|
|
25
|
+
import { ISSUE_SOURCES } from '../../utils/validationConstants';
|
|
27
26
|
|
|
28
27
|
/**
|
|
29
|
-
*
|
|
28
|
+
* Group issues into Errors (blocking) and Warnings (non-blocking)
|
|
30
29
|
*/
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const liquidIssues = [];
|
|
30
|
+
const groupByErrorsAndWarnings = (allIssues) => {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const warnings = [];
|
|
35
33
|
|
|
36
34
|
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;
|
|
35
|
+
if (isBlockingError(issue)) {
|
|
36
|
+
errors.push(issue);
|
|
37
|
+
} else {
|
|
38
|
+
warnings.push(issue);
|
|
57
39
|
}
|
|
58
|
-
|
|
59
|
-
// Default to HTML issues
|
|
60
|
-
htmlIssues.push(issue);
|
|
61
40
|
});
|
|
62
41
|
|
|
63
|
-
return {
|
|
42
|
+
return { errors, warnings };
|
|
64
43
|
};
|
|
65
44
|
|
|
66
45
|
/**
|
|
@@ -73,7 +52,7 @@ const isBlockingError = (issue) => {
|
|
|
73
52
|
return true;
|
|
74
53
|
}
|
|
75
54
|
// Client-side Liquid validation errors are blocking (genuine syntax errors)
|
|
76
|
-
if (source ===
|
|
55
|
+
if (source === ISSUE_SOURCES.LIQUID && severity === VALIDATION_SEVERITY.ERROR) {
|
|
77
56
|
return true;
|
|
78
57
|
}
|
|
79
58
|
// Rule Group #1 errors are blocking
|
|
@@ -83,24 +62,13 @@ const isBlockingError = (issue) => {
|
|
|
83
62
|
return false;
|
|
84
63
|
};
|
|
85
64
|
|
|
86
|
-
/**
|
|
87
|
-
* Get icon based on whether issue is blocking error or warning
|
|
88
|
-
* Blocking errors use error-icon, warnings use 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="warning" className="validation-tabs__icon validation-tabs__icon--warning" />;
|
|
96
|
-
};
|
|
97
|
-
|
|
98
65
|
/**
|
|
99
66
|
* ValidationTabContent - Renders the content for each tab
|
|
100
67
|
*/
|
|
101
68
|
const ValidationTabContent = ({
|
|
102
69
|
issues,
|
|
103
70
|
onErrorClick,
|
|
71
|
+
onExpand,
|
|
104
72
|
}) => {
|
|
105
73
|
if (!issues || issues.length === 0) {
|
|
106
74
|
return null;
|
|
@@ -108,6 +76,10 @@ const ValidationTabContent = ({
|
|
|
108
76
|
|
|
109
77
|
const handleNavigateClick = (issue, e) => {
|
|
110
78
|
e.stopPropagation();
|
|
79
|
+
// Expand panel when redirection is clicked so it does not stay stuck to footer
|
|
80
|
+
if (onExpand) {
|
|
81
|
+
onExpand();
|
|
82
|
+
}
|
|
111
83
|
if (onErrorClick) {
|
|
112
84
|
// Always call onErrorClick to acknowledge the error (enables buttons)
|
|
113
85
|
// If line number exists, navigate to it; otherwise just acknowledge and focus editor
|
|
@@ -135,9 +107,6 @@ const ValidationTabContent = ({
|
|
|
135
107
|
key={key}
|
|
136
108
|
className={`validation-tabs__item validation-tabs__item--${displaySeverity}`}
|
|
137
109
|
>
|
|
138
|
-
<div className="validation-tabs__item-icon">
|
|
139
|
-
{getSeverityIcon(issue)}
|
|
140
|
-
</div>
|
|
141
110
|
<div className="validation-tabs__item-content">
|
|
142
111
|
<span className="validation-tabs__item-message">
|
|
143
112
|
{message}
|
|
@@ -173,11 +142,13 @@ const ValidationTabContent = ({
|
|
|
173
142
|
ValidationTabContent.propTypes = {
|
|
174
143
|
issues: PropTypes.array,
|
|
175
144
|
onErrorClick: PropTypes.func,
|
|
145
|
+
onExpand: PropTypes.func,
|
|
176
146
|
};
|
|
177
147
|
|
|
178
148
|
ValidationTabContent.defaultProps = {
|
|
179
149
|
issues: [],
|
|
180
150
|
onErrorClick: null,
|
|
151
|
+
onExpand: null,
|
|
181
152
|
};
|
|
182
153
|
|
|
183
154
|
/**
|
|
@@ -187,137 +158,95 @@ const ValidationTabs = ({
|
|
|
187
158
|
intl,
|
|
188
159
|
validation,
|
|
189
160
|
onErrorClick,
|
|
190
|
-
|
|
161
|
+
isCollapsed = false,
|
|
162
|
+
onToggleCollapse,
|
|
163
|
+
onExpand,
|
|
191
164
|
className,
|
|
192
165
|
}) => {
|
|
193
|
-
const [activeKey, setActiveKey] = useState(
|
|
166
|
+
const [activeKey, setActiveKey] = useState('errors');
|
|
194
167
|
|
|
195
|
-
//
|
|
196
|
-
const {
|
|
168
|
+
// Group issues into Errors (blocking) and Warnings (non-blocking)
|
|
169
|
+
const { errors, warnings } = useMemo(() => {
|
|
197
170
|
if (!validation) {
|
|
198
|
-
return {
|
|
171
|
+
return { errors: [], warnings: [] };
|
|
199
172
|
}
|
|
200
|
-
|
|
201
|
-
// Get all issues from validation
|
|
202
173
|
const allIssues = validation.getAllIssues ? validation.getAllIssues() : [];
|
|
203
|
-
|
|
204
|
-
return categorized;
|
|
174
|
+
return groupByErrorsAndWarnings(allIssues);
|
|
205
175
|
}, [validation]);
|
|
206
176
|
|
|
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;
|
|
177
|
+
const errorsCount = errors.length;
|
|
178
|
+
const warningsCount = warnings.length;
|
|
179
|
+
const totalCount = errorsCount + warningsCount;
|
|
214
180
|
|
|
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');
|
|
181
|
+
// Default active tab: errors if any, else warnings
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
if (errorsCount > 0) {
|
|
184
|
+
setActiveKey('errors');
|
|
185
|
+
} else if (warningsCount > 0) {
|
|
186
|
+
setActiveKey('warnings');
|
|
224
187
|
}
|
|
225
|
-
}, [
|
|
188
|
+
}, [errorsCount, warningsCount]);
|
|
226
189
|
|
|
227
|
-
// Don't render if no issues
|
|
228
190
|
if (totalCount === 0) {
|
|
229
191
|
return null;
|
|
230
192
|
}
|
|
231
193
|
|
|
232
|
-
// Build tab panes (CapTab uses 'panes' with 'tab' and 'content' properties)
|
|
233
194
|
const tabPanes = [];
|
|
234
195
|
|
|
235
|
-
if (
|
|
196
|
+
if (errorsCount > 0) {
|
|
236
197
|
tabPanes.push({
|
|
237
|
-
key: '
|
|
198
|
+
key: 'errors',
|
|
238
199
|
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>
|
|
200
|
+
<CapTooltip title={`${intl.formatMessage(messages.errors)} (${errorsCount})`}>
|
|
201
|
+
<span className="validation-tabs__tab-label validation-tabs__tab-label--errors">
|
|
202
|
+
<FormattedMessage {...messages.errors} />
|
|
203
|
+
<span className="validation-tabs__tab-count">({errorsCount})</span>
|
|
247
204
|
</span>
|
|
248
205
|
</CapTooltip>
|
|
249
206
|
),
|
|
250
207
|
content: (
|
|
251
208
|
<ValidationTabContent
|
|
252
|
-
issues={
|
|
209
|
+
issues={errors}
|
|
253
210
|
onErrorClick={onErrorClick}
|
|
254
|
-
|
|
211
|
+
onExpand={onExpand}
|
|
255
212
|
/>
|
|
256
213
|
),
|
|
257
214
|
});
|
|
258
215
|
}
|
|
259
216
|
|
|
260
|
-
if (
|
|
217
|
+
if (warningsCount > 0) {
|
|
261
218
|
tabPanes.push({
|
|
262
|
-
key: '
|
|
219
|
+
key: 'warnings',
|
|
263
220
|
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>
|
|
221
|
+
<CapTooltip title={`${intl.formatMessage(messages.warnings)} (${warningsCount})`}>
|
|
222
|
+
<span className="validation-tabs__tab-label validation-tabs__tab-label--warnings">
|
|
223
|
+
<FormattedMessage {...messages.warnings} />
|
|
224
|
+
<span className="validation-tabs__tab-count">({warningsCount})</span>
|
|
272
225
|
</span>
|
|
273
226
|
</CapTooltip>
|
|
274
227
|
),
|
|
275
228
|
content: (
|
|
276
229
|
<ValidationTabContent
|
|
277
|
-
issues={
|
|
230
|
+
issues={warnings}
|
|
278
231
|
onErrorClick={onErrorClick}
|
|
279
|
-
|
|
232
|
+
onExpand={onExpand}
|
|
280
233
|
/>
|
|
281
234
|
),
|
|
282
235
|
});
|
|
283
236
|
}
|
|
284
237
|
|
|
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();
|
|
238
|
+
const handleToggleCollapse = () => {
|
|
239
|
+
if (onToggleCollapse) {
|
|
240
|
+
onToggleCollapse();
|
|
316
241
|
}
|
|
317
242
|
};
|
|
318
243
|
|
|
244
|
+
const collapseLabel = isCollapsed
|
|
245
|
+
? intl.formatMessage(messages.expandPanel)
|
|
246
|
+
: intl.formatMessage(messages.collapsePanel);
|
|
247
|
+
|
|
319
248
|
return (
|
|
320
|
-
<div className={`validation-tabs ${className || ''}`}>
|
|
249
|
+
<div className={`validation-tabs ${isCollapsed ? 'validation-tabs--collapsed' : ''} ${className || ''}`}>
|
|
321
250
|
<CapRow className="validation-tabs__header">
|
|
322
251
|
<StyledCapTab
|
|
323
252
|
className="validation-tabs__tabs"
|
|
@@ -326,14 +255,14 @@ const ValidationTabs = ({
|
|
|
326
255
|
panes={tabPanes}
|
|
327
256
|
/>
|
|
328
257
|
<CapRow className="validation-tabs__actions">
|
|
329
|
-
<CapTooltip title={
|
|
258
|
+
<CapTooltip title={collapseLabel}>
|
|
330
259
|
<button
|
|
331
260
|
type="button"
|
|
332
|
-
className="validation-
|
|
333
|
-
onClick={
|
|
334
|
-
aria-label={
|
|
261
|
+
className="validation-tabs__collapse-toggle"
|
|
262
|
+
onClick={handleToggleCollapse}
|
|
263
|
+
aria-label={collapseLabel}
|
|
335
264
|
>
|
|
336
|
-
<CapIcon type=
|
|
265
|
+
<CapIcon type={isCollapsed ? 'chevron-up' : 'chevron-down'} />
|
|
337
266
|
</button>
|
|
338
267
|
</CapTooltip>
|
|
339
268
|
</CapRow>
|
|
@@ -348,16 +277,18 @@ ValidationTabs.propTypes = {
|
|
|
348
277
|
getAllIssues: PropTypes.func,
|
|
349
278
|
}),
|
|
350
279
|
onErrorClick: PropTypes.func,
|
|
351
|
-
|
|
352
|
-
|
|
280
|
+
isCollapsed: PropTypes.bool,
|
|
281
|
+
onToggleCollapse: PropTypes.func,
|
|
282
|
+
onExpand: PropTypes.func,
|
|
353
283
|
className: PropTypes.string,
|
|
354
284
|
};
|
|
355
285
|
|
|
356
286
|
ValidationTabs.defaultProps = {
|
|
357
287
|
validation: null,
|
|
358
288
|
onErrorClick: null,
|
|
359
|
-
|
|
360
|
-
|
|
289
|
+
isCollapsed: false,
|
|
290
|
+
onToggleCollapse: null,
|
|
291
|
+
onExpand: null,
|
|
361
292
|
className: '',
|
|
362
293
|
};
|
|
363
294
|
|
|
@@ -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
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
* Centralized constants for the HTML Editor component
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
// Documentation URL for Liquid / HTML editor
|
|
8
|
+
export const LIQUID_DOC_URL = 'https://docs.capillarytech.com/docs/liquid-language-in-messages';
|
|
9
|
+
|
|
7
10
|
// HTML Editor Variants
|
|
8
11
|
export const HTML_EDITOR_VARIANTS = {
|
|
9
12
|
EMAIL: 'email',
|
|
@@ -10,7 +10,8 @@ import {
|
|
|
10
10
|
} from 'react';
|
|
11
11
|
import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
|
|
12
12
|
import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
|
|
13
|
-
import { BLOCKING_ERROR_RULE_IDS } from '../constants';
|
|
13
|
+
import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../constants';
|
|
14
|
+
import { ISSUE_SOURCES } from '../utils/validationConstants';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Custom hook for managing HTML/CSS validation
|
|
@@ -29,6 +30,21 @@ const getLineNumberFromPosition = (text, position) => {
|
|
|
29
30
|
return text.substring(0, position).split('\n').length;
|
|
30
31
|
};
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Get 1-based line and column from a character position in text
|
|
35
|
+
*/
|
|
36
|
+
const getLineAndColumnFromPosition = (text, position) => {
|
|
37
|
+
if (!text || position === undefined || position < 0) {
|
|
38
|
+
return { line: 1, column: 1 };
|
|
39
|
+
}
|
|
40
|
+
const before = text.substring(0, position);
|
|
41
|
+
const lines = before.split('\n');
|
|
42
|
+
const line = lines.length;
|
|
43
|
+
const lastLine = lines[lines.length - 1] || '';
|
|
44
|
+
const column = lastLine.length + 1;
|
|
45
|
+
return { line, column };
|
|
46
|
+
};
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
* Find line number for a tag or pattern in content
|
|
34
50
|
* Used for API errors to locate where the error occurs
|
|
@@ -326,47 +342,52 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
326
342
|
const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
|
|
327
343
|
const extractedLine = extractLineNumberFromMessage(errorMessage);
|
|
328
344
|
return {
|
|
329
|
-
type:
|
|
345
|
+
type: VALIDATION_SEVERITY.ERROR,
|
|
330
346
|
message: errorMessage,
|
|
331
347
|
line: extractedLine,
|
|
332
348
|
column: null,
|
|
333
349
|
rule: 'liquid-api-validation',
|
|
334
|
-
severity:
|
|
335
|
-
source:
|
|
350
|
+
severity: VALIDATION_SEVERITY.ERROR,
|
|
351
|
+
source: ISSUE_SOURCES.LIQUID,
|
|
336
352
|
};
|
|
337
353
|
});
|
|
338
354
|
|
|
339
355
|
const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
|
|
340
356
|
const extractedLine = extractLineNumberFromMessage(errorMessage);
|
|
341
357
|
return {
|
|
342
|
-
type:
|
|
358
|
+
type: VALIDATION_SEVERITY.ERROR,
|
|
343
359
|
message: errorMessage,
|
|
344
360
|
line: extractedLine,
|
|
345
361
|
column: null,
|
|
346
362
|
rule: 'standard-api-validation',
|
|
347
|
-
severity:
|
|
363
|
+
severity: VALIDATION_SEVERITY.ERROR,
|
|
348
364
|
source: 'api-validator',
|
|
349
365
|
};
|
|
350
366
|
});
|
|
351
367
|
|
|
352
368
|
// Security: protocol types are Rule Group #1 (blocking); others are warnings
|
|
369
|
+
// Use issue.position (from findUnsafeContent) to show real line/column when available
|
|
353
370
|
const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
|
|
371
|
+
const contentStr = typeof content === 'string' ? content : '';
|
|
354
372
|
const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
|
|
355
373
|
const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
|
|
374
|
+
const { line, column } = (issue?.position !== undefined && contentStr)
|
|
375
|
+
? getLineAndColumnFromPosition(contentStr, issue.position)
|
|
376
|
+
: { line: 1, column: 1 };
|
|
356
377
|
return {
|
|
357
|
-
type: isBlocking ?
|
|
378
|
+
type: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
|
|
358
379
|
message: `Security issue: ${issue.type}`,
|
|
359
|
-
line
|
|
360
|
-
column
|
|
380
|
+
line,
|
|
381
|
+
column,
|
|
361
382
|
rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
|
|
362
|
-
severity: isBlocking ?
|
|
383
|
+
severity: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
|
|
363
384
|
source: 'security',
|
|
364
385
|
};
|
|
365
386
|
});
|
|
366
387
|
|
|
367
388
|
// Sanitization warnings (Rule Group #1 entries have rule set by contentSanitizer)
|
|
368
389
|
const sanitizationAsIssues = (validationState.sanitizationWarnings || []).map((w) => {
|
|
369
|
-
const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ?
|
|
390
|
+
const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING;
|
|
370
391
|
return {
|
|
371
392
|
...w,
|
|
372
393
|
severity: sev,
|
|
@@ -399,7 +420,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
399
420
|
|
|
400
421
|
// Ensure we always return an array
|
|
401
422
|
return Array.isArray(allIssues) ? allIssues : [];
|
|
402
|
-
}, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
|
|
423
|
+
}, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
|
|
403
424
|
|
|
404
425
|
/**
|
|
405
426
|
* Check if validation is clean (no errors or warnings)
|
|
@@ -431,7 +452,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
431
452
|
|
|
432
453
|
const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
|
|
433
454
|
// Client-side Liquid validation errors are blocking (genuine syntax errors)
|
|
434
|
-
const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source ===
|
|
455
|
+
const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
|
|
435
456
|
const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
|
|
436
457
|
|
|
437
458
|
return {
|
|
@@ -328,6 +328,16 @@ export default defineMessages({
|
|
|
328
328
|
},
|
|
329
329
|
},
|
|
330
330
|
|
|
331
|
+
htmlEditorTooltip: {
|
|
332
|
+
id: `${scope}.htmlEditorTooltip`,
|
|
333
|
+
defaultMessage: 'This editor supports standard HTML for emails. Avoid CSS frameworks, external stylesheets, and scripts. {docLink}',
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
viewDocumentation: {
|
|
337
|
+
id: `${scope}.viewDocumentation`,
|
|
338
|
+
defaultMessage: 'View documentation',
|
|
339
|
+
},
|
|
340
|
+
|
|
331
341
|
// HTML Validator messages
|
|
332
342
|
validator: {
|
|
333
343
|
// General validation messages
|