@capillarytech/creatives-library 8.0.271 → 8.0.273

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 (153) 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/imageUrlUpload.js +13 -14
  14. package/utils/tests/commonUtil.test.js +224 -0
  15. package/utils/tests/imageUrlUpload.test.js +298 -0
  16. package/utils/transformTemplateConfig.js +0 -10
  17. package/v2Components/CapDeviceContent/index.js +61 -56
  18. package/v2Components/CapTagList/index.js +6 -1
  19. package/v2Components/CapTagListWithInput/index.js +5 -1
  20. package/v2Components/CapTagListWithInput/messages.js +1 -1
  21. package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
  22. package/v2Components/ErrorInfoNote/constants.js +1 -0
  23. package/v2Components/ErrorInfoNote/index.js +402 -72
  24. package/v2Components/ErrorInfoNote/messages.js +32 -6
  25. package/v2Components/ErrorInfoNote/style.scss +278 -6
  26. package/v2Components/FormBuilder/tests/index.test.js +13 -4
  27. package/v2Components/HtmlEditor/HTMLEditor.js +418 -99
  28. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +870 -0
  29. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1882 -133
  30. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
  31. package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
  32. package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
  33. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +23 -102
  34. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -140
  35. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
  36. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
  37. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -1
  38. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +31 -6
  39. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
  40. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
  41. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
  42. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
  43. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
  44. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
  45. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +7 -10
  46. package/v2Components/HtmlEditor/components/PreviewPane/index.js +22 -43
  47. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
  48. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +18 -0
  49. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +36 -31
  50. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +46 -34
  51. package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
  52. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +52 -46
  53. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +277 -0
  54. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +295 -0
  55. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
  56. package/v2Components/HtmlEditor/constants.js +45 -20
  57. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
  58. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +351 -16
  59. package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
  60. package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
  61. package/v2Components/HtmlEditor/hooks/useValidation.js +213 -56
  62. package/v2Components/HtmlEditor/index.js +1 -1
  63. package/v2Components/HtmlEditor/messages.js +102 -94
  64. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +214 -45
  65. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +134 -0
  66. package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
  67. package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
  68. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +158 -124
  69. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
  70. package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
  71. package/v2Components/HtmlEditor/utils/validationConstants.js +38 -0
  72. package/v2Components/MobilePushPreviewV2/constants.js +6 -0
  73. package/v2Components/MobilePushPreviewV2/index.js +33 -7
  74. package/v2Components/TemplatePreview/_templatePreview.scss +55 -24
  75. package/v2Components/TemplatePreview/index.js +47 -32
  76. package/v2Components/TemplatePreview/messages.js +4 -0
  77. package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
  78. package/v2Containers/BeeEditor/index.js +172 -90
  79. package/v2Containers/BeePopupEditor/_beePopupEditor.scss +14 -0
  80. package/v2Containers/BeePopupEditor/constants.js +10 -0
  81. package/v2Containers/BeePopupEditor/index.js +194 -0
  82. package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
  83. package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
  84. package/v2Containers/CreativesContainer/SlideBoxFooter.js +156 -13
  85. package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
  86. package/v2Containers/CreativesContainer/constants.js +1 -0
  87. package/v2Containers/CreativesContainer/index.js +251 -47
  88. package/v2Containers/CreativesContainer/messages.js +8 -0
  89. package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
  90. package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
  91. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +103 -0
  92. package/v2Containers/Email/actions.js +7 -0
  93. package/v2Containers/Email/constants.js +5 -1
  94. package/v2Containers/Email/index.js +234 -29
  95. package/v2Containers/Email/messages.js +32 -0
  96. package/v2Containers/Email/reducer.js +12 -1
  97. package/v2Containers/Email/sagas.js +61 -7
  98. package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
  99. package/v2Containers/Email/tests/reducer.test.js +46 -0
  100. package/v2Containers/Email/tests/sagas.test.js +320 -29
  101. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1246 -0
  102. package/v2Containers/EmailWrapper/components/EmailWrapperView.js +212 -21
  103. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
  104. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +2614 -0
  105. package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
  106. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
  107. package/v2Containers/EmailWrapper/constants.js +2 -0
  108. package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +627 -79
  109. package/v2Containers/EmailWrapper/index.js +103 -23
  110. package/v2Containers/EmailWrapper/messages.js +65 -1
  111. package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +955 -0
  112. package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +596 -82
  113. package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
  114. package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
  115. package/v2Containers/InApp/actions.js +7 -0
  116. package/v2Containers/InApp/constants.js +20 -4
  117. package/v2Containers/InApp/index.js +802 -360
  118. package/v2Containers/InApp/index.scss +4 -3
  119. package/v2Containers/InApp/messages.js +7 -3
  120. package/v2Containers/InApp/reducer.js +21 -3
  121. package/v2Containers/InApp/sagas.js +29 -9
  122. package/v2Containers/InApp/selectors.js +25 -5
  123. package/v2Containers/InApp/tests/index.test.js +154 -50
  124. package/v2Containers/InApp/tests/reducer.test.js +34 -0
  125. package/v2Containers/InApp/tests/sagas.test.js +61 -9
  126. package/v2Containers/InApp/tests/selectors.test.js +612 -0
  127. package/v2Containers/InAppWrapper/components/InAppWrapperView.js +151 -0
  128. package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
  129. package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -0
  130. package/v2Containers/InAppWrapper/constants.js +16 -0
  131. package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
  132. package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
  133. package/v2Containers/InAppWrapper/index.js +148 -0
  134. package/v2Containers/InAppWrapper/messages.js +49 -0
  135. package/v2Containers/InappAdvance/index.js +1099 -0
  136. package/v2Containers/InappAdvance/index.scss +10 -0
  137. package/v2Containers/InappAdvance/tests/index.test.js +448 -0
  138. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
  139. package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
  140. package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
  141. package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
  142. package/v2Containers/MobilePush/Create/index.js +1 -1
  143. package/v2Containers/MobilePush/Edit/index.js +10 -6
  144. package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
  145. package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
  146. package/v2Containers/TagList/index.js +62 -19
  147. package/v2Containers/Templates/_templates.scss +60 -1
  148. package/v2Containers/Templates/index.js +89 -4
  149. package/v2Containers/Templates/messages.js +4 -0
  150. package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
  151. package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
  152. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
  153. 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,11 @@ 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
