@capillarytech/creatives-library 8.0.260 → 8.0.262-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 (147) 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 +34 -0
  8. package/tests/integration/TemplateCreation/TemplateCreation.integration.test.js +17 -35
  9. package/tests/integration/TemplateCreation/api-response.js +31 -1
  10. package/tests/integration/TemplateCreation/msw-handler.js +2 -0
  11. package/utils/common.js +5 -0
  12. package/utils/commonUtils.js +28 -5
  13. package/utils/tests/commonUtil.test.js +224 -0
  14. package/utils/transformTemplateConfig.js +0 -10
  15. package/v2Components/CapDeviceContent/index.js +61 -56
  16. package/v2Components/CapTagList/index.js +6 -1
  17. package/v2Components/CapTagListWithInput/index.js +5 -1
  18. package/v2Components/CapTagListWithInput/messages.js +1 -1
  19. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  20. package/v2Components/ErrorInfoNote/constants.js +1 -0
  21. package/v2Components/ErrorInfoNote/index.js +457 -72
  22. package/v2Components/ErrorInfoNote/messages.js +36 -6
  23. package/v2Components/ErrorInfoNote/style.scss +282 -6
  24. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  25. package/v2Components/HtmlEditor/HTMLEditor.js +547 -94
  26. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
  27. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1883 -133
  28. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  29. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  30. package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
  31. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
  32. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
  33. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  34. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  35. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
  36. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +4 -4
  37. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  38. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  39. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  40. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  41. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  42. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  43. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
  44. package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
  45. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  46. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +1 -0
  47. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
  48. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +50 -34
  49. package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
  50. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +70 -41
  51. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
  52. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +364 -0
  53. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  54. package/v2Components/HtmlEditor/constants.js +42 -20
  55. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  56. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
  57. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  58. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  59. package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
  60. package/v2Components/HtmlEditor/index.js +1 -1
  61. package/v2Components/HtmlEditor/messages.js +92 -94
  62. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
  63. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
  64. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  65. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  66. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
  67. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  69. package/v2Components/HtmlEditor/utils/validationConstants.js +39 -0
  70. package/v2Components/MobilePushPreviewV2/index.js +32 -7
  71. package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
  72. package/v2Components/TemplatePreview/index.js +47 -32
  73. package/v2Components/TemplatePreview/messages.js +4 -0
  74. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  75. package/v2Containers/BeeEditor/index.js +172 -90
  76. package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
  77. package/v2Containers/BeePopupEditor/constants.js +10 -0
  78. package/v2Containers/BeePopupEditor/index.js +194 -0
  79. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  80. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  81. package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
  82. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  83. package/v2Containers/CreativesContainer/constants.js +1 -0
  84. package/v2Containers/CreativesContainer/index.js +242 -46
  85. package/v2Containers/CreativesContainer/messages.js +8 -0
  86. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  87. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  88. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
  89. package/v2Containers/Email/actions.js +7 -0
  90. package/v2Containers/Email/constants.js +5 -1
  91. package/v2Containers/Email/index.js +234 -29
  92. package/v2Containers/Email/messages.js +32 -0
  93. package/v2Containers/Email/reducer.js +12 -1
  94. package/v2Containers/Email/sagas.js +61 -7
  95. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  96. package/v2Containers/Email/tests/reducer.test.js +46 -0
  97. package/v2Containers/Email/tests/sagas.test.js +320 -29
  98. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1290 -0
  99. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +211 -21
  100. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  101. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2487 -0
  102. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  103. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  104. package/v2Containers/EmailWrapper/constants.js +2 -0
  105. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
  106. package/v2Containers/EmailWrapper/index.js +103 -23
  107. package/v2Containers/EmailWrapper/messages.js +65 -1
  108. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +956 -0
  109. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
  110. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  111. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  112. package/v2Containers/InApp/actions.js +7 -0
  113. package/v2Containers/InApp/constants.js +20 -4
  114. package/v2Containers/InApp/index.js +802 -359
  115. package/v2Containers/InApp/index.scss +4 -3
  116. package/v2Containers/InApp/messages.js +7 -3
  117. package/v2Containers/InApp/reducer.js +21 -3
  118. package/v2Containers/InApp/sagas.js +29 -9
  119. package/v2Containers/InApp/selectors.js +25 -5
  120. package/v2Containers/InApp/tests/index.test.js +154 -50
  121. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  122. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  123. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  124. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
  125. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  126. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
  127. package/v2Containers/InAppWrapper/constants.js +16 -0
  128. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  129. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  130. package/v2Containers/InAppWrapper/index.js +148 -0
  131. package/v2Containers/InAppWrapper/messages.js +49 -0
  132. package/v2Containers/InappAdvance/index.js +1099 -0
  133. package/v2Containers/InappAdvance/index.scss +10 -0
  134. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  135. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  136. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  137. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  138. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  139. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  140. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  141. package/v2Containers/TagList/index.js +62 -19
  142. package/v2Containers/Templates/_templates.scss +60 -1
  143. package/v2Containers/Templates/index.js +89 -4
  144. package/v2Containers/Templates/messages.js +4 -0
  145. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  146. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  147. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -1,11 +1,16 @@
