@capillarytech/creatives-library 8.0.249 → 8.0.250-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/constants/unified.js +2 -1
  4. package/initialReducer.js +2 -0
  5. package/package.json +1 -1
  6. package/services/api.js +10 -0
  7. package/services/tests/api.test.js +18 -0
  8. package/utils/common.js +5 -0
  9. package/utils/commonUtils.js +28 -5
  10. package/utils/tests/commonUtil.test.js +224 -0
  11. package/utils/transformTemplateConfig.js +0 -10
  12. package/v2Components/CapDeviceContent/index.js +61 -56
  13. package/v2Components/CapTagList/index.js +6 -1
  14. package/v2Components/CapTagListWithInput/index.js +5 -1
  15. package/v2Components/CapTagListWithInput/messages.js +1 -1
  16. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  17. package/v2Components/ErrorInfoNote/index.js +452 -72
  18. package/v2Components/ErrorInfoNote/messages.js +22 -0
  19. package/v2Components/ErrorInfoNote/style.scss +280 -4
  20. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  21. package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
  22. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
  23. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
  24. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  25. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  26. package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
  27. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
  28. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
  29. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  30. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  31. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
  32. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
  33. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  34. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  35. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  36. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  37. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  38. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  39. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
  40. package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
  41. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  42. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
  43. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
  44. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
  45. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
  46. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  47. package/v2Components/HtmlEditor/constants.js +42 -20
  48. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  52. package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
  53. package/v2Components/HtmlEditor/index.js +1 -1
  54. package/v2Components/HtmlEditor/messages.js +95 -85
  55. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
  56. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  57. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  58. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
  59. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  60. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  61. package/v2Components/MobilePushPreviewV2/index.js +32 -7
  62. package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
  63. package/v2Components/TemplatePreview/index.js +47 -32
  64. package/v2Components/TemplatePreview/messages.js +4 -0
  65. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  66. package/v2Containers/BeeEditor/index.js +172 -90
  67. package/v2Containers/BeePopupEditor/constants.js +10 -0
  68. package/v2Containers/BeePopupEditor/index.js +193 -0
  69. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  70. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  71. package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
  72. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  73. package/v2Containers/CreativesContainer/constants.js +1 -0
  74. package/v2Containers/CreativesContainer/index.js +239 -46
  75. package/v2Containers/CreativesContainer/messages.js +8 -0
  76. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  77. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  78. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
  79. package/v2Containers/Email/actions.js +7 -0
  80. package/v2Containers/Email/constants.js +5 -1
  81. package/v2Containers/Email/index.js +222 -27
  82. package/v2Containers/Email/messages.js +32 -0
  83. package/v2Containers/Email/reducer.js +12 -1
  84. package/v2Containers/Email/sagas.js +61 -7
  85. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  86. package/v2Containers/Email/tests/sagas.test.js +320 -29
  87. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
  88. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
  89. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  90. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
  91. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  92. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  93. package/v2Containers/EmailWrapper/constants.js +2 -0
  94. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
  95. package/v2Containers/EmailWrapper/index.js +103 -23
  96. package/v2Containers/EmailWrapper/messages.js +61 -1
  97. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
  98. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
  99. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  100. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  101. package/v2Containers/InApp/actions.js +7 -0
  102. package/v2Containers/InApp/constants.js +20 -4
  103. package/v2Containers/InApp/index.js +802 -359
  104. package/v2Containers/InApp/index.scss +4 -3
  105. package/v2Containers/InApp/messages.js +7 -3
  106. package/v2Containers/InApp/reducer.js +21 -3
  107. package/v2Containers/InApp/sagas.js +29 -9
  108. package/v2Containers/InApp/selectors.js +25 -5
  109. package/v2Containers/InApp/tests/index.test.js +154 -50
  110. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  111. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  112. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  113. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
  114. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  115. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
  116. package/v2Containers/InAppWrapper/constants.js +16 -0
  117. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  118. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  119. package/v2Containers/InAppWrapper/index.js +148 -0
  120. package/v2Containers/InAppWrapper/messages.js +49 -0
  121. package/v2Containers/InappAdvance/index.js +1099 -0
  122. package/v2Containers/InappAdvance/index.scss +10 -0
  123. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  124. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  125. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  126. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  127. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  128. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  129. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  130. package/v2Containers/TagList/index.js +62 -19
  131. package/v2Containers/Templates/_templates.scss +60 -1
  132. package/v2Containers/Templates/index.js +89 -4
  133. package/v2Containers/Templates/messages.js +4 -0
  134. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  135. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  136. package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +0 -214
@@ -12,14 +12,14 @@
12
12
  * Note: Uses injectIntl with forwardRef to provide direct access to CodeEditorPane via ref
13
13
  */
14
14
 
15
- import React, { useRef, useCallback, useMemo, useState } from 'react';
15
+ import React, {
16
+ useRef, useCallback, useMemo, useState, useEffect, useImperativeHandle, forwardRef,
17
+ } from 'react';
16
18
  import PropTypes from 'prop-types';
17
- import { Layout } from 'antd'; // Fallback - no Cap UI equivalent
18
19
  import { injectIntl, intlShape } from 'react-intl';
19
20
 
20
21
  // Cap UI Components (First Preference)
21
22
  import CapRow from '@capillarytech/cap-ui-library/CapRow';
22
- import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
23
23
  import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
24
24
  import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
25
25
  import CapModal from '@capillarytech/cap-ui-library/CapModal';
@@ -30,7 +30,6 @@ import DeviceToggle from './components/DeviceToggle';
30
30
  import SplitContainer from './components/SplitContainer';
31
31
  import CodeEditorPane from './components/CodeEditorPane';
32
32
  import PreviewPane from './components/PreviewPane';
33
- import ValidationErrorDisplay from './components/ValidationErrorDisplay';
34
33
  import { EditorProvider } from './components/common/EditorContext';
