@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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/v2Components/ErrorInfoNote/index.js +58 -113
  3. package/v2Components/ErrorInfoNote/messages.js +8 -12
  4. package/v2Components/HtmlEditor/HTMLEditor.js +48 -182
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +11 -15
  6. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +7 -8
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -6
  8. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +1 -1
  9. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +0 -1
  10. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +27 -2
  11. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +4 -4
  12. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +17 -0
  13. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +15 -28
  14. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +10 -33
  15. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +48 -13
  16. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +77 -146
  17. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +14 -14
  18. package/v2Components/HtmlEditor/constants.js +3 -0
  19. package/v2Components/HtmlEditor/hooks/useValidation.js +34 -13
  20. package/v2Components/HtmlEditor/messages.js +10 -0
  21. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +38 -36
  22. package/v2Components/HtmlEditor/utils/validationConstants.js +3 -4
  23. package/v2Components/MobilePushPreviewV2/constants.js +6 -0
  24. package/v2Components/MobilePushPreviewV2/index.js +4 -3
  25. package/v2Containers/CreativesContainer/index.js +1 -3
  26. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +6 -9
  27. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +5 -49
  28. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +23 -38
  29. package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
@@ -7,6 +7,8 @@ import { html } from '@codemirror/lang-html';
7
7
  import { syntaxHighlighting, HighlightStyle } from '@codemirror/language';
8
8
  import { tags } from '@lezer/highlight';
9
9
  import { EditorView } from '@codemirror/view';
10
+ import { VALIDATION_SEVERITY } from '../constants';
11
+ import { ISSUE_SOURCES } from './validationConstants';
10
12
 