1
1
  /**
2
2
  * Validation Hook for HTML Editor
3
- * Manages real-time validation and error display
3
+ * Manages real-time validation and error display.
4
+ * UI gating: only Rule Group #1 (Input & Sanitization) blocks Save/Update/Preview/Test.
5
+ * All other rules are warnings for backward compatibility with CKEditor legacy templates.
4
6
  */
5
7
 
6
- import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import {
9
+ useState, useEffect, useCallback, useRef,
10
+ } from 'react';
7
11
  import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
8
12
  import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
13
+ import { BLOCKING_ERROR_RULE_IDS } from '../constants';
9
14
 
10
15
  /**
11
16
  * Custom hook for managing HTML/CSS validation
@@ -16,12 +21,46 @@ import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/content
16
21
  * @param {Function} formatValidatorMessage - Message formatter function for validator internationalization
17
22
  * @returns {Object} Validation state and methods
18
23
  */
24
+ /**
25
+ * Get line number for a character position in text
26
+ */
27
+ const getLineNumberFromPosition = (text, position) => {
28
+ if (position === undefined || position < 0) return 1;
29
+ return text.substring(0, position).split('\n').length;
30
+ };
31
+
32
+ /**
33
+ * Find line number for a tag or pattern in content
34
+ * Used for API errors to locate where the error occurs
35
+ */
36
+ const findLineNumberForTag = (content, tagName) => {
37
+ if (!content || !tagName) return null;
38
+
39
+ // Try to find the tag in the content
40
+ // Look for patterns like {{ tagName }}, {{tagName}}, etc.
41
+ const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
42
+ const patterns = [
43
+ new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g'),
44
+ new RegExp(`\\{%[^%]*${escapedTagName}[^%]*%\\}`, 'g'),
45
+ ];
46
+
47
+ for (const pattern of patterns) {
48
+ const match = pattern.exec(content);
49
+ if (match) {
50
+ return getLineNumberFromPosition(content, match.index);
51
+ }
52
+ }
53
+
54
+ return null;
55
+ };
56
+
19
57
  export const useValidation = (content, variant = 'email', options = {}, formatSanitizerMessage = null, formatValidatorMessage = null) => {
20
58
  const {
21
59
  enableRealTime = true,
22
60
  debounceMs = 500,
23
61
  enableSanitization = true,
24
- securityLevel = 'standard'
62
+ securityLevel = 'standard',
63
+ apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
25
64
  } = options;
26
65
 
27
66
  // Validation state
@@ -42,8 +81,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
42
81
  totalErrors: 0,
43
82
  totalWarnings: 0,
44
83
  totalInfo: 0,
45
- hasSecurityIssues: false
46
- }
84
+ hasSecurityIssues: false,
85
+ },
47
86
  });
48
87
 
49
88
  // Refs for debouncing
@@ -55,7 +94,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
55
94
  */