+ BLOCKING_ERROR_RULE_IDS,
45
+ VALIDATION_SEVERITY,
46
+ } from './constants';
44
47
 
45
48
  // Styles
46
49
  import './_htmlEditor.scss';
@@ -48,8 +51,33 @@ import './components/FullscreenModal/_fullscreenModal.scss';
48
51
 
49
52
  // Messages
50
53
  import messages from './messages';
54
+ import { ISSUE_SOURCES } from './utils/validationConstants';
55
+
56
+ /** Check if an issue is a blocking error (Errors tab). Non-blocking = Warnings. */
57
+ const isBlockingError = (issue) => {
58
+ const { rule, source, severity } = issue || {};
59
+ if (rule === 'liquid-api-validation' || rule === 'standard-api-validation') return true;
60
+ if (source === ISSUE_SOURCES.LIQUID && severity === VALIDATION_SEVERITY.ERROR) return true;
61
+ if (BLOCKING_ERROR_RULE_IDS.includes(rule)) return true;
62
+ return false;
63
+ };
64
+
65
+ /** Count issues as Errors (blocking) and Warnings (non-blocking). */
66
+ const countIssuesBySeverity = (allIssues) => {
67
+ let errors = 0;
68
+ let warnings = 0;
69
+ (allIssues || []).forEach((issue) => {
70
+ if (isBlockingError(issue)) errors += 1;
71
+ else warnings += 1;
72
+ });
73
+ return {
74
+ errors,
75
+ warnings,
76
+ total: (allIssues || []).length,
77
+ };
78
+ };
51
79
 