35
34
 
36
35
  // Hooks and utilities
@@ -40,7 +39,9 @@ import { useLayoutState } from './hooks/useLayoutState';
40
39
  import { useValidation } from './hooks/useValidation';
41
40
 
42
41
  // Constants
43
- import { HTML_EDITOR_VARIANTS, DEVICE_TYPES, DEFAULT_HTML_CONTENT } from './constants';
42
+ import {
43
+ HTML_EDITOR_VARIANTS, DEVICE_TYPES, DEFAULT_HTML_CONTENT, TAG, EMBEDDED, DEFAULT, FULL, ALL, SMS, EMAIL,
44
+ } from './constants';
44
45
 
45
46
  // Styles
46
47
  import './_htmlEditor.scss';
@@ -49,7 +50,7 @@ import './components/FullscreenModal/_fullscreenModal.scss';
49
50
  // Messages
50
51
  import messages from './messages';
51
52
 
52
- const HTMLEditor = ({
53
+ const HTMLEditor = forwardRef(({
53
54
  intl,
54
55
  variant = HTML_EDITOR_VARIANTS.EMAIL, // New prop: 'email' or 'inapp'
55
56
  layoutType, // Layout type for InApp variant
@@ -61,17 +62,33 @@ const HTMLEditor = ({
61
62
  showFullscreenButton = true,
62
63
  autoSave = true,
63
64
  autoSaveInterval = 30000, // 30 seconds
65
+ // Tag-related props - tags are fetched and managed by parent component (EmailHTMLEditor, INAPP, etc.)
66
+ tags = [],
67
+ injectedTags = {},
68
+ location,
69
+ eventContextTags = [],
70
+ selectedOfferDetails = [],
71
+ channel,
72
+ userLocale = 'en',
73
+ moduleFilterEnabled = true,
74
+ onTagContextChange, // Parent component handles tag fetching
75
+ onTagSelect = null,
76
+ onContextChange = null,
77
+ globalActions = null,
78
+ isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
79
+ isFullMode = true, // Full mode vs library mode - controls layout and visibility
80
+ onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
81
+ onValidationChange = null, // Callback when validation state changes (for parent to track errors)
82
+ apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent { liquidErrors: [], standardErrors: [] }
64
83
  ...props
65
- }) => {
84
+ }, ref) => {
66
85
  // Separate refs for main and modal editors to avoid conflicts
67
86
  const mainEditorRef = useRef(null);
68
87
  const modalEditorRef = useRef(null);
69
88
  const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
70
89
 
71
90
  // Get the currently active editor ref based on fullscreen state
72
- const getActiveEditorRef = useCallback(() => {
73
- return isFullscreenModalOpen ? modalEditorRef : mainEditorRef;
74
- }, [isFullscreenModalOpen]);
91
+ const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
75
92
 
76
93
  // Initialize custom hooks for state management - always call both hooks to follow Rules of Hooks
77
94
  const isEmailVariant = variant === HTML_EDITOR_VARIANTS.EMAIL;
@@ -80,7 +97,7 @@ const HTMLEditor = ({
80
97
  autoSave: isEmailVariant ? autoSave : false,
81
98
  autoSaveInterval,
82
99
  onSave: isEmailVariant ? onSave : null,
83
- onChange: isEmailVariant ? onContentChange : null
100
+ onChange: isEmailVariant ? onContentChange : null,
84
101
  };
85
102
 
86
103
  const emailContent = useEditorContent(
@@ -97,7 +114,7 @@ const HTMLEditor = ({
97
114
  // Convert string content to device-specific format
98
115
  inAppInitialContent = {
99
116
  [DEVICE_TYPES.ANDROID]: initialContent,
100
- [DEVICE_TYPES.IOS]: initialContent
117
+ [DEVICE_TYPES.IOS]: initialContent,
101
118
  };
102
119
  } else {
103
120
  // Use provided device-specific content
@@ -109,7 +126,7 @@ const HTMLEditor = ({
109
126
  autoSave: isInAppVariant ? autoSave : false,
110
127
  autoSaveInterval,
111
128
  onSave: isInAppVariant ? onSave : null,
112
- onChange: isInAppVariant ? onContentChange : null
129
+ onChange: isInAppVariant ? onContentChange : null,
113
130
  };
114
131
 
115
132
  const inAppContent = useInAppContent(inAppInitialContent, inAppOptions);
@@ -117,6 +134,64 @@ const HTMLEditor = ({
117
134
  // Use appropriate content hook based on variant
118
135
  const content = variant === HTML_EDITOR_VARIANTS.EMAIL ? emailContent : inAppContent;
119
136
 
137
+ // Update content when initialContent prop changes (for edit mode)
138
+ // This ensures the editor updates when template data loads
139
+ useEffect(() => {
140
+ if (isEmailVariant && emailContent && initialContent !== undefined && initialContent !== null) {
141
+ // Only update if content is different to avoid unnecessary updates
142
+ if (emailContent.content !== initialContent) {
143
+ emailContent.updateContent(initialContent, true); // immediate update
144
+ }
145
+ } else if (isInAppVariant && inAppContent && initialContent !== undefined && initialContent !== null) {
146
+ // Handle InApp variant updates
147
+ const contentToUpdate = typeof initialContent === 'string'
148
+ ? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
149
+ : initialContent;
150
+ if (inAppContent.updateContent) {
151
+ const currentContent = inAppContent.getDeviceContent?.(inAppContent.activeDevice);
152
+ const newContent = contentToUpdate[inAppContent.activeDevice] || contentToUpdate[DEVICE_TYPES.ANDROID] || '';
153
+ if (currentContent !== newContent) {
154
+ inAppContent.updateContent(newContent, true);
155
+ }
156
+ }
157
+ }
158
+ }, [initialContent, isEmailVariant, isInAppVariant]);
159
+ // Handle context change for tag API calls
160
+ // If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
161
+ const handleContextChange = useCallback((contextData) => {
162
+ // If onContextChange is provided, use it instead of making our own API call
163
+ // This prevents duplicate API calls when parent component handles tag fetching
164
+ if (onContextChange) {
165
+ onContextChange(contextData);
166
+ return;
167
+ }
168
+
169
+ // Only make API call if onContextChange is not provided and globalActions is available
170
+ if (!globalActions || !location) {
171
+ return;
172
+ }
173
+
174
+ const { type } = location.query || {};
175
+ const tempData = (contextData || '').toLowerCase();
176
+ const isEmbedded = type === EMBEDDED;
177
+ const embedded = isEmbedded ? type : FULL;
178
+ const context = tempData === ALL ? DEFAULT : tempData;
179
+
180
+ // Determine layout: INAPP variant uses SMS, EMAIL variant uses EMAIL
181
+ const layout = variant === HTML_EDITOR_VARIANTS.INAPP ? SMS : EMAIL;
182
+
183
+ const query = {
184
+ layout,
185
+ type: TAG,
186
+ context,
187
+ embedded,
188
+ };
189
+
190
+ // Call the API via Redux action - this will trigger the saga which calls Api.fetchSchemaForEntity
191
+ // The API endpoint will be: /meta/TAG?query={...}
192
+ globalActions.fetchSchemaForEntity(query);
193
+ }, [variant, globalActions, location, onContextChange]);
194
+
120
195
  // Destructure content properties for cleaner access throughout component
121
196
  const {
122
197
  activeDevice,
@@ -124,14 +199,14 @@ const HTMLEditor = ({
124
199
  switchDevice,
125
200
  toggleContentSync,
126
201
  getDeviceContent,
127
- markAsSaved
202
+ markAsSaved,
128
203
  } = content || {};
129
204
 
130
205
  const layout = useLayoutState({
131
206
  splitSizes: [50, 50],
132
207
  viewMode: 'desktop',
133
208
  mobileWidth: 375,
134
- isFullscreen: false
209
+ isFullscreen: false,
135
210
  });
136
211
 
137
212
  // Get current content for validation based on variant
@@ -158,7 +233,7 @@ const HTMLEditor = ({
158
233
  'sanitizer.productionValidHtml': messages.sanitizer.productionValidHtml,
159
234
  'sanitizer.productionSanitized': messages.sanitizer.productionSanitized,
160
235
  'sanitizer.productionInlineCss': messages.sanitizer.productionInlineCss,
161
- 'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent
236
+ 'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent,
162
237
  };
163
238
 
164
239
  const messageObj = messageMap[messageKey];
@@ -179,7 +254,7 @@ const HTMLEditor = ({
179
254
  'validator.largeImageDetected': messages.validator.largeImageDetected,
180
255
  'validator.unclosedCssRule': messages.validator.unclosedCssRule,
181
256
  'validator.emptyCssRule': messages.validator.emptyCssRule,
182
- 'validator.cssValidationFailed': messages.validator.cssValidationFailed
257
+ 'validator.cssValidationFailed': messages.validator.cssValidationFailed,
183
258
  };
184
259
 
185
260
  const messageObj = messageMap[messageKey];
@@ -190,57 +265,469 @@ const HTMLEditor = ({
190
265
  enableRealTime: true,
191
266
  debounceMs: 500,
192
267
  enableSanitization: true,
193
- securityLevel: 'standard'
268
+ securityLevel: 'standard',
269
+ apiValidationErrors, // Pass API validation errors to merge with client-side validation
194
270
  }, formatSanitizerMessage, formatValidatorMessage);
195
271
 
196
- // Handle label insertion at cursor position
197
- const handleLabelInsert = useCallback((label, position) => {
198
- // With injectIntl({ forwardRef: true }), ref points directly to CodeEditorPane
199
- const activeEditorRef = getActiveEditorRef();
200
- const editor = activeEditorRef.current;
272
+ // Expose validation and content state via ref
273
+ useImperativeHandle(ref, () => ({
274
+ getValidation: () => validation,
275
+ getContent: () => currentContent,
276
+ isContentEmpty: () => !currentContent || currentContent.trim() === '',
277
+ getIssueCounts: () => {
278
+ // Check if validation is ready and has getAllIssues method
279
+ if (!validation || typeof validation.getAllIssues !== 'function') {
280
+ return {
281
+ html: 0, label: 0, liquid: 0, total: 0,
282
+ };
283
+ }
284
+ const allIssues = validation.getAllIssues();
285
+ const ISSUE_SOURCES = {
286
+ HTMLHINT: 'htmlhint',
287
+ CSS_VALIDATOR: 'css-validator',
288
+ CUSTOM: 'custom',
289
+ SECURITY: 'security',
290
+ LIQUID: 'liquid-validator',
291
+ };
292
+ const LABEL_ISSUE_PATTERNS = [
293
+ 'tag must be paired',
294
+ 'open tag match failed',
295
+ 'closed tag match failed',
296
+ 'unclosed',
297
+ 'missing required',
298
+ 'tag-pair',
299
+ 'attr-value-not-empty',
300
+ 'attr-no-duplication',
301
+ 'tag-self-close',
302
+ 'spec-char-escape',
303
+ 'tagname-lowercase',
304
+ 'attr-lowercase',
305
+ 'src-not-empty',
306
+ 'alt-require',
307
+ ];
308
+
309
+ let htmlCount = 0;
310
+ let labelCount = 0;
311
+ let liquidCount = 0;
201
312
 
202
- if (!editor) {
203
- CapNotification.warning({
204
- message: intl.formatMessage(messages.labelInsertError),
205
- description: intl.formatMessage(messages.editorNotReady),
206
- duration: 3
313
+ allIssues.forEach((issue) => {
314
+ const { source, rule, message } = issue;
315
+ const messageLower = (message || '').toLowerCase();
316
+ const ruleLower = (rule || '').toLowerCase();
317
+
318
+ // Check if it's a Liquid issue
319
+ if (source === ISSUE_SOURCES.LIQUID) {
320
+ liquidCount++;
321
+ return;
322
+ }
323
+
324
+ // Check if it's a Label (tag syntax) issue
325
+ const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
326
+ (pattern) => messageLower.includes(pattern.toLowerCase())
327
+ || ruleLower.includes(pattern.toLowerCase()),
328
+ );
329
+
330
+ if (isLabelIssue) {
331
+ labelCount++;
332
+ return;
333
+ }
334
+
335
+ // Default to HTML issues
336
+ htmlCount++;
207
337
  });
338
+
339
+ return {
340
+ html: htmlCount,
341
+ label: labelCount,
342
+ liquid: liquidCount,
343
+ total: allIssues.length,
344
+ };
345
+ },
346
+ getValidationState: () =>
347
+ ({
348
+ isValidating: validation?.isValidating || false,
349
+ hasErrors: validation?.hasBlockingErrors || false,
350
+ issueCounts: (() => {
351
+ if (!validation || typeof validation.getAllIssues !== 'function') {
352
+ return {
353
+ html: 0, label: 0, liquid: 0, total: 0,
354
+ };
355
+ }
356
+ const allIssues = validation.getAllIssues();
357
+ // Use same logic as getIssueCounts
358
+ const ISSUE_SOURCES = {
359
+ HTMLHINT: 'htmlhint',
360
+ CSS_VALIDATOR: 'css-validator',
361
+ CUSTOM: 'custom',
362
+ SECURITY: 'security',
363
+ LIQUID: 'liquid-validator',
364
+ };
365
+ const LABEL_ISSUE_PATTERNS = [
366
+ 'tag must be paired',
367
+ 'open tag match failed',
368
+ 'closed tag match failed',
369
+ 'unclosed',
370
+ 'missing required',
371
+ 'tag-pair',
372
+ 'attr-value-not-empty',
373
+ 'attr-no-duplication',
374
+ 'tag-self-close',
375
+ 'spec-char-escape',
376
+ 'tagname-lowercase',
377
+ 'attr-lowercase',
378
+ 'src-not-empty',
379
+ 'alt-require',
380
+ ];
381
+
382
+ let htmlCount = 0;
383
+ let labelCount = 0;
384
+ let liquidCount = 0;
385
+
386
+ allIssues.forEach((issue) => {
387
+ const { source, rule, message } = issue;
388
+ const messageLower = (message || '').toLowerCase();
389
+ const ruleLower = (rule || '').toLowerCase();
390
+
391
+ if (source === ISSUE_SOURCES.LIQUID) {
392
+ liquidCount++;
393
+ return;
394
+ }
395
+
396
+ const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
397
+ (pattern) => messageLower.includes(pattern.toLowerCase())
398
+ || ruleLower.includes(pattern.toLowerCase()),
399
+ );
400
+
401
+ if (isLabelIssue) {
402
+ labelCount++;
403
+ return;
404
+ }
405
+
406
+ htmlCount++;
407
+ });
408
+
409
+ return {
410
+ html: htmlCount,
411
+ label: labelCount,
412
+ liquid: liquidCount,
413
+ total: allIssues.length,
414
+ };
415
+ })(),
416
+ })
417
+ ,
418
+ }), [validation, currentContent, apiValidationErrors]); // Include apiValidationErrors so ref methods return updated counts
419
+
420
+ // Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
421
+ const onValidationChangeRef = useRef(onValidationChange);
422
+ useEffect(() => {
423
+ onValidationChangeRef.current = onValidationChange;
424
+ }, [onValidationChange]);
425
+
426
+ // Track last sent validation state to prevent duplicate updates
427
+ const lastSentValidationStateRef = useRef(null);
428
+
429
+ // Store validation ref to access current value without triggering re-renders
430
+ const validationRef = useRef(validation);
431
+ validationRef.current = validation;
432
+
433
+ // Extract STABLE primitive values from validation for dependency array
434
+ const isValidating = validation?.isValidating;
435
+ const validationTotalErrors = validation?.summary?.totalErrors || 0;
436
+ const validationTotalWarnings = validation?.summary?.totalWarnings || 0;
437
+ const validationLastValidated = validation?.lastValidated?.getTime?.() || null;
438
+ // Only Rule Group #1 (Input & Sanitization) blocks; use for UI gating (Done/Update/Preview/Test)
439
+ const validationHasBlockingErrors = validation?.hasBlockingErrors || false;
440
+
441
+ // Notify parent component when validation state changes
442
+ useEffect(() => {
443
+ if (!onValidationChangeRef.current) {
208
444
  return;
209
445
  }
210
446
 
211
- // Check if the required methods exist
212
- if (typeof editor?.insertText !== 'function') {
213
- CapNotification.error({
214
- message: intl.formatMessage(messages.labelInsertError),
215
- description: intl.formatMessage(messages.editorMethodNotAvailable),
216
- duration: 4
447
+ // Skip if still validating (wait for validation to complete)
448
+ if (isValidating) {
449
+ return;
450
+ }
451
+
452
+ // Calculate issue counts from validation using ref (avoid stale closure)
453
+ const calculateIssueCounts = () => {
454
+ const currentValidation = validationRef.current;
455
+ if (!currentValidation || typeof currentValidation.getAllIssues !== 'function') {
456
+ return {
457
+ html: 0, label: 0, liquid: 0, total: 0,
458
+ };
459
+ }
460
+ const allIssues = currentValidation.getAllIssues();
461
+ const ISSUE_SOURCES = {
462
+ HTMLHINT: 'htmlhint',
463
+ CSS_VALIDATOR: 'css-validator',
464
+ CUSTOM: 'custom',
465
+ SECURITY: 'security',
466
+ LIQUID: 'liquid-validator',
467
+ };
468
+ const LABEL_ISSUE_PATTERNS = [
469
+ 'tag must be paired',
470
+ 'open tag match failed',
471
+ 'closed tag match failed',
472
+ 'unclosed',
473
+ 'missing required',
474
+ 'tag-pair',
475
+ 'attr-value-not-empty',
476
+ 'attr-no-duplication',
477
+ 'tag-self-close',
478
+ 'spec-char-escape',
479
+ 'tagname-lowercase',
480
+ 'attr-lowercase',
481
+ 'src-not-empty',
482
+ 'alt-require',
483
+ ];
484
+
485
+ let htmlCount = 0;
486
+ let labelCount = 0;
487
+ let liquidCount = 0;
488
+
489
+ allIssues.forEach((issue) => {
490
+ const { source, rule, message } = issue;
491
+ const messageLower = (message || '').toLowerCase();
492
+ const ruleLower = (rule || '').toLowerCase();
493
+
494
+ if (source === ISSUE_SOURCES.LIQUID) {
495
+ liquidCount += 1;
496
+ return;
497
+ }
498
+
499
+ const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
500
+ (pattern) => messageLower.includes(pattern.toLowerCase())
501
+ || ruleLower.includes(pattern.toLowerCase()),
502
+ );
503
+
504
+ if (isLabelIssue) {
505
+ labelCount += 1;
506
+ return;
507
+ }
508
+
509
+ htmlCount += 1;
217
510
  });
511
+
512
+ return {
513
+ html: htmlCount,
514
+ label: labelCount,
515
+ liquid: liquidCount,
516
+ total: allIssues.length,
517
+ };
518
+ };
519
+
520
+ const issueCounts = calculateIssueCounts();
521
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
522
+
523
+ // hasErrors = only Rule Group #1 (Input & Sanitization) – gates Done/Update/Preview/Test
524
+ const newState = {
525
+ isContentEmpty,
526
+ issueCounts,
527
+ validationComplete: true,
528
+ hasErrors: validationRef.current?.hasBlockingErrors ?? false,
529
+ };
530
+
531
+ // Only call callback if state actually changed (prevent infinite loops)
532
+ const lastState = lastSentValidationStateRef.current;
533
+ const hasChanged = !lastState
534
+ || lastState.isContentEmpty !== newState.isContentEmpty
535
+ || lastState.validationComplete !== newState.validationComplete
536
+ || lastState.hasErrors !== newState.hasErrors
537
+ || lastState.issueCounts?.total !== newState.issueCounts?.total
538
+ || lastState.issueCounts?.html !== newState.issueCounts?.html
539
+ || lastState.issueCounts?.label !== newState.issueCounts?.label
540
+ || lastState.issueCounts?.liquid !== newState.issueCounts?.liquid;
541
+
542
+ if (hasChanged) {
543
+ lastSentValidationStateRef.current = newState;
544
+ onValidationChangeRef.current(newState);
545
+ }
546
+ }, [isValidating, validationTotalErrors, validationTotalWarnings, validationLastValidated, validationHasBlockingErrors, currentContent, apiValidationErrors]);
547
+
548
+ // Send initial state on mount to ensure parent has correct initial button state
549
+ const hasInitializedRef = useRef(false);
550
+ useEffect(() => {
551
+ if (hasInitializedRef.current || !onValidationChangeRef.current) {
218
552
  return;
219
553
  }
554
+ hasInitializedRef.current = true;
220
555
 
221
- try {
222
- // Get current cursor position or use provided position
223
- const cursor = position !== undefined
224
- ? position
225
- : (typeof editor?.getCursor === 'function' ? editor.getCursor() : 0);
556
+ // Send initial state with validationComplete=false to indicate validation pending
557
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
558
+ onValidationChangeRef.current({
559
+ isContentEmpty,
560
+ issueCounts: {
561
+ html: 0, label: 0, liquid: 0, total: 0,
562
+ },
563
+ validationComplete: false, // Validation hasn't run yet
564
+ hasErrors: false,
565
+ });
566
+ }, [currentContent]); // Only depend on currentContent to run on initial content load
567
+
568
+ // Force validation state recalculation when API validation errors change
569
+ // This ensures that API errors are included in issue counts and displayed in ValidationErrorDisplay
570
+ useEffect(() => {
571
+ if (!onValidationChangeRef.current) {
572
+ return;
573
+ }
574
+
575
+ // Skip if still validating (wait for validation to complete)
576
+ if (validation?.isValidating) {
577
+ return;
578
+ }
579
+
580
+ // Recalculate issue counts including API errors
581
+ const calculateIssueCounts = () => {
582
+ if (!validation || typeof validation.getAllIssues !== 'function') {
583
+ return {
584
+ html: 0, label: 0, liquid: 0, total: 0,
585
+ };
586
+ }
587
+ const allIssues = validation.getAllIssues();
588
+ const ISSUE_SOURCES = {
589
+ HTMLHINT: 'htmlhint',
590
+ CSS_VALIDATOR: 'css-validator',
591
+ CUSTOM: 'custom',
592
+ SECURITY: 'security',
593
+ LIQUID: 'liquid-validator',
594
+ };
595
+ const LABEL_ISSUE_PATTERNS = [
596
+ 'tag must be paired',
597
+ 'open tag match failed',
598
+ 'closed tag match failed',
599
+ 'unclosed',
600
+ 'missing required',
601
+ 'tag-pair',
602
+ 'attr-value-not-empty',
603
+ 'attr-no-duplication',
604
+ 'tag-self-close',
605
+ 'spec-char-escape',
606
+ 'tagname-lowercase',
607
+ 'attr-lowercase',
608
+ 'src-not-empty',
609
+ 'alt-require',
610
+ ];
226
611
 
227
- // Insert label at cursor position
228
- editor.insertText(label, cursor);
612
+ let htmlCount = 0;
613
+ let labelCount = 0;
614
+ let liquidCount = 0;
229
615
 
230
- // Focus the editor if focus method is available
231
- editor?.focus?.();
616
+ allIssues.forEach((issue) => {
617
+ const { source, rule, message } = issue;
618
+ const messageLower = (message || '').toLowerCase();
619
+ const ruleLower = (rule || '').toLowerCase();
232
620
 
233
- // Show success notification
621
+ if (source === ISSUE_SOURCES.LIQUID) {
622
+ liquidCount += 1;
623
+ return;
624
+ }
625
+
626
+ const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
627
+ (pattern) => messageLower.includes(pattern.toLowerCase())
628
+ || ruleLower.includes(pattern.toLowerCase()),
629
+ );
630
+
631
+ if (isLabelIssue) {
632
+ labelCount += 1;
633
+ return;
634
+ }
635
+
636
+ htmlCount += 1;
637
+ });
638
+
639
+ return {
640
+ html: htmlCount,
641
+ label: labelCount,
642
+ liquid: liquidCount,
643
+ total: allIssues.length,
644
+ };
645
+ };
646
+
647
+ const issueCounts = calculateIssueCounts();
648
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
649
+
650
+ const newState = {
651
+ isContentEmpty,
652
+ issueCounts,
653
+ validationComplete: true,
654
+ hasErrors: validation?.hasBlockingErrors ?? false,
655
+ };
656
+
657
+ const lastState = lastSentValidationStateRef.current;
658
+ const hasChanged = !lastState
659
+ || lastState.hasErrors !== newState.hasErrors
660
+ || lastState.issueCounts?.total !== newState.issueCounts?.total
661
+ || lastState.issueCounts?.html !== newState.issueCounts?.html
662
+ || lastState.issueCounts?.label !== newState.issueCounts?.label
663
+ || lastState.issueCounts?.liquid !== newState.issueCounts?.liquid;
664
+
665
+ if (hasChanged) {
666
+ lastSentValidationStateRef.current = newState;
667
+ onValidationChangeRef.current(newState);
668
+ }
669
+ }, [apiValidationErrors, validation, currentContent, onValidationChangeRef]);
670
+
671
+ // Handle label insertion at cursor position
672
+ // Note: This is called for notification purposes only when tag is inserted via CodeEditorPane
673
+ // The actual insertion happens in CodeEditorPane.handleTagSelect
674
+ const handleLabelInsert = useCallback((label, position) => {
675
+ // If position is explicitly null, it means the editor wasn't ready when tag was selected
676
+ // In this case, CodeEditorPane couldn't insert the tag, so we should try here
677
+ if (position === null) {
678
+ // With injectIntl({ forwardRef: true }), ref points directly to CodeEditorPane
679
+ const activeEditorRef = getActiveEditorRef();
680
+ const editor = activeEditorRef.current;
681
+
682
+ if (!editor) {
683
+ CapNotification.warning({
684
+ message: intl.formatMessage(messages.labelInsertError),
685
+ description: intl.formatMessage(messages.editorNotReady),
686
+ duration: 3,
687
+ });
688
+ return;
689
+ }
690
+
691
+ // Check if the required methods exist
692
+ if (typeof editor?.insertText !== 'function') {
693
+ CapNotification.error({
694
+ message: intl.formatMessage(messages.labelInsertError),
695
+ description: intl.formatMessage(messages.editorMethodNotAvailable),
696
+ duration: 4,
697
+ });
698
+ return;
699
+ }
700
+
701
+ try {
702
+ // Get current cursor position
703
+ const cursor = typeof editor?.getCursor === 'function' ? editor.getCursor() : 0;
704
+
705
+ // Insert label at cursor position
706
+ editor.insertText(label, cursor);
707
+
708
+ // Focus the editor if focus method is available
709
+ editor?.focus?.();
710
+
711
+ // Show success notification
712
+ CapNotification.success({
713
+ message: intl.formatMessage(messages.labelInserted),
714
+ description: intl.formatMessage(messages.labelInsertedDescription, { label }),
715
+ duration: 2,
716
+ });
717
+ } catch (error) {
718
+ CapNotification.error({
719
+ message: intl.formatMessage(messages.labelInsertError),
720
+ description: error.message,
721
+ duration: 4,
722
+ });
723
+ }
724
+ } else {
725
+ // Tag was already inserted by CodeEditorPane (position is a valid number)
726
+ // Just show success notification - no need to access editor
234
727
  CapNotification.success({
235
728
  message: intl.formatMessage(messages.labelInserted),
236
729
  description: intl.formatMessage(messages.labelInsertedDescription, { label }),
237
- duration: 2
238
- });
239
- } catch (error) {
240
- CapNotification.error({
241
- message: intl.formatMessage(messages.labelInsertError),
242
- description: error.message,
243
- duration: 4
730
+ duration: 2,
244
731
  });
245
732
  }
246
733
  }, [intl, getActiveEditorRef]);
@@ -259,13 +746,13 @@ const HTMLEditor = ({
259
746
 
260
747
  CapNotification.success({
261
748
  message: intl.formatMessage(messages.contentSaved),
262
- duration: 2
749
+ duration: 2,
263
750
  });
264
751
  } catch (error) {
265
752
  CapNotification.error({
266
753
  message: intl.formatMessage(messages.saveError),
267
754
  description: error.message,
268
- duration: 4
755
+ duration: 4,
269
756
  });
270
757
  }
271
758
  }, [content, onSave, intl, markAsSaved]);
@@ -276,21 +763,27 @@ const HTMLEditor = ({
276
763
  const editorInstance = activeEditorRef.current;
277
764
  const { line, column = 1 } = error || {};
278
765
 
279
- if (editorInstance && line) {
766
+ // Notify parent that user acknowledged errors by clicking redirection icon
767
+ // This enables the buttons even if we can't navigate to a specific line
768
+ if (onErrorAcknowledged) {
769
+ onErrorAcknowledged();
770
+ }
771
+
772
+ if (editorInstance) {
280
773
  try {
281
- // Access the CodeMirror view through the exposed ref methods
282
- if (editorInstance?.navigateToLine) {
774
+ // If line number exists, navigate to it; otherwise just focus the editor
775
+ if (line && editorInstance?.navigateToLine) {
283
776
  editorInstance.navigateToLine(line, column);
284
777
  } else {
778
+ // For API errors without line numbers, just focus the editor
285
779
  editorInstance?.focus?.();
286
- // Fallback: just focus the editor if navigation isn't available
287
780
  }
288
781
  } catch (err) {
289
782
  // Fallback: just focus the editor
290
783
  editorInstance?.focus?.();
291
784
  }
292
785
  }
293
- }, [getActiveEditorRef]);
786
+ }, [getActiveEditorRef, onErrorAcknowledged]);
294
787
 
295
788
  // Handle fullscreen modal
296
789
  const handleOpenFullscreen = useCallback(() => {
@@ -307,6 +800,7 @@ const HTMLEditor = ({
307
800
  content,
308
801
  layout,
309
802
  validation,
803
+ isLiquidEnabled,
310
804
  editorRef: getActiveEditorRef(),
311
805
  handleLabelInsert,
312
806
  handleSave,
@@ -319,13 +813,14 @@ const HTMLEditor = ({
319
813
  switchDevice,
320
814
  toggleContentSync,
321
815
  getDeviceContent,
322
- layoutType
323
- })
816
+ layoutType,
817
+ }),
324
818
  }), [
325
819
  variant,
326
820
  content,
327
821
  layout,
328
822
  validation,
823
+ isLiquidEnabled,
329
824
  getActiveEditorRef,
330
825
  handleLabelInsert,
331
826
  handleSave,
@@ -336,7 +831,7 @@ const HTMLEditor = ({
336
831
  switchDevice,
337
832
  toggleContentSync,
338
833
  getDeviceContent,
339
- layoutType
834
+ layoutType,
340
835
  ]);
341
836
 
342
837
  // Loading state
@@ -348,9 +843,14 @@ const HTMLEditor = ({
348
843
  );
349
844
  }
350
845
 
846
+ // Add library-mode class when not in full mode
847
+ // Note: isFullMode defaults to true, so library mode is when isFullMode === false
848
+ const isLibraryMode = isFullMode === false;
849
+ const editorClassName = `html-editor html-editor--${variant} ${isLibraryMode ? 'html-editor--library-mode' : ''} ${className}`.trim();
850
+
351
851
  return (
352
852
  <EditorProvider value={contextValue}>
353
- <div className={`html-editor html-editor--${variant} ${className}`} {...props}>
853
+ <div className={editorClassName} {...props}>
354
854
  {/* Editor Toolbar - Conditional based on variant */}
355
855
  {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
356
856
  <EditorToolbar
@@ -387,19 +887,23 @@ const HTMLEditor = ({
387
887
  ref={mainEditorRef}
388
888
  readOnly={readOnly}
389
889
  onLabelInsert={handleLabelInsert}
890
+ onErrorClick={handleValidationErrorClick}
891
+ tags={tags}
892
+ injectedTags={injectedTags}
893
+ location={location}
894
+ eventContextTags={eventContextTags}
895
+ selectedOfferDetails={selectedOfferDetails}
896
+ channel={channel}
897
+ userLocale={userLocale}
898
+ moduleFilterEnabled={moduleFilterEnabled}
899
+ onTagContextChange={onTagContextChange}
900
+ onTagSelect={onTagSelect}
901
+ onContextChange={handleContextChange}
390
902
  />
391
903
 
392
904
  {/* Preview Pane */}
393
905
  <PreviewPane />
394
906
  </SplitContainer>
395
-
396
- {/* Validation Display - Full Width Below Split Container */}
397
- <ValidationErrorDisplay
398
- validation={validation}
399
- onErrorClick={handleValidationErrorClick}
400
- variant={variant}
401
- className="html-editor-validation"
402
- />
403
907
  </CapRow>
404
908
 
405
909
  {/* Fullscreen Modal */}
@@ -411,17 +915,17 @@ const HTMLEditor = ({
411
915
  maskClosable={false}
412
916
  centered
413
917
  closable={false}
414
- width={"90vw"}
918
+ width="90vw"
415
919
  className="html-editor-fullscreen-modal"
416
920
  >
417
- <CapRow className="html-editor-fullscreen">
921
+ <CapRow className={`html-editor-fullscreen html-editor-fullscreen--${variant} ${isLibraryMode ? 'html-editor-fullscreen--library-mode' : ''}`}>
418
922
  {/* Editor Toolbar - Conditional based on variant */}
419
923
  {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
420
924
  <EditorToolbar
421
- showFullscreenButton={true} // Show fullscreen button in modal to allow closing
925
+ showFullscreenButton // Show fullscreen button in modal to allow closing
422
926
  onLabelInsert={handleLabelInsert}
423
927
  onSave={handleSave}
424
- isFullscreenMode={true}
928
+ isFullscreenMode
425
929
  onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
426
930
  />
427
931
  ) : (
@@ -434,10 +938,10 @@ const HTMLEditor = ({
434
938
  onKeepContentSameChange={toggleContentSync}
435
939
  />
436
940
  <EditorToolbar
437
- showFullscreenButton={true} // Show fullscreen button in modal to allow closing
941
+ showFullscreenButton // Show fullscreen button in modal to allow closing
438
942
  onLabelInsert={handleLabelInsert}
439
943
  onSave={handleSave}
440
- isFullscreenMode={true}
944
+ isFullscreenMode
441
945
  onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
442
946
  variant={variant}
443
947
  showTitle={false} // Hide title in InApp variant
@@ -452,28 +956,30 @@ const HTMLEditor = ({
452
956
  <CodeEditorPane
453
957
  ref={modalEditorRef}
454
958
  readOnly={readOnly}
455
- isFullscreenMode={true}
959
+ isFullscreenMode
456
960
  onLabelInsert={handleLabelInsert}
961
+ onErrorClick={handleValidationErrorClick}
962
+ tags={tags}
963
+ injectedTags={injectedTags}
964
+ location={location}
965
+ eventContextTags={eventContextTags}
966
+ selectedOfferDetails={selectedOfferDetails}
967
+ channel={channel}
968
+ userLocale={userLocale}
969
+ moduleFilterEnabled={moduleFilterEnabled}
970
+ onTagContextChange={onTagContextChange}
457
971
  />
458
972
 
459
- {/* Preview Pane */}
460
- <PreviewPane isFullscreenMode={true} isModalContext={true} />
461
- </SplitContainer>
462
-
463
- {/* Validation Display in Modal */}
464
- <ValidationErrorDisplay
465
- validation={validation}
466
- onErrorClick={handleValidationErrorClick}
467
- variant={variant}
468
- className="html-editor-validation"
469
- />
973
+ {/* Preview Pane */}
974
+ <PreviewPane isFullscreenMode isModalContext />
975
+ </SplitContainer>
470
976
  </CapRow>
471
977
  </CapRow>
472
978
  </CapModal>
473
979
  </div>
474
980
  </EditorProvider>
475
981
  );
476
- };
982
+ });
477
983
 
478
984
  HTMLEditor.propTypes = {
479
985
  intl: intlShape.isRequired,
@@ -481,7 +987,7 @@ HTMLEditor.propTypes = {
481
987
  layoutType: PropTypes.string, // Layout type for InApp variant
482
988
  initialContent: PropTypes.oneOfType([
483
989
  PropTypes.string,
484
- PropTypes.objectOf(PropTypes.string) // Per-device content for INAPP variant
990
+ PropTypes.objectOf(PropTypes.string), // Per-device content for INAPP variant
485
991
  ]),
486
992
  onSave: PropTypes.func,
487
993
  onContentChange: PropTypes.func,
@@ -489,11 +995,33 @@ HTMLEditor.propTypes = {
489
995
  readOnly: PropTypes.bool,
490
996
  showFullscreenButton: PropTypes.bool,
491
997
  autoSave: PropTypes.bool,
492
- autoSaveInterval: PropTypes.number
998
+ autoSaveInterval: PropTypes.number,
999
+ // Tag-related props - tags are fetched and managed by parent component
1000
+ tags: PropTypes.array,
1001
+ injectedTags: PropTypes.object,
1002
+ location: PropTypes.object,
1003
+ eventContextTags: PropTypes.array,
1004
+ selectedOfferDetails: PropTypes.array,
1005
+ channel: PropTypes.string,
1006
+ userLocale: PropTypes.string,
1007
+ moduleFilterEnabled: PropTypes.bool,
1008
+ onTagContextChange: PropTypes.func, // Required - parent must handle tag fetching
1009
+ onTagSelect: PropTypes.func,
1010
+ onContextChange: PropTypes.func, // Deprecated: use globalActions instead
1011
+ globalActions: PropTypes.object,
1012
+ isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
1013
+ isFullMode: PropTypes.bool, // Full mode vs library mode
1014
+ onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
1015
+ onValidationChange: PropTypes.func, // Callback when validation state changes
1016
+ apiValidationErrors: PropTypes.shape({
1017
+ liquidErrors: PropTypes.arrayOf(PropTypes.string),
1018
+ standardErrors: PropTypes.arrayOf(PropTypes.string),
1019
+ }), // API validation errors from validateLiquidTemplateContent
493
1020
  };
494
1021
 
495
1022
  HTMLEditor.defaultProps = {
496
1023
  variant: HTML_EDITOR_VARIANTS.EMAIL, // Default to email variant
1024
+ layoutType: null,
497
1025
  initialContent: null, // Will use default from useEditorContent hook
498
1026
  onSave: null,
499
1027
  onContentChange: null,
@@ -501,8 +1029,26 @@ HTMLEditor.defaultProps = {
501
1029
  readOnly: false,
502
1030
  showFullscreenButton: true,
503
1031
  autoSave: true,
504
- autoSaveInterval: 30000
1032
+ autoSaveInterval: 30000,
1033
+ // Tag-related defaults - tags are fetched and managed by parent component
1034
+ tags: [],
1035
+ injectedTags: {},
1036
+ location: null,
1037
+ eventContextTags: [],
1038
+ selectedOfferDetails: [],
1039
+ channel: null,
1040
+ userLocale: 'en',
1041
+ moduleFilterEnabled: true,
1042
+ onTagContextChange: null, // Parent component should provide this
1043
+ onTagSelect: null,
1044
+ onContextChange: null,
1045
+ globalActions: null, // Redux actions for API calls
1046
+ isLiquidEnabled: false,
1047
+ isFullMode: true, // Default to full mode
1048
+ onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
1049
+ onValidationChange: null, // Callback when validation state changes
1050
+ apiValidationErrors: null, // API validation errors from validateLiquidTemplateContent
505
1051
  };
506
1052
 
507
- // Export with forwardRef to allow direct access to CodeEditorPane via ref
508
- export default injectIntl(HTMLEditor, { forwardRef: true });
1053
+ // Export with injectIntl - HTMLEditor now uses forwardRef internally
1054
+ export default injectIntl(HTMLEditor);