56
95
  const performValidation = useCallback(async (htmlContent) => {
57
96
  if (!htmlContent) {
58
- setValidationState(prev => ({
97
+ setValidationState((prev) => ({
59
98
  ...prev,
60
99
  isValidating: false,
61
100
  htmlErrors: [],
@@ -72,13 +111,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
72
111
  totalErrors: 0,
73
112
  totalWarnings: 0,
74
113
  totalInfo: 0,
75
- hasSecurityIssues: false
76
- }
114
+ hasSecurityIssues: false,
115
+ },
77
116
  }));
78
117
  return;
79
118
  }
80
119
 
81
- setValidationState(prev => ({ ...prev, isValidating: true }));
120
+ setValidationState((prev) => ({ ...prev, isValidating: true }));
82
121
 
83
122
  try {
84
123
  // 1. HTML Validation
@@ -91,15 +130,21 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
91
130
  const isSecure = isContentSafe(htmlContent);
92
131
  const securityIssues = isSecure ? [] : findUnsafeContent(htmlContent);
93
132
 
94
- // 4. Sanitization (if enabled)
133
+ // 4. Sanitization (if enabled) – Rule Group #1 issues come from here
95
134
  let sanitizationResult = null;
96
135
  if (enableSanitization) {
97
136
  sanitizationResult = sanitizeHTML(htmlContent, variant, securityLevel, formatSanitizerMessage);
98
137
  }
99
138
 
100
- // Calculate summary
101
- const totalErrors = htmlValidation.errors.length + cssValidation.errors.length;
102
- const totalWarnings = htmlValidation.warnings.length + cssValidation.warnings.length;
139
+ const sanitizationWarnings = sanitizationResult?.warnings || [];
140
+ const blockingSanitizerCount = sanitizationWarnings.filter((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)).length;
141
+ const protocolSecurityCount = (securityIssues || []).filter((s) => ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'].includes(s?.type)).length;
142
+
143
+ // Summary: totalErrors/totalWarnings are for display; blocking count is for gating
144
+ const totalErrors = htmlValidation.errors.length + cssValidation.errors.length + blockingSanitizerCount + protocolSecurityCount;
145
+ const totalWarnings = htmlValidation.warnings.length + cssValidation.warnings.length
146
+ + (sanitizationWarnings.length - blockingSanitizerCount)
147
+ + (securityIssues.length - protocolSecurityCount);
103
148
  const totalInfo = htmlValidation.info.length + cssValidation.info.length;
104
149
  const hasSecurityIssues = securityIssues.length > 0;
105
150
 
@@ -114,20 +159,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
114
159
  cssWarnings: cssValidation.warnings,
115
160
  cssInfo: cssValidation.info,
116
161
  securityIssues,
117
- sanitizationWarnings: sanitizationResult?.warnings || [],
162
+ sanitizationWarnings,
118
163
  isValid: htmlValidation.isValid && cssValidation.isValid,
119
164
  isSecure,
120
165
  summary: {
121
166
  totalErrors,
122
167
  totalWarnings,
123
168
  totalInfo,
124
- hasSecurityIssues
125
- }
169
+ hasSecurityIssues,
170
+ },
126
171
  });
127
-
128
172
  } catch (error) {
129
173
  console.error('Validation error:', error);
130
- setValidationState(prev => ({
174
+ setValidationState((prev) => ({
131
175
  ...prev,
132
176
  isValidating: false,
133
177
  htmlErrors: [{
@@ -137,13 +181,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
137
181
  column: 1,
138
182
  rule: 'validation-error',
139
183
  severity: 'error',
140
- source: 'validator'
184
+ source: 'validator',
141
185
  }],
142
186
  isValid: false,
143
187
  summary: {
144
188
  ...prev.summary,
145
- totalErrors: 1
146
- }
189
+ totalErrors: 1,
190
+ },
147
191
  }));
148
192
  }
149
193
  }, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
@@ -208,8 +252,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
208
252
  totalErrors: 0,
209
253
  totalWarnings: 0,
210
254
  totalInfo: 0,
211
- hasSecurityIssues: false
212
- }
255
+ hasSecurityIssues: false,
256
+ },
213
257
  });
214
258
  }, []);
215
259
 
@@ -223,10 +267,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
223
267
  ...validationState.htmlInfo,
224
268
  ...validationState.cssErrors,
225
269
  ...validationState.cssWarnings,