52
- const HTMLEditor = ({
80
+ const HTMLEditor = forwardRef(({
53
81
  intl,
54
82
  variant = HTML_EDITOR_VARIANTS.EMAIL, // New prop: 'email' or 'inapp'
55
83
  layoutType, // Layout type for InApp variant
@@ -61,17 +89,33 @@ const HTMLEditor = ({
61
89
  showFullscreenButton = true,
62
90
  autoSave = true,
63
91
  autoSaveInterval = 30000, // 30 seconds
92
+ // Tag-related props - tags are fetched and managed by parent component (EmailHTMLEditor, INAPP, etc.)
93
+ tags = [],
94
+ injectedTags = {},
95
+ location,
96
+ eventContextTags = [],
97
+ selectedOfferDetails = [],
98
+ channel,
99
+ userLocale = 'en',
100
+ moduleFilterEnabled = true,
101
+ onTagContextChange, // Parent component handles tag fetching
102
+ onTagSelect = null,
103
+ onContextChange = null,
104
+ globalActions = null,
105
+ isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
106
+ isFullMode = true, // Full mode vs library mode - controls layout and visibility
107
+ onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
108
+ onValidationChange = null, // Callback when validation state changes (for parent to track errors)
109
+ apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent { liquidErrors: [], standardErrors: [] }
64
110
  ...props
65
- }) => {
111
+ }, ref) => {
66
112
  // Separate refs for main and modal editors to avoid conflicts
67
113
  const mainEditorRef = useRef(null);
68
114
  const modalEditorRef = useRef(null);
69
115
  const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
70
116
 
71
117
  // Get the currently active editor ref based on fullscreen state
72
- const getActiveEditorRef = useCallback(() => {
73
- return isFullscreenModalOpen ? modalEditorRef : mainEditorRef;
74
- }, [isFullscreenModalOpen]);
118
+ const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
75
119
 
76
120
  // Initialize custom hooks for state management - always call both hooks to follow Rules of Hooks
77
121
  const isEmailVariant = variant === HTML_EDITOR_VARIANTS.EMAIL;
@@ -80,7 +124,7 @@ const HTMLEditor = ({
80
124
  autoSave: isEmailVariant ? autoSave : false,
81
125
  autoSaveInterval,
82
126
  onSave: isEmailVariant ? onSave : null,
83
- onChange: isEmailVariant ? onContentChange : null
127
+ onChange: isEmailVariant ? onContentChange : null,
84
128
  };
85
129
 
86
130
  const emailContent = useEditorContent(
@@ -97,7 +141,7 @@ const HTMLEditor = ({
97
141
  // Convert string content to device-specific format
98
142
  inAppInitialContent = {
99
143
  [DEVICE_TYPES.ANDROID]: initialContent,
100
- [DEVICE_TYPES.IOS]: initialContent
144
+ [DEVICE_TYPES.IOS]: initialContent,
101
145
  };
102
146
  } else {
103
147
  // Use provided device-specific content
@@ -109,7 +153,7 @@ const HTMLEditor = ({
109
153
  autoSave: isInAppVariant ? autoSave : false,
110
154
  autoSaveInterval,
111
155
  onSave: isInAppVariant ? onSave : null,
112
- onChange: isInAppVariant ? onContentChange : null
156
+ onChange: isInAppVariant ? onContentChange : null,
113
157
  };
114
158
 
115
159
  const inAppContent = useInAppContent(inAppInitialContent, inAppOptions);
@@ -117,6 +161,64 @@ const HTMLEditor = ({
117
161
  // Use appropriate content hook based on variant
118
162
  const content = variant === HTML_EDITOR_VARIANTS.EMAIL ? emailContent : inAppContent;
119
163
 
164
+ // Update content when initialContent prop changes (for edit mode)
165
+ // This ensures the editor updates when template data loads
166
+ useEffect(() => {
167
+ if (isEmailVariant && emailContent && initialContent !== undefined && initialContent !== null) {
168
+ // Only update if content is different to avoid unnecessary updates
169
+ if (emailContent.content !== initialContent) {
170
+ emailContent.updateContent(initialContent, true); // immediate update
171
+ }
172
+ } else if (isInAppVariant && inAppContent && initialContent !== undefined && initialContent !== null) {
173
+ // Handle InApp variant updates
174
+ const contentToUpdate = typeof initialContent === 'string'
175
+ ? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
176
+ : initialContent;
177
+ if (inAppContent.updateContent) {
178
+ const currentContent = inAppContent.getDeviceContent?.(inAppContent.activeDevice);
179
+ const newContent = contentToUpdate[inAppContent.activeDevice] || contentToUpdate[DEVICE_TYPES.ANDROID] || '';
180
+ if (currentContent !== newContent) {
181
+ inAppContent.updateContent(newContent, true);
182
+ }
183
+ }
184
+ }
185
+ }, [initialContent, isEmailVariant, isInAppVariant]);
186
+ // Handle context change for tag API calls
187
+ // If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
188
+ const handleContextChange = useCallback((contextData) => {
189
+ // If onContextChange is provided, use it instead of making our own API call
190
+ // This prevents duplicate API calls when parent component handles tag fetching
191
+ if (onContextChange) {
192
+ onContextChange(contextData);
193
+ return;
194
+ }
195
+
196
+ // Only make API call if onContextChange is not provided and globalActions is available
197
+ if (!globalActions || !location) {
198
+ return;
199
+ }
200
+
201
+ const { type } = location.query || {};
202
+ const tempData = (contextData || '').toLowerCase();
203
+ const isEmbedded = type === EMBEDDED;
204
+ const embedded = isEmbedded ? type : FULL;
205
+ const context = tempData === ALL ? DEFAULT : tempData;
206
+
207
+ // Determine layout: INAPP variant uses SMS, EMAIL variant uses EMAIL
208
+ const layout = variant === HTML_EDITOR_VARIANTS.INAPP ? SMS : EMAIL;
209
+
210
+ const query = {
211
+ layout,
212
+ type: TAG,
213
+ context,
214
+ embedded,
215
+ };
216
+
217
+ // Call the API via Redux action - this will trigger the saga which calls Api.fetchSchemaForEntity
218
+ // The API endpoint will be: /meta/TAG?query={...}
219
+ globalActions.fetchSchemaForEntity(query);
220
+ }, [variant, globalActions, location, onContextChange]);
221
+
120
222
  // Destructure content properties for cleaner access throughout component
121
223
  const {
122
224
  activeDevice,
@@ -124,14 +226,14 @@ const HTMLEditor = ({
124
226
  switchDevice,
125
227
  toggleContentSync,
126
228
  getDeviceContent,
127
- markAsSaved
229
+ markAsSaved,
128
230
  } = content || {};
129
231
 
130
232
  const layout = useLayoutState({
131
233
  splitSizes: [50, 50],
132
234
  viewMode: 'desktop',
133
235
  mobileWidth: 375,
134
- isFullscreen: false
236
+ isFullscreen: false,
135
237
  });
136
238
 
137
239
  // Get current content for validation based on variant
@@ -158,7 +260,7 @@ const HTMLEditor = ({
158
260
  'sanitizer.productionValidHtml': messages.sanitizer.productionValidHtml,
159
261
  'sanitizer.productionSanitized': messages.sanitizer.productionSanitized,
160
262
  'sanitizer.productionInlineCss': messages.sanitizer.productionInlineCss,
161
- 'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent
263
+ 'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent,
162
264
  };
163
265
 
164
266
  const messageObj = messageMap[messageKey];
@@ -179,7 +281,7 @@ const HTMLEditor = ({
179
281
  'validator.largeImageDetected': messages.validator.largeImageDetected,
180
282
  'validator.unclosedCssRule': messages.validator.unclosedCssRule,
181
283
  'validator.emptyCssRule': messages.validator.emptyCssRule,
182
- 'validator.cssValidationFailed': messages.validator.cssValidationFailed
284
+ 'validator.cssValidationFailed': messages.validator.cssValidationFailed,
183
285
  };
184
286
 
185
287
  const messageObj = messageMap[messageKey];
@@ -190,57 +292,215 @@ const HTMLEditor = ({
190
292
  enableRealTime: true,
191
293
  debounceMs: 500,
192
294
  enableSanitization: true,
193
- securityLevel: 'standard'
295
+ securityLevel: 'standard',
296
+ apiValidationErrors, // Pass API validation errors to merge with client-side validation
194
297
  }, formatSanitizerMessage, formatValidatorMessage);
195
298
 
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;
299
+ // Expose validation and content state via ref
300
+ useImperativeHandle(ref, () => ({
301
+ getValidation: () => validation,
302
+ getContent: () => currentContent,
303
+ isContentEmpty: () => !currentContent || currentContent.trim() === '',
304
+ getIssueCounts: () => {
305
+ if (!validation || typeof validation.getAllIssues !== 'function') {
306
+ return { errors: 0, warnings: 0, total: 0 };
307
+ }
308
+ return countIssuesBySeverity(validation.getAllIssues());
309
+ },
310
+ getValidationState: () => ({
311
+ isValidating: validation?.isValidating || false,
312
+ hasErrors: validation?.hasBlockingErrors || false,
313
+ issueCounts: !validation || typeof validation.getAllIssues !== 'function'
314
+ ? { errors: 0, warnings: 0, total: 0 }
315
+ : countIssuesBySeverity(validation.getAllIssues()),
316
+ })
317
+ ,
318
+ }), [validation, currentContent, apiValidationErrors]); // Include apiValidationErrors so ref methods return updated counts
319
+
320
+ // Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
321
+ const onValidationChangeRef = useRef(onValidationChange);
322
+ useEffect(() => {
323
+ onValidationChangeRef.current = onValidationChange;
324
+ }, [onValidationChange]);
325
+
326
+ // Track last sent validation state to prevent duplicate updates
327
+ const lastSentValidationStateRef = useRef(null);
328
+
329
+ // Store validation ref to access current value without triggering re-renders
330
+ const validationRef = useRef(validation);
331
+ validationRef.current = validation;
332
+
333
+ // Extract STABLE primitive values from validation for dependency array
334
+ const isValidating = validation?.isValidating;
335
+ const validationTotalErrors = validation?.summary?.totalErrors || 0;
336
+ const validationTotalWarnings = validation?.summary?.totalWarnings || 0;
337
+ const validationLastValidated = validation?.lastValidated?.getTime?.() || null;
338
+ // Only Rule Group #1 (Input & Sanitization) blocks; use for UI gating (Done/Update/Preview/Test)
339
+ const validationHasBlockingErrors = validation?.hasBlockingErrors || false;
340
+
341
+ // Notify parent component when validation state changes
342
+ useEffect(() => {
343
+ if (!onValidationChangeRef.current) {
344
+ return;
345
+ }
201
346
 
202
- if (!editor) {
203
- CapNotification.warning({
204
- message: intl.formatMessage(messages.labelInsertError),
205
- description: intl.formatMessage(messages.editorNotReady),
206
- duration: 3
207
- });
347
+ // Skip if still validating (wait for validation to complete)
348
+ if (isValidating) {
208
349
  return;
209
350
  }
210
351
 
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
217
- });
352
+ // Calculate issue counts: Errors (blocking) and Warnings (non-blocking)
353
+ const calculateIssueCounts = () => {
354
+ const currentValidation = validationRef.current;
355
+ if (!currentValidation || typeof currentValidation.getAllIssues !== 'function') {
356
+ return { errors: 0, warnings: 0, total: 0 };
357
+ }
358
+ return countIssuesBySeverity(currentValidation.getAllIssues());
359
+ };
360
+
361
+ const issueCounts = calculateIssueCounts();
362
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
363
+
364
+ // hasErrors = only Rule Group #1 (Input & Sanitization) – gates Done/Update/Preview/Test
365
+ const newState = {
366
+ isContentEmpty,
367
+ issueCounts,
368
+ validationComplete: true,
369
+ hasErrors: validationRef.current?.hasBlockingErrors ?? false,
370
+ };
371
+
372
+ // Only call callback if state actually changed (prevent infinite loops)
373
+ const lastState = lastSentValidationStateRef.current;
374
+ const hasChanged = !lastState
375
+ || lastState.isContentEmpty !== newState.isContentEmpty
376
+ || lastState.validationComplete !== newState.validationComplete
377
+ || lastState.hasErrors !== newState.hasErrors
378
+ || lastState.issueCounts?.total !== newState.issueCounts?.total
379
+ || lastState.issueCounts?.errors !== newState.issueCounts?.errors
380
+ || lastState.issueCounts?.warnings !== newState.issueCounts?.warnings;
381
+
382
+ if (hasChanged) {
383
+ lastSentValidationStateRef.current = newState;
384
+ onValidationChangeRef.current(newState);
385
+ }
386
+ }, [isValidating, validationTotalErrors, validationTotalWarnings, validationLastValidated, validationHasBlockingErrors, currentContent, apiValidationErrors]);
387
+
388
+ // Send initial state on mount to ensure parent has correct initial button state
389
+ const hasInitializedRef = useRef(false);
390
+ useEffect(() => {
391
+ if (hasInitializedRef.current || !onValidationChangeRef.current) {
392
+ return;
393
+ }
394
+ hasInitializedRef.current = true;
395
+
396
+ // Send initial state with validationComplete=false to indicate validation pending
397
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
398
+ onValidationChangeRef.current({
399
+ isContentEmpty,
400
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
401
+ validationComplete: false, // Validation hasn't run yet
402
+ hasErrors: false,
403
+ });
404
+ }, [currentContent]); // Only depend on currentContent to run on initial content load
405
+
406
+ // Force validation state recalculation when API validation errors change
407
+ // This ensures that API errors are included in issue counts and displayed in ValidationErrorDisplay
408
+ useEffect(() => {
409
+ if (!onValidationChangeRef.current) {
218
410
  return;
219
411
  }
220
412
 
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);
413
+ // Skip if still validating (wait for validation to complete)
414
+ if (validation?.isValidating) {
415
+ return;
416
+ }
417
+
418
+ // Recalculate issue counts (Errors + Warnings) including API errors
419
+ const issueCounts = !validation || typeof validation.getAllIssues !== 'function'
420
+ ? { errors: 0, warnings: 0, total: 0 }
421
+ : countIssuesBySeverity(validation.getAllIssues());
422
+ const isContentEmpty = !currentContent || currentContent.trim() === '';
423
+
424
+ const newState = {
425
+ isContentEmpty,
426
+ issueCounts,
427
+ validationComplete: true,
428
+ hasErrors: validation?.hasBlockingErrors ?? false,
429
+ };
226
430
 
227
- // Insert label at cursor position
228
- editor.insertText(label, cursor);
431
+ const lastState = lastSentValidationStateRef.current;
432
+ const hasChanged = !lastState
433
+ || lastState.hasErrors !== newState.hasErrors
434
+ || lastState.issueCounts?.total !== newState.issueCounts?.total
435
+ || lastState.issueCounts?.errors !== newState.issueCounts?.errors
436
+ || lastState.issueCounts?.warnings !== newState.issueCounts?.warnings;
229
437
 
230
- // Focus the editor if focus method is available
231
- editor?.focus?.();
438
+ if (hasChanged) {
439
+ lastSentValidationStateRef.current = newState;
440
+ onValidationChangeRef.current(newState);
441
+ }
442
+ }, [apiValidationErrors, validation, currentContent, onValidationChangeRef]);
443
+
444
+ // Handle label insertion at cursor position
445
+ // Note: This is called for notification purposes only when tag is inserted via CodeEditorPane
446
+ // The actual insertion happens in CodeEditorPane.handleTagSelect
447
+ const handleLabelInsert = useCallback((label, position) => {
448
+ // If position is explicitly null, it means the editor wasn't ready when tag was selected
449
+ // In this case, CodeEditorPane couldn't insert the tag, so we should try here
450
+ if (position === null) {
451
+ // With injectIntl({ forwardRef: true }), ref points directly to CodeEditorPane
452
+ const activeEditorRef = getActiveEditorRef();
453
+ const editor = activeEditorRef.current;
454
+
455
+ if (!editor) {
456
+ CapNotification.warning({
457
+ message: intl.formatMessage(messages.labelInsertError),
458
+ description: intl.formatMessage(messages.editorNotReady),
459
+ duration: 3,
460
+ });
461
+ return;
462
+ }
232
463
 
233
- // Show success notification
464
+ // Check if the required methods exist
465
+ if (typeof editor?.insertText !== 'function') {
466
+ CapNotification.error({
467
+ message: intl.formatMessage(messages.labelInsertError),
468
+ description: intl.formatMessage(messages.editorMethodNotAvailable),
469
+ duration: 4,
470
+ });
471
+ return;
472
+ }
473
+
474
+ try {
475
+ // Get current cursor position
476
+ const cursor = typeof editor?.getCursor === 'function' ? editor.getCursor() : 0;
477
+
478
+ // Insert label at cursor position
479
+ editor.insertText(label, cursor);
480
+
481
+ // Focus the editor if focus method is available
482
+ editor?.focus?.();
483
+
484
+ // Show success notification
485
+ CapNotification.success({
486
+ message: intl.formatMessage(messages.labelInserted),
487
+ description: intl.formatMessage(messages.labelInsertedDescription, { label }),
488
+ duration: 2,
489
+ });
490
+ } catch (error) {
491
+ CapNotification.error({
492
+ message: intl.formatMessage(messages.labelInsertError),
493
+ description: error.message,
494
+ duration: 4,
495
+ });
496
+ }
497
+ } else {
498
+ // Tag was already inserted by CodeEditorPane (position is a valid number)
499
+ // Just show success notification - no need to access editor
234
500
  CapNotification.success({
235
501
  message: intl.formatMessage(messages.labelInserted),
236
502
  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
503
+ duration: 2,
244
504
  });
245
505
  }
246
506
  }, [intl, getActiveEditorRef]);
@@ -249,23 +509,23 @@ const HTMLEditor = ({
249
509
  const handleSave = useCallback(() => {
250
510
  try {
251
511
  const { html, css, javascript } = content?.content || {};
252
- const currentContent = { html, css, javascript };
512
+ const contentToSave = { html, css, javascript };
253
513
 
254
514
  if (onSave) {
255
- onSave(currentContent);
515
+ onSave(contentToSave);
256
516
  }
257
517
 
258
518
  markAsSaved?.();
259
519
 
260
520
  CapNotification.success({
261
521
  message: intl.formatMessage(messages.contentSaved),
262
- duration: 2
522
+ duration: 2,
263
523
  });
264
524
  } catch (error) {
265
525
  CapNotification.error({
266
526
  message: intl.formatMessage(messages.saveError),
267
527
  description: error.message,
268
- duration: 4
528
+ duration: 4,
269
529
  });
270
530
  }
271
531
  }, [content, onSave, intl, markAsSaved]);
@@ -276,21 +536,27 @@ const HTMLEditor = ({
276
536
  const editorInstance = activeEditorRef.current;
277
537
  const { line, column = 1 } = error || {};
278
538
 
279
- if (editorInstance && line) {
539
+ // Notify parent that user acknowledged errors by clicking redirection icon
540
+ // This enables the buttons even if we can't navigate to a specific line
541
+ if (onErrorAcknowledged) {
542
+ onErrorAcknowledged();
543
+ }
544
+
545
+ if (editorInstance) {
280
546
  try {
281
- // Access the CodeMirror view through the exposed ref methods
282
- if (editorInstance?.navigateToLine) {
547
+ // If line number exists, navigate to it; otherwise just focus the editor
548
+ if (line && editorInstance?.navigateToLine) {
283
549
  editorInstance.navigateToLine(line, column);
284
550
  } else {
551
+ // For API errors without line numbers, just focus the editor
285
552
  editorInstance?.focus?.();
286
- // Fallback: just focus the editor if navigation isn't available
287
553
  }
288
554
  } catch (err) {
289
555
  // Fallback: just focus the editor
290
556
  editorInstance?.focus?.();
291
557
  }
292
558
  }
293
- }, [getActiveEditorRef]);
559
+ }, [getActiveEditorRef, onErrorAcknowledged]);
294
560
 
295
561
  // Handle fullscreen modal
296
562
  const handleOpenFullscreen = useCallback(() => {
@@ -307,6 +573,7 @@ const HTMLEditor = ({
307
573
  content,
308
574
  layout,
309
575
  validation,
576
+ isLiquidEnabled,
310
577
  editorRef: getActiveEditorRef(),
311
578
  handleLabelInsert,
312
579
  handleSave,
@@ -319,13 +586,14 @@ const HTMLEditor = ({
319
586
  switchDevice,
320
587
  toggleContentSync,
321
588
  getDeviceContent,
322
- layoutType
323
- })
589
+ layoutType,
590
+ }),
324
591
  }), [
325
592
  variant,
326
593
  content,
327
594
  layout,
328
595
  validation,
596
+ isLiquidEnabled,
329
597
  getActiveEditorRef,
330
598
  handleLabelInsert,
331
599
  handleSave,
@@ -336,7 +604,7 @@ const HTMLEditor = ({
336
604
  switchDevice,
337
605
  toggleContentSync,
338
606
  getDeviceContent,
339
- layoutType
607
+ layoutType,
340
608
  ]);
341
609
 
342
610
  // Loading state
@@ -348,9 +616,14 @@ const HTMLEditor = ({
348
616
  );
349
617
  }
350
618
 
619
+ // Add library-mode class when not in full mode
620
+ // Note: isFullMode defaults to true, so library mode is when isFullMode === false
621
+ const isLibraryMode = isFullMode === false;
622
+ const editorClassName = `html-editor html-editor--${variant} ${isLibraryMode ? 'html-editor--library-mode' : ''} ${className}`.trim();
623
+
351
624
  return (
352
625
  <EditorProvider value={contextValue}>
353
- <div className={`html-editor html-editor--${variant} ${className}`} {...props}>
626
+ <div className={editorClassName} {...props}>
354
627
  {/* Editor Toolbar - Conditional based on variant */}
355
628
  {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
356
629
  <EditorToolbar
@@ -387,19 +660,23 @@ const HTMLEditor = ({
387
660
  ref={mainEditorRef}
388
661
  readOnly={readOnly}
389
662
  onLabelInsert={handleLabelInsert}
663
+ onErrorClick={handleValidationErrorClick}
664
+ tags={tags}
665
+ injectedTags={injectedTags}
666
+ location={location}
667
+ eventContextTags={eventContextTags}
668
+ selectedOfferDetails={selectedOfferDetails}
669
+ channel={channel}
670
+ userLocale={userLocale}
671
+ moduleFilterEnabled={moduleFilterEnabled}
672
+ onTagContextChange={onTagContextChange}
673
+ onTagSelect={onTagSelect}
674
+ onContextChange={handleContextChange}
390
675
  />
391
676
 
392
677
  {/* Preview Pane */}
393
678
  <PreviewPane />
394
679
  </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
680
  </CapRow>
404
681
 
405
682
  {/* Fullscreen Modal */}
@@ -408,20 +685,20 @@ const HTMLEditor = ({
408
685
  visible={isFullscreenModalOpen}
409
686
  onCancel={handleCloseFullscreen}
410
687
  footer={null}
411
- maskClosable={false}
688
+ maskClosable
412
689
  centered
413
690
  closable={false}
414
- width={"90vw"}
691
+ width="93vw"
415
692
  className="html-editor-fullscreen-modal"
416
693
  >
417
- <CapRow className="html-editor-fullscreen">
694
+ <CapRow className={`html-editor-fullscreen html-editor-fullscreen--${variant} ${isLibraryMode ? 'html-editor-fullscreen--library-mode' : ''}`}>
418
695
  {/* Editor Toolbar - Conditional based on variant */}
419
696
  {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
420
697
  <EditorToolbar
421
- showFullscreenButton={true} // Show fullscreen button in modal to allow closing
698
+ showFullscreenButton // Show fullscreen button in modal to allow closing
422
699
  onLabelInsert={handleLabelInsert}
423
700
  onSave={handleSave}
424
- isFullscreenMode={true}
701
+ isFullscreenMode
425
702
  onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
426
703
  />
427
704
  ) : (
@@ -434,10 +711,10 @@ const HTMLEditor = ({
434
711
  onKeepContentSameChange={toggleContentSync}
435
712
  />
436
713
  <EditorToolbar
437
- showFullscreenButton={true} // Show fullscreen button in modal to allow closing
714
+ showFullscreenButton // Show fullscreen button in modal to allow closing
438
715
  onLabelInsert={handleLabelInsert}
439
716
  onSave={handleSave}
440
- isFullscreenMode={true}
717
+ isFullscreenMode
441
718
  onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
442
719
  variant={variant}
443
720
  showTitle={false} // Hide title in InApp variant
@@ -452,28 +729,30 @@ const HTMLEditor = ({
452
729
  <CodeEditorPane
453
730
  ref={modalEditorRef}
454
731
  readOnly={readOnly}
455
- isFullscreenMode={true}
732
+ isFullscreenMode
456
733
  onLabelInsert={handleLabelInsert}
734
+ onErrorClick={handleValidationErrorClick}
735
+ tags={tags}
736
+ injectedTags={injectedTags}
737
+ location={location}
738
+ eventContextTags={eventContextTags}
739
+ selectedOfferDetails={selectedOfferDetails}
740
+ channel={channel}
741
+ userLocale={userLocale}
742
+ moduleFilterEnabled={moduleFilterEnabled}
743
+ onTagContextChange={onTagContextChange}
457
744
  />
458
745
 
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
- />
746
+ {/* Preview Pane */}
747
+ <PreviewPane isFullscreenMode isModalContext />
748
+ </SplitContainer>
470
749
  </CapRow>
471
750
  </CapRow>
472
751
  </CapModal>
473
752
  </div>
474
753
  </EditorProvider>
475
754
  );
476
- };
755
+ });
477
756
 
478
757
  HTMLEditor.propTypes = {
479
758
  intl: intlShape.isRequired,
@@ -481,7 +760,7 @@ HTMLEditor.propTypes = {
481
760
  layoutType: PropTypes.string, // Layout type for InApp variant
482
761
  initialContent: PropTypes.oneOfType([
483
762
  PropTypes.string,
484
- PropTypes.objectOf(PropTypes.string) // Per-device content for INAPP variant
763
+ PropTypes.objectOf(PropTypes.string), // Per-device content for INAPP variant
485
764
  ]),
486
765
  onSave: PropTypes.func,
487
766
  onContentChange: PropTypes.func,
@@ -489,11 +768,33 @@ HTMLEditor.propTypes = {
489
768
  readOnly: PropTypes.bool,
490
769
  showFullscreenButton: PropTypes.bool,
491
770
  autoSave: PropTypes.bool,
492
- autoSaveInterval: PropTypes.number
771
+ autoSaveInterval: PropTypes.number,
772
+ // Tag-related props - tags are fetched and managed by parent component
773
+ tags: PropTypes.array,
774
+ injectedTags: PropTypes.object,
775
+ location: PropTypes.object,
776
+ eventContextTags: PropTypes.array,
777
+ selectedOfferDetails: PropTypes.array,
778
+ channel: PropTypes.string,
779
+ userLocale: PropTypes.string,
780
+ moduleFilterEnabled: PropTypes.bool,
781
+ onTagContextChange: PropTypes.func, // Required - parent must handle tag fetching
782
+ onTagSelect: PropTypes.func,
783
+ onContextChange: PropTypes.func, // Deprecated: use globalActions instead
784
+ globalActions: PropTypes.object,
785
+ isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
786
+ isFullMode: PropTypes.bool, // Full mode vs library mode
787
+ onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
788
+ onValidationChange: PropTypes.func, // Callback when validation state changes
789
+ apiValidationErrors: PropTypes.shape({
790
+ liquidErrors: PropTypes.arrayOf(PropTypes.string),
791
+ standardErrors: PropTypes.arrayOf(PropTypes.string),
792
+ }), // API validation errors from validateLiquidTemplateContent
493
793
  };
494
794
 
495
795
  HTMLEditor.defaultProps = {
496
796
  variant: HTML_EDITOR_VARIANTS.EMAIL, // Default to email variant
797
+ layoutType: null,
497
798
  initialContent: null, // Will use default from useEditorContent hook
498
799
  onSave: null,
499
800
  onContentChange: null,
@@ -501,8 +802,26 @@ HTMLEditor.defaultProps = {
501
802
  readOnly: false,
502
803
  showFullscreenButton: true,
503
804
  autoSave: true,
504
- autoSaveInterval: 30000
805
+ autoSaveInterval: 30000,
806
+ // Tag-related defaults - tags are fetched and managed by parent component
807
+ tags: [],
808
+ injectedTags: {},
809
+ location: null,
810
+ eventContextTags: [],
811
+ selectedOfferDetails: [],
812
+ channel: null,
813
+ userLocale: 'en',
814
+ moduleFilterEnabled: true,
815
+ onTagContextChange: null, // Parent component should provide this
816
+ onTagSelect: null,
817
+ onContextChange: null,
818
+ globalActions: null, // Redux actions for API calls
819
+ isLiquidEnabled: false,
820
+ isFullMode: true, // Default to full mode
821
+ onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
822
+ onValidationChange: null, // Callback when validation state changes
823
+ apiValidationErrors: null, // API validation errors from validateLiquidTemplateContent
505
824
  };
506
825
 
507
- // Export with forwardRef to allow direct access to CodeEditorPane via ref
508
- export default injectIntl(HTMLEditor, { forwardRef: true });
826
+ // Export with injectIntl - HTMLEditor now uses forwardRef internally
827
+ export default injectIntl(HTMLEditor);