11
13
  /**
12
14
  * Liquid Template Syntax Patterns
@@ -145,13 +147,13 @@ export class LiquidValidator {
145
147
  } else {
146
148
  // Stray closing brace - no matching opening brace
147
149
  this.errors.push({
148
- type: 'error',
150
+ type: VALIDATION_SEVERITY.ERROR,
149
151
  message: 'Stray closing }} without matching opening {{',
150
152
  line: this.getLineNumber(html, position),
151
153
  column: 1,
152
154
  rule: 'liquid-stray-closing-output',
153
- severity: 'error',
154
- source: 'liquid-validator',
155
+ severity: VALIDATION_SEVERITY.ERROR,
156
+ source: ISSUE_SOURCES.LIQUID,
155
157
  });
156
158
  }
157
159
  }
@@ -162,13 +164,13 @@ export class LiquidValidator {
162
164
  // Report each unclosed opening brace
163
165
  stack.forEach((position) => {
164
166
  this.errors.push({
165
- type: 'error',
167
+ type: VALIDATION_SEVERITY.ERROR,
166
168
  message: 'unclosed Liquid output tag - missing }}',
167
169
  line: this.getLineNumber(html, position),
168
170
  column: 1,
169
171
  rule: 'liquid-unclosed-output',
170
- severity: 'error',
171
- source: 'liquid-validator',
172
+ severity: VALIDATION_SEVERITY.ERROR,
173
+ source: ISSUE_SOURCES.LIQUID,
172
174
  });
173
175
  });
174
176
  }
@@ -197,13 +199,13 @@ export class LiquidValidator {
197
199
  if (openTags.length > closeTags.length) {
198
200
  const unmatchedCount = openTags.length - closeTags.length;
199
201
  this.errors.push({
200
- type: 'error',
202
+ type: VALIDATION_SEVERITY.ERROR,
201
203
  message: `${unmatchedCount} unclosed Liquid logic tag(s) - missing %}`,
202
204
  line: this.getLineNumber(html, openTags[openTags.length - 1]),
203
205
  column: 1,
204
206
  rule: 'liquid-unclosed-logic',
205
- severity: 'error',
206
- source: 'liquid-validator',
207
+ severity: VALIDATION_SEVERITY.ERROR,
208
+ source: ISSUE_SOURCES.LIQUID,
207
209
  });
208
210
  }
209
211
  }
@@ -218,13 +220,13 @@ export class LiquidValidator {
218
220
  if (nestedOutput) {
219
221
  nestedOutput.forEach((match) => {
220
222
  this.errors.push({
221
- type: 'error',
223
+ type: VALIDATION_SEVERITY.ERROR,
222
224
  message: `Nested braces in Liquid output tag: ${match}`,
223
225
  line: this.getLineNumber(html, html.indexOf(match)),
224
226
  column: 1,
225
227
  rule: 'liquid-nested-braces',
226
- severity: 'error',
227
- source: 'liquid-validator',
228
+ severity: VALIDATION_SEVERITY.ERROR,
229
+ source: ISSUE_SOURCES.LIQUID,
228
230
  });
229
231
  });
230
232
  }
@@ -235,13 +237,13 @@ export class LiquidValidator {
235
237
  if (nestedLogic) {
236
238
  nestedLogic.forEach((match) => {
237
239
  this.errors.push({
238
- type: 'error',
240
+ type: VALIDATION_SEVERITY.ERROR,
239
241
  message: `Nested braces in Liquid logic tag: ${match}`,
240
242
  line: this.getLineNumber(html, html.indexOf(match)),
241
243
  column: 1,
242
244
  rule: 'liquid-nested-braces',
243
- severity: 'error',
244
- source: 'liquid-validator',
245
+ severity: VALIDATION_SEVERITY.ERROR,
246
+ source: ISSUE_SOURCES.LIQUID,
245
247
  });
246
248
  });
247
249
  }
@@ -302,23 +304,23 @@ export class LiquidValidator {
302
304
 
303
305
  if (!lastOpening) {
304
306
  this.errors.push({
305
- type: 'error',
307
+ type: VALIDATION_SEVERITY.ERROR,
306
308
  message: `Unexpected closing tag: {% ${keyword} %}`,
307
309
  line: tag.line,
308
310
  column: 1,
309
311
  rule: 'liquid-unexpected-closing',
310
- severity: 'error',
311
- source: 'liquid-validator',
312
+ severity: VALIDATION_SEVERITY.ERROR,
313
+ source: ISSUE_SOURCES.LIQUID,
312
314
  });
313
315
  } else if (lastOpening.keyword !== expectedOpening) {
314
316
  this.errors.push({
315
- type: 'error',
317
+ type: VALIDATION_SEVERITY.ERROR,
316
318
  message: `Mismatched Liquid tags: {% ${lastOpening.keyword} %} ... {% ${keyword} %}`,
317
319
  line: tag.line,
318
320
  column: 1,
319
321
  rule: 'liquid-mismatched-tags',
320
- severity: 'error',
321
- source: 'liquid-validator',
322
+ severity: VALIDATION_SEVERITY.ERROR,
323
+ source: ISSUE_SOURCES.LIQUID,
322
324
  });
323
325
  }
324
326
  }
@@ -329,13 +331,13 @@ export class LiquidValidator {
329
331
  stack.forEach(({ keyword, tag }) => {
330
332
  const expectedClosing = pairs[keyword];
331
333
  const error = {
332
- type: 'error',
334
+ type: VALIDATION_SEVERITY.ERROR,
333
335
  message: `Unclosed Liquid tag: {% ${keyword} %} - missing {% ${expectedClosing} %}`,
334
336
  line: tag.line,
335
337
  column: 1,
336
338
  rule: 'liquid-unclosed-tag',
337
- severity: 'error',
338
- source: 'liquid-validator',
339
+ severity: VALIDATION_SEVERITY.ERROR,
340
+ source: ISSUE_SOURCES.LIQUID,
339
341
  };
340
342
  this.errors.push(error);
341
343
  });
@@ -344,13 +346,13 @@ export class LiquidValidator {
344
346
  // Check for unclosed opening tags
345
347
  stack.forEach((unclosed) => {
346
348
  this.errors.push({
347
- type: 'error',
349
+ type: VALIDATION_SEVERITY.ERROR,
348
350
  message: `Unclosed Liquid tag: {% ${unclosed.keyword} %}`,
349
351
  line: unclosed.tag.line,
350
352
  column: 1,
351
353
  rule: 'liquid-unclosed-tag',
352
- severity: 'error',
353
- source: 'liquid-validator',
354
+ severity: VALIDATION_SEVERITY.ERROR,
355
+ source: ISSUE_SOURCES.LIQUID,
354
356
  });
355
357
  });
356
358
  }
@@ -364,13 +366,13 @@ export class LiquidValidator {
364
366
  if (malformedFilters) {
365
367
  malformedFilters.forEach((match) => {
366
368
  this.warnings.push({
367
- type: 'warning',
369
+ type: VALIDATION_SEVERITY.WARNING,
368
370
  message: `Malformed Liquid filter: ${match.trim()}`,
369
371
  line: this.getLineNumber(html, html.indexOf(match)),
370
372
  column: 1,
371
373
  rule: 'liquid-malformed-filter',
372
- severity: 'warning',
373
- source: 'liquid-validator',
374
+ severity: VALIDATION_SEVERITY.WARNING,
375
+ source: ISSUE_SOURCES.LIQUID,
374
376
  });
375
377
  });
376
378
  }
@@ -401,13 +403,13 @@ export class LiquidValidator {
401
403
  // Only show info for truly custom/unknown filters
402
404
  // Standard Liquid filters are now included in commonFilters list
403
405
  this.info.push({
404
- type: 'info',
406
+ type: VALIDATION_SEVERITY.INFO,
405
407
  message: `Using filter: ${filterName}`,
406
408
  line: this.getLineNumber(html, filterMatch.index),
407
409
  column: 1,
408
410
  rule: 'liquid-filter-usage',
409
- severity: 'info',
410
- source: 'liquid-validator',
411
+ severity: VALIDATION_SEVERITY.INFO,
412
+ source: ISSUE_SOURCES.LIQUID,
411
413
  });
412
414
  }
413
415
  }
@@ -422,13 +424,13 @@ export class LiquidValidator {
422
424
  if (suspiciousVariables) {
423
425
  suspiciousVariables.forEach((match) => {
424
426
  this.warnings.push({
425
- type: 'warning',
427
+ type: VALIDATION_SEVERITY.WARNING,
426
428
  message: `Potentially undefined variable: ${match}`,
427
429
  line: this.getLineNumber(html, html.indexOf(match)),
428
430
  column: 1,
429
431
  rule: 'liquid-undefined-variable',
430
- severity: 'warning',
431
- source: 'liquid-validator',
432
+ severity: VALIDATION_SEVERITY.WARNING,
433
+ source: ISSUE_SOURCES.LIQUID,
432
434
  });
433
435
  });
434
436
  }
@@ -31,9 +31,8 @@ export const LABEL_ISSUE_PATTERNS = [
31
31
  'alt-require',
32
32
  ];
33
33
 
34
- // Tab keys for error categorization
34
+ // Tab keys: Errors = blocking, Warnings = non-blocking
35
35
  export const ERROR_TAB_KEYS = {
36
- HTML: 'html',
37
- LABEL: 'label',
38
- LIQUID: 'liquid',
36
+ ERRORS: 'errors',
37
+ WARNINGS: 'warnings',
39
38
  };
@@ -0,0 +1,6 @@
1
+ export const DEVICE_TYPES = {
2
+ ANDROID: 'android',
3
+ IOS: 'ios',
4
+ };
5
+
6
+ export const IPHONE = 'iphone';
@@ -16,6 +16,7 @@ import { INAPP } from '../../v2Containers/App/constants';
16
16
  import { ANDROID, IOS } from '../../v2Containers/InApp/constants';
17
17
  import { getCtaObject } from '../../v2Containers/InApp/utils';
18
18
  import { CAROUSEL, VIDEO } from '../../v2Containers/MobilePushNew/constants';
19
+ import { DEVICE_TYPES, IPHONE } from './constants';
19
20
 
20
21
  class MobilePushPreviewV2 extends React.Component { // eslint-disable-line react/prefer-stateless-function
21
22
  constructor(props) {
@@ -57,7 +58,7 @@ class MobilePushPreviewV2 extends React.Component { // eslint-disable-line react
57
58
  const isBeeFreeTemplate = get(androidContent, 'isBEEeditor') || get(iosContent, 'isBEEeditor');
58
59
  if (isBeeFreeTemplate) {
59
60
  // Normalize device to 'android' or 'ios' for comparison
60
- const normalizedDevice = device === 'iphone' ? 'ios' : device?.toLowerCase();
61
+ const normalizedDevice = device === IPHONE ? DEVICE_TYPES.IOS : device?.toLowerCase();
61
62
  const isAndroid = normalizedDevice === ANDROID.toLowerCase();
62
63
  content = {
63
64
  inAppPreviewContent: isAndroid ? androidContent?.beeHtml : iosContent?.beeHtml,
@@ -78,7 +79,7 @@ class MobilePushPreviewV2 extends React.Component { // eslint-disable-line react
78
79
  ctaData: getCtaObject(iosContent?.expandableDetails?.ctas),
79
80
  };
80
81
  // Normalize device to 'android' or 'ios' for comparison
81
- const normalizedDevice = device === 'iphone' ? 'ios' : device?.toLowerCase();
82
+ const normalizedDevice = device === IPHONE ? DEVICE_TYPES.IOS : device?.toLowerCase();
82
83
  const isAndroid = normalizedDevice === ANDROID.toLowerCase();
83
84
  content = {
84
85
  inAppPreviewContent: isAndroid ? androidPreviewContent : iosPreviewContent,
@@ -149,7 +150,7 @@ class MobilePushPreviewV2 extends React.Component { // eslint-disable-line react
149
150
 
150
151
  getPreview(device) {
151
152
  // Normalize device to 'android' or 'ios' for comparison
152
- const normalizedDevice = device === 'iphone' ? 'ios' : device?.toLowerCase();
153
+ const normalizedDevice = device === IPHONE ? DEVICE_TYPES.IOS : device?.toLowerCase();
153
154
  const deviceParam = normalizedDevice === ANDROID.toLowerCase() ? ANDROID : IOS;
154
155
  return (
155
156
  <TemplatePreview
@@ -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
- "html": 0,
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
- "html": 0,
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
- "html": 0,
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 HTML/Label/Liquid validation errors (excluding API errors which we just cleared)
655
- // If errors exist, block save and reset acknowledgment
656
- // Try to get issue counts from ref first, fallback to stored state
657
- let issueCounts = {
658
- html: 0, label: 0, liquid: 0, total: 0,
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
  }
@@ -46,9 +46,7 @@ const mockGetAllIssues = jest.fn(() => []);
46
46
  const mockGetValidationState = jest.fn(() => ({
47
47
  isValidating: false,
48
48
  hasErrors: false,
49
- issueCounts: {
50
- html: 0, label: 0, liquid: 0, total: 0,
51
- },
49
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
52
50
  }));
53
51
 
54
52
  // Mock HtmlEditor - it exports a lazy-loaded component by default
@@ -62,12 +60,10 @@ jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
62
60
  isContentEmpty: () => !props.initialContent || (typeof props.initialContent === 'string' && props.initialContent.trim() === ''),
63
61
  getIssueCounts: () => {
64
62
  const issues = mockGetAllIssues();
65
- return {
66
- html: issues.filter((i) => i.source === 'htmlhint' || i.source === 'css-validator').length,
67
- label: issues.filter((i) => i.rule === 'tag-pair' || i.message?.includes('tag must be paired')).length,
68
- liquid: issues.filter((i) => i.source === 'liquid-validator').length,
69
- total: issues.length,
70
- };
63
+ const total = issues.length;
64
+ const errors = issues.filter((i) => (i.source === 'liquid-validator' && i.severity === 'error')
65
+ || ['liquid-api-validation', 'standard-api-validation'].includes(i.rule)).length;
66
+ return { errors, warnings: total - errors, total };
71
67
  },
72
68
  }));
73
69
 
@@ -94,9 +90,7 @@ jest.mock('../../../../v2Components/HtmlEditor/index.lazy', () => {
94
90
  <button
95
91
  onClick={() => props.onValidationChange && props.onValidationChange({
96
92
  isContentEmpty: false,
97
- issueCounts: {
98
- html: 0, label: 0, liquid: 0, total: 0,
99
- },
93
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
100
94
  validationComplete: true,
101
95
  hasErrors: false,
102
96
  })}
@@ -122,12 +116,10 @@ jest.mock('../../../../v2Components/HtmlEditor', () => {
122
116
  isContentEmpty: () => !props.initialContent || (typeof props.initialContent === 'string' && props.initialContent.trim() === ''),
123
117
  getIssueCounts: () => {
124
118
  const issues = mockGetAllIssues();
125
- return {
126
- html: issues.filter((i) => i.source === 'htmlhint' || i.source === 'css-validator').length,
127
- label: issues.filter((i) => i.rule === 'tag-pair' || i.message?.includes('tag must be paired')).length,
128
- liquid: issues.filter((i) => i.source === 'liquid-validator').length,
129
- total: issues.length,
130
- };
119
+ const total = issues.length;
120
+ const errors = issues.filter((i) => (i.source === 'liquid-validator' && i.severity === 'error')
121
+ || ['liquid-api-validation', 'standard-api-validation'].includes(i.rule)).length;
122
+ return { errors, warnings: total - errors, total };
131
123
  },
132
124
  }));
133
125
 
@@ -154,9 +146,7 @@ jest.mock('../../../../v2Components/HtmlEditor', () => {
154
146
  <button
155
147
  onClick={() => props.onValidationChange && props.onValidationChange({
156
148
  isContentEmpty: false,
157
- issueCounts: {
158
- html: 0, label: 0, liquid: 0, total: 0,
159
- },
149
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
160
150
  validationComplete: true,
161
151
  hasErrors: false,
162
152
  })}
@@ -426,9 +416,7 @@ describe('EmailHTMLEditor', () => {
426
416
  mockGetValidationState.mockReturnValue({
427
417
  isValidating: false,
428
418
  hasErrors: false,
429
- issueCounts: {
430
- html: 0, label: 0, liquid: 0, total: 0,
431
- },
419
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
432
420
  });
433
421
  // Reset hasLiquidSupportFeature mock to return true by default
434
422
  mockHasLiquidSupportFeature.mockReturnValue(true);
@@ -878,7 +866,7 @@ describe('EmailHTMLEditor', () => {
878
866
  isValidating: false,
879
867
  hasErrors: true,
880
868
  issueCounts: {
881
- html: 0, label: 0, liquid: 0, total: 1,
869
+ errors: 0, warnings: 1, total: 1,
882
870
  },
883
871
  });
884
872
 
@@ -921,7 +909,7 @@ describe('EmailHTMLEditor', () => {
921
909
  isValidating: false,
922
910
  hasErrors: true,
923
911
  issueCounts: {
924
- html: 0, label: 0, liquid: 0, total: 1,
912
+ errors: 0, warnings: 1, total: 1,
925
913
  },
926
914
  });
927
915
 
@@ -963,7 +951,7 @@ describe('EmailHTMLEditor', () => {
963
951
  isValidating: false,
964
952
  hasErrors: true,
965
953
  issueCounts: {
966
- html: 0, label: 0, liquid: 1, total: 1,
954
+ errors: 1, warnings: 0, total: 1,
967
955
  },
968
956
  });
969
957
 
@@ -2149,9 +2137,8 @@ describe('EmailHTMLEditor', () => {
2149
2137
 
2150
2138
  const issueCounts = ref.current.getIssueCounts();
2151
2139
  expect(issueCounts).toEqual({
2152
- html: expect.any(Number),
2153
- label: expect.any(Number),
2154
- liquid: expect.any(Number),
2140
+ errors: expect.any(Number),
2141
+ warnings: expect.any(Number),
2155
2142
  total: expect.any(Number),
2156
2143
  });
2157
2144
  });
@@ -2201,7 +2188,7 @@ describe('EmailHTMLEditor', () => {
2201
2188
  });
2202
2189
 
2203
2190
  it('getIssueCounts returns default object with zeros (lines 168-170)', async () => {
2204
- // This covers lines 168-170: return { html: 0, label: 0, liquid: 0, total: 0 };
2191
+ // This covers default return: { errors: 0, warnings: 0, total: 0 };
2205
2192
  const ref = React.createRef();
2206
2193
 
2207
2194
  render(
@@ -2218,18 +2205,16 @@ describe('EmailHTMLEditor', () => {
2218
2205
  }, { timeout: 3000 });
2219
2206
 
2220
2207
  // When htmlEditorRef.current?.getIssueCounts is not available,
2221
- // the default return value should be { html: 0, label: 0, liquid: 0, total: 0 }
2208
+ // the default return value should be { errors: 0, warnings: 0, total: 0 }
2222
2209
  const issueCounts = ref.current.getIssueCounts();
2223
2210
  expect(issueCounts).toEqual({
2224
- html: expect.any(Number),
2225
- label: expect.any(Number),
2226
- liquid: expect.any(Number),
2211
+ errors: expect.any(Number),
2212
+ warnings: expect.any(Number),
2227
2213
  total: expect.any(Number),
2228
2214
  });
2229
2215
  // Verify all keys exist
2230
- expect(issueCounts.html).toBeDefined();
2231
- expect(issueCounts.label).toBeDefined();
2232
- expect(issueCounts.liquid).toBeDefined();
2216
+ expect(issueCounts.errors).toBeDefined();
2217
+ expect(issueCounts.warnings).toBeDefined();
2233
2218
  expect(issueCounts.total).toBeDefined();
2234
2219
  });
2235
2220
 
@@ -14,8 +14,10 @@ export default css`
14
14
  margin: 0 auto;
15
15
  width: 100%;
16
16
  padding: ${CAP_SPACE_24} 0;
17
- .ant-tabs-content{
18
- margin-top: ${CAP_SPACE_16}
17
+ /* Only main channel tabs content, not HTML Editor validation panel tabs */
18
+ > .cap-tab-v2 > .ant-tabs-content-holder > .ant-tabs-content,
19
+ > .cap-tab-v2 > .ant-tabs-content {
20
+ margin-top: ${CAP_SPACE_16};
19
21
  }
20
22
  ` : `.cap-tab-v2 {
21
23
  height: calc(100vh - 11.25rem);