226
- ...validationState.cssInfo
270
+ ...validationState.cssInfo,
227
271
  ];
228
272
 
229
- return allErrors.filter(error => error.severity === severity);
273
+ return allErrors.filter((error) => error.severity === severity);
230
274
  }, [validationState]);
231
275
 
232
276
  /**
@@ -239,32 +283,111 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
239
283
  ...validationState.htmlInfo,
240
284
  ...validationState.cssErrors,
241
285
  ...validationState.cssWarnings,
242
- ...validationState.cssInfo
286
+ ...validationState.cssInfo,
243
287
  ];
244
288
 
245
- return allErrors.filter(error => error.source === source);
289
+ return allErrors.filter((error) => error.source === source);
246
290
  }, [validationState]);
247
291
 
292
+ /**
293
+ * Extract line number from error message if present
294
+ * API errors might contain line numbers in messages like "Error at line 5" or "Line 10: error"
295
+ * Also tries to find line number by searching for the problematic tag in content
296
+ */
297
+ const extractLineNumberFromMessage = useCallback((message) => {
298
+ if (!message || typeof message !== 'string') {
299
+ return null;
300
+ }
301
+ // Try to match patterns like "line 5", "Line 10", "at line 15", "line: 20", etc.
302
+ const lineMatch = message.match(/\b(?:line|Line|LINE)\s*:?\s*(\d+)\b/i);
303
+ if (lineMatch && lineMatch[1]) {
304
+ return parseInt(lineMatch[1], 10);
305
+ }
306
+
307
+ // Try to extract tag name from error message (e.g., "Unsupported tags: test" -> "test")
308
+ const tagMatch = message.match(/(?:tag|tags|Tag|Tags)[\s:]+([a-zA-Z_][a-zA-Z0-9_.]*)/i);
309
+ if (tagMatch && tagMatch[1] && content) {
310
+ const tagName = tagMatch[1];
311
+ const lineNumber = findLineNumberForTag(content, tagName);
312
+ if (lineNumber) {
313
+ return lineNumber;
314
+ }
315
+ }
316
+
317
+ return null;
318
+ }, [content]);
319
+
248
320
  /**
249
321
  * Get all errors and warnings combined
322
+ * Includes both client-side validation errors and API validation errors
250
323
  */
251
324
  const getAllIssues = useCallback(() => {
252
- return [
253
- ...validationState.htmlErrors,
254
- ...validationState.htmlWarnings,
255
- ...validationState.htmlInfo,
256
- ...validationState.cssErrors,
257
- ...validationState.cssWarnings,
258
- ...validationState.cssInfo,
259
- ...validationState.securityIssues.map(issue => ({
325
+ // API errors (liquid + standard) are blocking – they block Save/Update/Preview/Test
326
+ const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
327
+ const extractedLine = extractLineNumberFromMessage(errorMessage);
328
+ return {
260
329
  type: 'error',
330
+ message: errorMessage,
331
+ line: extractedLine,
332
+ column: null,
333
+ rule: 'liquid-api-validation',
334
+ severity: 'error',
335
+ source: 'liquid-validator',
336
+ };
337
+ });
338
+
339
+ const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
340
+ const extractedLine = extractLineNumberFromMessage(errorMessage);
341
+ return {
342
+ type: 'error',
343
+ message: errorMessage,
344
+ line: extractedLine,
345
+ column: null,
346
+ rule: 'standard-api-validation',
347
+ severity: 'error',
348
+ source: 'api-validator',
349
+ };
350
+ });
351
+
352
+ // Security: protocol types are Rule Group #1 (blocking); others are warnings
353
+ const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
354
+ const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
355
+ const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
356
+ return {
357
+ type: isBlocking ? 'error' : 'warning',
261
358
  message: `Security issue: ${issue.type}`,
262
359
  line: 1,
263
360
  column: 1,
264
- rule: 'security-violation',
265
- severity: 'error',
266
- source: 'security'
267
- }))
361
+ rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
362
+ severity: isBlocking ? 'error' : 'warning',
363
+ source: 'security',
364
+ };
365
+ });
366
+
367
+ // Sanitization warnings (Rule Group #1 entries have rule set by contentSanitizer)
368
+ const sanitizationAsIssues = (validationState.sanitizationWarnings || []).map((w) => {
369
+ const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? 'error' : 'warning';
370
+ return {
371
+ ...w,
372
+ severity: sev,
373
+ rule: w.rule || 'sanitizer.unknown',
374
+ line: w.line ?? 1,
375
+ column: w.column ?? 1,
376
+ source: w.source || 'sanitizer',
377
+ };
378
+ });
379
+
380
+ const allIssues = [
381
+ ...(validationState.htmlErrors || []),
382
+ ...(validationState.htmlWarnings || []),
383
+ ...(validationState.htmlInfo || []),
384
+ ...(validationState.cssErrors || []),
385
+ ...(validationState.cssWarnings || []),
386
+ ...(validationState.cssInfo || []),
387
+ ...securityAsIssues,
388
+ ...sanitizationAsIssues,
389
+ ...apiLiquidErrors,
390
+ ...apiStandardErrors,
268
391
  ].sort((a, b) => {
269
392
  // Sort by severity (error > warning > info) then by line number
270
393
  const severityOrder = { error: 0, warning: 1, info: 2 };
@@ -273,16 +396,22 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
273
396
  }
274
397
  return (a.line || 0) - (b.line || 0);
275
398
  });
