@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.
- package/assets/Android.png +0 -0
- package/assets/iOS.png +0 -0
- package/constants/unified.js +2 -1
- package/initialReducer.js +2 -0
- package/package.json +1 -1
- package/services/api.js +10 -0
- package/services/tests/api.test.js +18 -0
- package/utils/common.js +5 -0
- package/utils/commonUtils.js +28 -5
- package/utils/tests/commonUtil.test.js +224 -0
- package/utils/transformTemplateConfig.js +0 -10
- package/v2Components/CapDeviceContent/index.js +61 -56
- package/v2Components/CapTagList/index.js +6 -1
- package/v2Components/CapTagListWithInput/index.js +5 -1
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +5 -0
- package/v2Components/ErrorInfoNote/index.js +452 -72
- package/v2Components/ErrorInfoNote/messages.js +22 -0
- package/v2Components/ErrorInfoNote/style.scss +280 -4
- package/v2Components/FormBuilder/tests/index.test.js +13 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +640 -94
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1167 -133
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
- package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
- package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +13 -101
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +148 -139
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +2 -1
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +9 -0
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +22 -0
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +4 -7
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +35 -45
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +1 -3
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +7 -6
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +3 -6
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +11 -13
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +68 -39
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +391 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +51 -0
- package/v2Components/HtmlEditor/constants.js +42 -20
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +373 -16
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +795 -0
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +5 -2
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +88 -146
- package/v2Components/HtmlEditor/hooks/useValidation.js +189 -53
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +95 -85
- package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +94 -45
- package/v2Components/HtmlEditor/utils/contentSanitizer.js +40 -41
- package/v2Components/HtmlEditor/utils/htmlValidator.js +71 -72
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +134 -102
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +23 -25
- package/v2Components/HtmlEditor/utils/validationAdapter.js +66 -41
- package/v2Components/MobilePushPreviewV2/index.js +32 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +44 -24
- package/v2Components/TemplatePreview/index.js +47 -32
- package/v2Components/TemplatePreview/messages.js +4 -0
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +1 -0
- package/v2Containers/BeeEditor/index.js +172 -90
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +193 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +127 -51
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +163 -13
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +2 -1
- package/v2Containers/CreativesContainer/constants.js +1 -0
- package/v2Containers/CreativesContainer/index.js +239 -46
- package/v2Containers/CreativesContainer/messages.js +8 -0
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +11 -2
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +38 -50
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +106 -0
- package/v2Containers/Email/actions.js +7 -0
- package/v2Containers/Email/constants.js +5 -1
- package/v2Containers/Email/index.js +222 -27
- package/v2Containers/Email/messages.js +32 -0
- package/v2Containers/Email/reducer.js +12 -1
- package/v2Containers/Email/sagas.js +61 -7
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +2 -0
- package/v2Containers/Email/tests/sagas.test.js +320 -29
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1321 -0
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +210 -15
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1749 -0
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +520 -0
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +2 -67
- package/v2Containers/EmailWrapper/constants.js +2 -0
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +629 -77
- package/v2Containers/EmailWrapper/index.js +103 -23
- package/v2Containers/EmailWrapper/messages.js +61 -1
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +643 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +594 -77
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +376 -0
- package/v2Containers/InApp/__tests__/sagas.test.js +363 -0
- package/v2Containers/InApp/actions.js +7 -0
- package/v2Containers/InApp/constants.js +20 -4
- package/v2Containers/InApp/index.js +802 -359
- package/v2Containers/InApp/index.scss +4 -3
- package/v2Containers/InApp/messages.js +7 -3
- package/v2Containers/InApp/reducer.js +21 -3
- package/v2Containers/InApp/sagas.js +29 -9
- package/v2Containers/InApp/selectors.js +25 -5
- package/v2Containers/InApp/tests/index.test.js +154 -50
- package/v2Containers/InApp/tests/reducer.test.js +34 -0
- package/v2Containers/InApp/tests/sagas.test.js +61 -9
- package/v2Containers/InApp/tests/selectors.test.js +612 -0
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +162 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +9 -0
- package/v2Containers/InAppWrapper/constants.js +16 -0
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +473 -0
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +198 -0
- package/v2Containers/InAppWrapper/index.js +148 -0
- package/v2Containers/InAppWrapper/messages.js +49 -0
- package/v2Containers/InappAdvance/index.js +1099 -0
- package/v2Containers/InappAdvance/index.scss +10 -0
- package/v2Containers/InappAdvance/tests/index.test.js +448 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +3 -0
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +2 -0
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +9 -0
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +12 -0
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +4 -0
- package/v2Containers/TagList/index.js +62 -19
- package/v2Containers/Templates/_templates.scss +60 -1
- package/v2Containers/Templates/index.js +89 -4
- package/v2Containers/Templates/messages.js +4 -0
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +34 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +0 -152
- 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, {
|
|
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 {
|
|
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
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
message
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
//
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
228
|
-
|
|
612
|
+
let htmlCount = 0;
|
|
613
|
+
let labelCount = 0;
|
|
614
|
+
let liquidCount = 0;
|
|
229
615
|
|
|
230
|
-
|
|
231
|
-
|
|
616
|
+
allIssues.forEach((issue) => {
|
|
617
|
+
const { source, rule, message } = issue;
|
|
618
|
+
const messageLower = (message || '').toLowerCase();
|
|
619
|
+
const ruleLower = (rule || '').toLowerCase();
|
|
232
620
|
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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={
|
|
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=
|
|
918
|
+
width="90vw"
|
|
415
919
|
className="html-editor-fullscreen-modal"
|
|
416
920
|
>
|
|
417
|
-
<CapRow className=
|
|
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
|
|
925
|
+
showFullscreenButton // Show fullscreen button in modal to allow closing
|
|
422
926
|
onLabelInsert={handleLabelInsert}
|
|
423
927
|
onSave={handleSave}
|
|
424
|
-
isFullscreenMode
|
|
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
|
|
941
|
+
showFullscreenButton // Show fullscreen button in modal to allow closing
|
|
438
942
|
onLabelInsert={handleLabelInsert}
|
|
439
943
|
onSave={handleSave}
|
|
440
|
-
isFullscreenMode
|
|
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
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
508
|
-
export default injectIntl(HTMLEditor
|
|
1053
|
+
// Export with injectIntl - HTMLEditor now uses forwardRef internally
|
|
1054
|
+
export default injectIntl(HTMLEditor);
|