276
- }, [validationState]);
399
+
400
+ // Ensure we always return an array
401
+ return Array.isArray(allIssues) ? allIssues : [];
402
+ }, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
277
403
 
278
404
  /**
279
405
  * Check if validation is clean (no errors or warnings)
406
+ * Includes API validation errors in the check
280
407
  */
281
408
  const isClean = useCallback(() => {
282
- return validationState.summary.totalErrors === 0 &&
283
- validationState.summary.totalWarnings === 0 &&
284
- !validationState.summary.hasSecurityIssues;
285
- }, [validationState.summary]);
409
+ const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
410
+ return validationState.summary.totalErrors === 0
411
+ && validationState.summary.totalWarnings === 0
412
+ && !validationState.summary.hasSecurityIssues
413
+ && !hasApiErrors;
414
+ }, [validationState.summary, apiValidationErrors]);
286
415
 
287
416
  // Effect to validate content when it changes
288
417
  useEffect(() => {
@@ -292,14 +421,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
292
421
  }, [content, validateContent, enableRealTime]);
293
422
 
294
423
  // Cleanup on unmount
295
- useEffect(() => {
296
- return () => {
297
- if (debounceRef.current) {
298
- clearTimeout(debounceRef.current);
299
- }
300
- };
424
+ useEffect(() => () => {
425
+ if (debounceRef.current) {
426
+ clearTimeout(debounceRef.current);
427
+ }
301
428
  }, []);
302
429
 
430
+ const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
431
+
432
+ const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
433
+ // Client-side Liquid validation errors are blocking (genuine syntax errors)
434
+ const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === 'liquid-validator' && e.severity === 'error');
435
+ const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
436
+
303
437
  return {
304
438
  // Validation state
305
439
  ...validationState,
@@ -316,10 +450,12 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
316
450
  isClean,
317
451
 
318
452
  // Computed properties
319
- hasErrors: validationState.summary.totalErrors > 0,
453
+ hasErrors: validationState.summary.totalErrors > 0 || hasApiErrors,
320
454
  hasWarnings: validationState.summary.totalWarnings > 0,
321
- hasSecurityIssues: validationState.summary.hasSecurityIssues
455
+ hasSecurityIssues: validationState.summary.hasSecurityIssues,
456
+ /** True only when Rule Group #1 (Input & Sanitization) issues exist. Use for UI gating. */
457
+ hasBlockingErrors,
322
458
  };
323
459
  };
324
460
 
325
- export default useValidation;
461
+ export default useValidation;
@@ -26,4 +26,4 @@ export { HTML_EDITOR_VARIANTS, DEVICE_TYPES } from './constants';
26
26
  * - Default export (lazy): ~0KB initial bundle (loads ~400KB when needed)
27
27
  * - HTMLEditorSync: ~400KB added to initial bundle
28
28
  * - Constants: ~1KB added to initial bundle
29
- */
29
+ */