@capillarytech/creatives-library 8.0.254 → 8.0.255-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 +34 -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/constants.js +1 -0
- package/v2Components/ErrorInfoNote/index.js +457 -72
- package/v2Components/ErrorInfoNote/messages.js +36 -6
- package/v2Components/ErrorInfoNote/style.scss +282 -6
- package/v2Components/FormBuilder/tests/index.test.js +13 -4
- package/v2Components/HtmlEditor/HTMLEditor.js +547 -94
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +874 -0
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1358 -133
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +27 -16
- package/v2Components/HtmlEditor/_htmlEditor.scss +108 -45
- package/v2Components/HtmlEditor/_index.lazy.scss +0 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +22 -101
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +149 -140
- 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 +24 -34
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +49 -31
- package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +50 -34
- package/v2Components/HtmlEditor/components/ValidationPanel/constants.js +6 -0
- package/v2Components/HtmlEditor/components/ValidationPanel/index.js +70 -41
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +254 -0
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +364 -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 +794 -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/__tests__/validationAdapter.test.js +134 -0
- 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/HtmlEditor/utils/validationConstants.js +40 -0
- package/v2Components/MobilePushPreviewV2/index.js +32 -7
- package/v2Components/TemplatePreview/_templatePreview.scss +55 -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/_beePopupEditor.scss +14 -0
- package/v2Containers/BeePopupEditor/constants.js +10 -0
- package/v2Containers/BeePopupEditor/index.js +194 -0
- package/v2Containers/BeePopupEditor/tests/index.test.js +627 -0
- package/v2Containers/CreativesContainer/SlideBoxContent.js +128 -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 +234 -29
- 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/reducer.test.js +46 -0
- package/v2Containers/Email/tests/sagas.test.js +320 -29
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +1285 -0
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +207 -19
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +40 -74
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +1870 -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 +151 -0
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +267 -0
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +23 -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,10 @@ 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';
|
|
45
|
+
import { ISSUE_SOURCES, LABEL_ISSUE_PATTERNS } from './utils/validationConstants';
|
|
44
46
|
|
|
45
47
|
// Styles
|
|
46
48
|
import './_htmlEditor.scss';
|
|
@@ -49,7 +51,7 @@ import './components/FullscreenModal/_fullscreenModal.scss';
|
|
|
49
51
|
// Messages
|
|
50
52
|
import messages from './messages';
|
|
51
53
|
|
|
52
|
-
const HTMLEditor = ({
|
|
54
|
+
const HTMLEditor = forwardRef(({
|
|
53
55
|
intl,
|
|
54
56
|
variant = HTML_EDITOR_VARIANTS.EMAIL, // New prop: 'email' or 'inapp'
|
|
55
57
|
layoutType, // Layout type for InApp variant
|
|
@@ -61,17 +63,33 @@ const HTMLEditor = ({
|
|
|
61
63
|
showFullscreenButton = true,
|
|
62
64
|
autoSave = true,
|
|
63
65
|
autoSaveInterval = 30000, // 30 seconds
|
|
66
|
+
// Tag-related props - tags are fetched and managed by parent component (EmailHTMLEditor, INAPP, etc.)
|
|
67
|
+
tags = [],
|
|
68
|
+
injectedTags = {},
|
|
69
|
+
location,
|
|
70
|
+
eventContextTags = [],
|
|
71
|
+
selectedOfferDetails = [],
|
|
72
|
+
channel,
|
|
73
|
+
userLocale = 'en',
|
|
74
|
+
moduleFilterEnabled = true,
|
|
75
|
+
onTagContextChange, // Parent component handles tag fetching
|
|
76
|
+
onTagSelect = null,
|
|
77
|
+
onContextChange = null,
|
|
78
|
+
globalActions = null,
|
|
79
|
+
isLiquidEnabled = false, // Controls Liquid tab visibility in ValidationTabs
|
|
80
|
+
isFullMode = true, // Full mode vs library mode - controls layout and visibility
|
|
81
|
+
onErrorAcknowledged = null, // Callback when user clicks redirection icon to acknowledge errors
|
|
82
|
+
onValidationChange = null, // Callback when validation state changes (for parent to track errors)
|
|
83
|
+
apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent { liquidErrors: [], standardErrors: [] }
|
|
64
84
|
...props
|
|
65
|
-
}) => {
|
|
85
|
+
}, ref) => {
|
|
66
86
|
// Separate refs for main and modal editors to avoid conflicts
|
|
67
87
|
const mainEditorRef = useRef(null);
|
|
68
88
|
const modalEditorRef = useRef(null);
|
|
69
89
|
const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
|
|
70
90
|
|
|
71
91
|
// Get the currently active editor ref based on fullscreen state
|
|
72
|
-
const getActiveEditorRef = useCallback(() =>
|
|
73
|
-
return isFullscreenModalOpen ? modalEditorRef : mainEditorRef;
|
|
74
|
-
}, [isFullscreenModalOpen]);
|
|
92
|
+
const getActiveEditorRef = useCallback(() => isFullscreenModalOpen ? modalEditorRef : mainEditorRef, [isFullscreenModalOpen]);
|
|
75
93
|
|
|
76
94
|
// Initialize custom hooks for state management - always call both hooks to follow Rules of Hooks
|
|
77
95
|
const isEmailVariant = variant === HTML_EDITOR_VARIANTS.EMAIL;
|
|
@@ -80,7 +98,7 @@ const HTMLEditor = ({
|
|
|
80
98
|
autoSave: isEmailVariant ? autoSave : false,
|
|
81
99
|
autoSaveInterval,
|
|
82
100
|
onSave: isEmailVariant ? onSave : null,
|
|
83
|
-
onChange: isEmailVariant ? onContentChange : null
|
|
101
|
+
onChange: isEmailVariant ? onContentChange : null,
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
const emailContent = useEditorContent(
|
|
@@ -97,7 +115,7 @@ const HTMLEditor = ({
|
|
|
97
115
|
// Convert string content to device-specific format
|
|
98
116
|
inAppInitialContent = {
|
|
99
117
|
[DEVICE_TYPES.ANDROID]: initialContent,
|
|
100
|
-
[DEVICE_TYPES.IOS]: initialContent
|
|
118
|
+
[DEVICE_TYPES.IOS]: initialContent,
|
|
101
119
|
};
|
|
102
120
|
} else {
|
|
103
121
|
// Use provided device-specific content
|
|
@@ -109,7 +127,7 @@ const HTMLEditor = ({
|
|
|
109
127
|
autoSave: isInAppVariant ? autoSave : false,
|
|
110
128
|
autoSaveInterval,
|
|
111
129
|
onSave: isInAppVariant ? onSave : null,
|
|
112
|
-
onChange: isInAppVariant ? onContentChange : null
|
|
130
|
+
onChange: isInAppVariant ? onContentChange : null,
|
|
113
131
|
};
|
|
114
132
|
|
|
115
133
|
const inAppContent = useInAppContent(inAppInitialContent, inAppOptions);
|
|
@@ -117,6 +135,64 @@ const HTMLEditor = ({
|
|
|
117
135
|
// Use appropriate content hook based on variant
|
|
118
136
|
const content = variant === HTML_EDITOR_VARIANTS.EMAIL ? emailContent : inAppContent;
|
|
119
137
|
|
|
138
|
+
// Update content when initialContent prop changes (for edit mode)
|
|
139
|
+
// This ensures the editor updates when template data loads
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (isEmailVariant && emailContent && initialContent !== undefined && initialContent !== null) {
|
|
142
|
+
// Only update if content is different to avoid unnecessary updates
|
|
143
|
+
if (emailContent.content !== initialContent) {
|
|
144
|
+
emailContent.updateContent(initialContent, true); // immediate update
|
|
145
|
+
}
|
|
146
|
+
} else if (isInAppVariant && inAppContent && initialContent !== undefined && initialContent !== null) {
|
|
147
|
+
// Handle InApp variant updates
|
|
148
|
+
const contentToUpdate = typeof initialContent === 'string'
|
|
149
|
+
? { [DEVICE_TYPES.ANDROID]: initialContent, [DEVICE_TYPES.IOS]: initialContent }
|
|
150
|
+
: initialContent;
|
|
151
|
+
if (inAppContent.updateContent) {
|
|
152
|
+
const currentContent = inAppContent.getDeviceContent?.(inAppContent.activeDevice);
|
|
153
|
+
const newContent = contentToUpdate[inAppContent.activeDevice] || contentToUpdate[DEVICE_TYPES.ANDROID] || '';
|
|
154
|
+
if (currentContent !== newContent) {
|
|
155
|
+
inAppContent.updateContent(newContent, true);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}, [initialContent, isEmailVariant, isInAppVariant]);
|
|
160
|
+
// Handle context change for tag API calls
|
|
161
|
+
// If variant is INAPP, use SMS layout; otherwise use the channel (EMAIL)
|
|
162
|
+
const handleContextChange = useCallback((contextData) => {
|
|
163
|
+
// If onContextChange is provided, use it instead of making our own API call
|
|
164
|
+
// This prevents duplicate API calls when parent component handles tag fetching
|
|
165
|
+
if (onContextChange) {
|
|
166
|
+
onContextChange(contextData);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Only make API call if onContextChange is not provided and globalActions is available
|
|
171
|
+
if (!globalActions || !location) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const { type } = location.query || {};
|
|
176
|
+
const tempData = (contextData || '').toLowerCase();
|
|
177
|
+
const isEmbedded = type === EMBEDDED;
|
|
178
|
+
const embedded = isEmbedded ? type : FULL;
|
|
179
|
+
const context = tempData === ALL ? DEFAULT : tempData;
|
|
180
|
+
|
|
181
|
+
// Determine layout: INAPP variant uses SMS, EMAIL variant uses EMAIL
|
|
182
|
+
const layout = variant === HTML_EDITOR_VARIANTS.INAPP ? SMS : EMAIL;
|
|
183
|
+
|
|
184
|
+
const query = {
|
|
185
|
+
layout,
|
|
186
|
+
type: TAG,
|
|
187
|
+
context,
|
|
188
|
+
embedded,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// Call the API via Redux action - this will trigger the saga which calls Api.fetchSchemaForEntity
|
|
192
|
+
// The API endpoint will be: /meta/TAG?query={...}
|
|
193
|
+
globalActions.fetchSchemaForEntity(query);
|
|
194
|
+
}, [variant, globalActions, location, onContextChange]);
|
|
195
|
+
|
|
120
196
|
// Destructure content properties for cleaner access throughout component
|
|
121
197
|
const {
|
|
122
198
|
activeDevice,
|
|
@@ -124,14 +200,14 @@ const HTMLEditor = ({
|
|
|
124
200
|
switchDevice,
|
|
125
201
|
toggleContentSync,
|
|
126
202
|
getDeviceContent,
|
|
127
|
-
markAsSaved
|
|
203
|
+
markAsSaved,
|
|
128
204
|
} = content || {};
|
|
129
205
|
|
|
130
206
|
const layout = useLayoutState({
|
|
131
207
|
splitSizes: [50, 50],
|
|
132
208
|
viewMode: 'desktop',
|
|
133
209
|
mobileWidth: 375,
|
|
134
|
-
isFullscreen: false
|
|
210
|
+
isFullscreen: false,
|
|
135
211
|
});
|
|
136
212
|
|
|
137
213
|
// Get current content for validation based on variant
|
|
@@ -158,7 +234,7 @@ const HTMLEditor = ({
|
|
|
158
234
|
'sanitizer.productionValidHtml': messages.sanitizer.productionValidHtml,
|
|
159
235
|
'sanitizer.productionSanitized': messages.sanitizer.productionSanitized,
|
|
160
236
|
'sanitizer.productionInlineCss': messages.sanitizer.productionInlineCss,
|
|
161
|
-
'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent
|
|
237
|
+
'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent,
|
|
162
238
|
};
|
|
163
239
|
|
|
164
240
|
const messageObj = messageMap[messageKey];
|
|
@@ -179,7 +255,7 @@ const HTMLEditor = ({
|
|
|
179
255
|
'validator.largeImageDetected': messages.validator.largeImageDetected,
|
|
180
256
|
'validator.unclosedCssRule': messages.validator.unclosedCssRule,
|
|
181
257
|
'validator.emptyCssRule': messages.validator.emptyCssRule,
|
|
182
|
-
'validator.cssValidationFailed': messages.validator.cssValidationFailed
|
|
258
|
+
'validator.cssValidationFailed': messages.validator.cssValidationFailed,
|
|
183
259
|
};
|
|
184
260
|
|
|
185
261
|
const messageObj = messageMap[messageKey];
|
|
@@ -190,57 +266,375 @@ const HTMLEditor = ({
|
|
|
190
266
|
enableRealTime: true,
|
|
191
267
|
debounceMs: 500,
|
|
192
268
|
enableSanitization: true,
|
|
193
|
-
securityLevel: 'standard'
|
|
269
|
+
securityLevel: 'standard',
|
|
270
|
+
apiValidationErrors, // Pass API validation errors to merge with client-side validation
|
|
194
271
|
}, formatSanitizerMessage, formatValidatorMessage);
|
|
195
272
|
|
|
196
|
-
//
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
273
|
+
// Expose validation and content state via ref
|
|
274
|
+
useImperativeHandle(ref, () => ({
|
|
275
|
+
getValidation: () => validation,
|
|
276
|
+
getContent: () => currentContent,
|
|
277
|
+
isContentEmpty: () => !currentContent || currentContent.trim() === '',
|
|
278
|
+
getIssueCounts: () => {
|
|
279
|
+
// Check if validation is ready and has getAllIssues method
|
|
280
|
+
if (!validation || typeof validation.getAllIssues !== 'function') {
|
|
281
|
+
return {
|
|
282
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const allIssues = validation.getAllIssues();
|
|
286
|
+
|
|
287
|
+
let htmlCount = 0;
|
|
288
|
+
let labelCount = 0;
|
|
289
|
+
let liquidCount = 0;
|
|
290
|
+
|
|
291
|
+
allIssues.forEach((issue) => {
|
|
292
|
+
const { source, rule, message } = issue;
|
|
293
|
+
const messageLower = (message || '').toLowerCase();
|
|
294
|
+
const ruleLower = (rule || '').toLowerCase();
|
|
295
|
+
|
|
296
|
+
// Check if it's a Liquid issue
|
|
297
|
+
if (source === ISSUE_SOURCES.LIQUID) {
|
|
298
|
+
liquidCount++;
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Check if it's a Label (tag syntax) issue
|
|
303
|
+
const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
|
|
304
|
+
(pattern) => messageLower.includes(pattern.toLowerCase())
|
|
305
|
+
|| ruleLower.includes(pattern.toLowerCase()),
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
if (isLabelIssue) {
|
|
309
|
+
labelCount++;
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
201
312
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
message: intl.formatMessage(messages.labelInsertError),
|
|
205
|
-
description: intl.formatMessage(messages.editorNotReady),
|
|
206
|
-
duration: 3
|
|
313
|
+
// Default to HTML issues
|
|
314
|
+
htmlCount++;
|
|
207
315
|
});
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
html: htmlCount,
|
|
319
|
+
label: labelCount,
|
|
320
|
+
liquid: liquidCount,
|
|
321
|
+
total: allIssues.length,
|
|
322
|
+
};
|
|
323
|
+
},
|
|
324
|
+
getValidationState: () => ({
|
|
325
|
+
isValidating: validation?.isValidating || false,
|
|
326
|
+
hasErrors: validation?.hasBlockingErrors || false,
|
|
327
|
+
issueCounts: (() => {
|
|
328
|
+
if (!validation || typeof validation.getAllIssues !== 'function') {
|
|
329
|
+
return {
|
|
330
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const allIssues = validation.getAllIssues();
|
|
334
|
+
// Use same logic as getIssueCounts
|
|
335
|
+
let htmlCount = 0;
|
|
336
|
+
let labelCount = 0;
|
|
337
|
+
let liquidCount = 0;
|
|
338
|
+
|
|
339
|
+
allIssues.forEach((issue) => {
|
|
340
|
+
const { source, rule, message } = issue;
|
|
341
|
+
const messageLower = (message || '').toLowerCase();
|
|
342
|
+
const ruleLower = (rule || '').toLowerCase();
|
|
343
|
+
|
|
344
|
+
if (source === ISSUE_SOURCES.LIQUID) {
|
|
345
|
+
liquidCount++;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
|
|
350
|
+
(pattern) => messageLower.includes(pattern.toLowerCase())
|
|
351
|
+
|| ruleLower.includes(pattern.toLowerCase()),
|
|
352
|
+
);
|
|
353
|
+
|
|
354
|
+
if (isLabelIssue) {
|
|
355
|
+
labelCount++;
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
htmlCount++;
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
return {
|
|
363
|
+
html: htmlCount,
|
|
364
|
+
label: labelCount,
|
|
365
|
+
liquid: liquidCount,
|
|
366
|
+
total: allIssues.length,
|
|
367
|
+
};
|
|
368
|
+
})(),
|
|
369
|
+
})
|
|
370
|
+
,
|
|
371
|
+
}), [validation, currentContent, apiValidationErrors]); // Include apiValidationErrors so ref methods return updated counts
|
|
372
|
+
|
|
373
|
+
// Use ref to store callback to avoid infinite loops (callback in deps would cause re-runs)
|
|
374
|
+
const onValidationChangeRef = useRef(onValidationChange);
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
onValidationChangeRef.current = onValidationChange;
|
|
377
|
+
}, [onValidationChange]);
|
|
378
|
+
|
|
379
|
+
// Track last sent validation state to prevent duplicate updates
|
|
380
|
+
const lastSentValidationStateRef = useRef(null);
|
|
381
|
+
|
|
382
|
+
// Store validation ref to access current value without triggering re-renders
|
|
383
|
+
const validationRef = useRef(validation);
|
|
384
|
+
validationRef.current = validation;
|
|
385
|
+
|
|
386
|
+
// Extract STABLE primitive values from validation for dependency array
|
|
387
|
+
const isValidating = validation?.isValidating;
|
|
388
|
+
const validationTotalErrors = validation?.summary?.totalErrors || 0;
|
|
389
|
+
const validationTotalWarnings = validation?.summary?.totalWarnings || 0;
|
|
390
|
+
const validationLastValidated = validation?.lastValidated?.getTime?.() || null;
|
|
391
|
+
// Only Rule Group #1 (Input & Sanitization) blocks; use for UI gating (Done/Update/Preview/Test)
|
|
392
|
+
const validationHasBlockingErrors = validation?.hasBlockingErrors || false;
|
|
393
|
+
|
|
394
|
+
// Notify parent component when validation state changes
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
if (!onValidationChangeRef.current) {
|
|
208
397
|
return;
|
|
209
398
|
}
|
|
210
399
|
|
|
211
|
-
//
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
400
|
+
// Skip if still validating (wait for validation to complete)
|
|
401
|
+
if (isValidating) {
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Calculate issue counts from validation using ref (avoid stale closure)
|
|
406
|
+
const calculateIssueCounts = () => {
|
|
407
|
+
const currentValidation = validationRef.current;
|
|
408
|
+
if (!currentValidation || typeof currentValidation.getAllIssues !== 'function') {
|
|
409
|
+
return {
|
|
410
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
const allIssues = currentValidation.getAllIssues();
|
|
414
|
+
|
|
415
|
+
let htmlCount = 0;
|
|
416
|
+
let labelCount = 0;
|
|
417
|
+
let liquidCount = 0;
|
|
418
|
+
|
|
419
|
+
allIssues.forEach((issue) => {
|
|
420
|
+
const { source, rule, message } = issue;
|
|
421
|
+
const messageLower = (message || '').toLowerCase();
|
|
422
|
+
const ruleLower = (rule || '').toLowerCase();
|
|
423
|
+
|
|
424
|
+
if (source === ISSUE_SOURCES.LIQUID) {
|
|
425
|
+
liquidCount += 1;
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
|
|
430
|
+
(pattern) => messageLower.includes(pattern.toLowerCase())
|
|
431
|
+
|| ruleLower.includes(pattern.toLowerCase()),
|
|
432
|
+
);
|
|
433
|
+
|
|
434
|
+
if (isLabelIssue) {
|
|
435
|
+
labelCount += 1;
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
htmlCount += 1;
|
|
217
440
|
});
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
html: htmlCount,
|
|
444
|
+
label: labelCount,
|
|
445
|
+
liquid: liquidCount,
|
|
446
|
+
total: allIssues.length,
|
|
447
|
+
};
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
const issueCounts = calculateIssueCounts();
|
|
451
|
+
const isContentEmpty = !currentContent || currentContent.trim() === '';
|
|
452
|
+
|
|
453
|
+
// hasErrors = only Rule Group #1 (Input & Sanitization) – gates Done/Update/Preview/Test
|
|
454
|
+
const newState = {
|
|
455
|
+
isContentEmpty,
|
|
456
|
+
issueCounts,
|
|
457
|
+
validationComplete: true,
|
|
458
|
+
hasErrors: validationRef.current?.hasBlockingErrors ?? false,
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
// Only call callback if state actually changed (prevent infinite loops)
|
|
462
|
+
const lastState = lastSentValidationStateRef.current;
|
|
463
|
+
const hasChanged = !lastState
|
|
464
|
+
|| lastState.isContentEmpty !== newState.isContentEmpty
|
|
465
|
+
|| lastState.validationComplete !== newState.validationComplete
|
|
466
|
+
|| lastState.hasErrors !== newState.hasErrors
|
|
467
|
+
|| lastState.issueCounts?.total !== newState.issueCounts?.total
|
|
468
|
+
|| lastState.issueCounts?.html !== newState.issueCounts?.html
|
|
469
|
+
|| lastState.issueCounts?.label !== newState.issueCounts?.label
|
|
470
|
+
|| lastState.issueCounts?.liquid !== newState.issueCounts?.liquid;
|
|
471
|
+
|
|
472
|
+
if (hasChanged) {
|
|
473
|
+
lastSentValidationStateRef.current = newState;
|
|
474
|
+
onValidationChangeRef.current(newState);
|
|
475
|
+
}
|
|
476
|
+
}, [isValidating, validationTotalErrors, validationTotalWarnings, validationLastValidated, validationHasBlockingErrors, currentContent, apiValidationErrors]);
|
|
477
|
+
|
|
478
|
+
// Send initial state on mount to ensure parent has correct initial button state
|
|
479
|
+
const hasInitializedRef = useRef(false);
|
|
480
|
+
useEffect(() => {
|
|
481
|
+
if (hasInitializedRef.current || !onValidationChangeRef.current) {
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
hasInitializedRef.current = true;
|
|
485
|
+
|
|
486
|
+
// Send initial state with validationComplete=false to indicate validation pending
|
|
487
|
+
const isContentEmpty = !currentContent || currentContent.trim() === '';
|
|
488
|
+
onValidationChangeRef.current({
|
|
489
|
+
isContentEmpty,
|
|
490
|
+
issueCounts: {
|
|
491
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
492
|
+
},
|
|
493
|
+
validationComplete: false, // Validation hasn't run yet
|
|
494
|
+
hasErrors: false,
|
|
495
|
+
});
|
|
496
|
+
}, [currentContent]); // Only depend on currentContent to run on initial content load
|
|
497
|
+
|
|
498
|
+
// Force validation state recalculation when API validation errors change
|
|
499
|
+
// This ensures that API errors are included in issue counts and displayed in ValidationErrorDisplay
|
|
500
|
+
useEffect(() => {
|
|
501
|
+
if (!onValidationChangeRef.current) {
|
|
218
502
|
return;
|
|
219
503
|
}
|
|
220
504
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
: (typeof editor?.getCursor === 'function' ? editor.getCursor() : 0);
|
|
505
|
+
// Skip if still validating (wait for validation to complete)
|
|
506
|
+
if (validation?.isValidating) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
226
509
|
|
|
227
|
-
|
|
228
|
-
|
|
510
|
+
// Recalculate issue counts including API errors
|
|
511
|
+
const calculateIssueCounts = () => {
|
|
512
|
+
if (!validation || typeof validation.getAllIssues !== 'function') {
|
|
513
|
+
return {
|
|
514
|
+
html: 0, label: 0, liquid: 0, total: 0,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
const allIssues = validation.getAllIssues();
|
|
518
|
+
|
|
519
|
+
let htmlCount = 0;
|
|
520
|
+
let labelCount = 0;
|
|
521
|
+
let liquidCount = 0;
|
|
522
|
+
|
|
523
|
+
allIssues.forEach((issue) => {
|
|
524
|
+
const { source, rule, message } = issue;
|
|
525
|
+
const messageLower = (message || '').toLowerCase();
|
|
526
|
+
const ruleLower = (rule || '').toLowerCase();
|
|
229
527
|
|
|
230
|
-
|
|
231
|
-
|
|
528
|
+
if (source === ISSUE_SOURCES.LIQUID) {
|
|
529
|
+
liquidCount += 1;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
|
|
534
|
+
(pattern) => messageLower.includes(pattern.toLowerCase())
|
|
535
|
+
|| ruleLower.includes(pattern.toLowerCase()),
|
|
536
|
+
);
|
|
232
537
|
|
|
233
|
-
|
|
538
|
+
if (isLabelIssue) {
|
|
539
|
+
labelCount += 1;
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
htmlCount += 1;
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
return {
|
|
547
|
+
html: htmlCount,
|
|
548
|
+
label: labelCount,
|
|
549
|
+
liquid: liquidCount,
|
|
550
|
+
total: allIssues.length,
|
|
551
|
+
};
|
|
552
|
+
};
|
|
553
|
+
|
|
554
|
+
const issueCounts = calculateIssueCounts();
|
|
555
|
+
const isContentEmpty = !currentContent || currentContent.trim() === '';
|
|
556
|
+
|
|
557
|
+
const newState = {
|
|
558
|
+
isContentEmpty,
|
|
559
|
+
issueCounts,
|
|
560
|
+
validationComplete: true,
|
|
561
|
+
hasErrors: validation?.hasBlockingErrors ?? false,
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const lastState = lastSentValidationStateRef.current;
|
|
565
|
+
const hasChanged = !lastState
|
|
566
|
+
|| lastState.hasErrors !== newState.hasErrors
|
|
567
|
+
|| lastState.issueCounts?.total !== newState.issueCounts?.total
|
|
568
|
+
|| lastState.issueCounts?.html !== newState.issueCounts?.html
|
|
569
|
+
|| lastState.issueCounts?.label !== newState.issueCounts?.label
|
|
570
|
+
|| lastState.issueCounts?.liquid !== newState.issueCounts?.liquid;
|
|
571
|
+
|
|
572
|
+
if (hasChanged) {
|
|
573
|
+
lastSentValidationStateRef.current = newState;
|
|
574
|
+
onValidationChangeRef.current(newState);
|
|
575
|
+
}
|
|
576
|
+
}, [apiValidationErrors, validation, currentContent, onValidationChangeRef]);
|
|
577
|
+
|
|
578
|
+
// Handle label insertion at cursor position
|
|
579
|
+
// Note: This is called for notification purposes only when tag is inserted via CodeEditorPane
|
|
580
|
+
// The actual insertion happens in CodeEditorPane.handleTagSelect
|
|
581
|
+
const handleLabelInsert = useCallback((label, position) => {
|
|
582
|
+
// If position is explicitly null, it means the editor wasn't ready when tag was selected
|
|
583
|
+
// In this case, CodeEditorPane couldn't insert the tag, so we should try here
|
|
584
|
+
if (position === null) {
|
|
585
|
+
// With injectIntl({ forwardRef: true }), ref points directly to CodeEditorPane
|
|
586
|
+
const activeEditorRef = getActiveEditorRef();
|
|
587
|
+
const editor = activeEditorRef.current;
|
|
588
|
+
|
|
589
|
+
if (!editor) {
|
|
590
|
+
CapNotification.warning({
|
|
591
|
+
message: intl.formatMessage(messages.labelInsertError),
|
|
592
|
+
description: intl.formatMessage(messages.editorNotReady),
|
|
593
|
+
duration: 3,
|
|
594
|
+
});
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Check if the required methods exist
|
|
599
|
+
if (typeof editor?.insertText !== 'function') {
|
|
600
|
+
CapNotification.error({
|
|
601
|
+
message: intl.formatMessage(messages.labelInsertError),
|
|
602
|
+
description: intl.formatMessage(messages.editorMethodNotAvailable),
|
|
603
|
+
duration: 4,
|
|
604
|
+
});
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
try {
|
|
609
|
+
// Get current cursor position
|
|
610
|
+
const cursor = typeof editor?.getCursor === 'function' ? editor.getCursor() : 0;
|
|
611
|
+
|
|
612
|
+
// Insert label at cursor position
|
|
613
|
+
editor.insertText(label, cursor);
|
|
614
|
+
|
|
615
|
+
// Focus the editor if focus method is available
|
|
616
|
+
editor?.focus?.();
|
|
617
|
+
|
|
618
|
+
// Show success notification
|
|
619
|
+
CapNotification.success({
|
|
620
|
+
message: intl.formatMessage(messages.labelInserted),
|
|
621
|
+
description: intl.formatMessage(messages.labelInsertedDescription, { label }),
|
|
622
|
+
duration: 2,
|
|
623
|
+
});
|
|
624
|
+
} catch (error) {
|
|
625
|
+
CapNotification.error({
|
|
626
|
+
message: intl.formatMessage(messages.labelInsertError),
|
|
627
|
+
description: error.message,
|
|
628
|
+
duration: 4,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
} else {
|
|
632
|
+
// Tag was already inserted by CodeEditorPane (position is a valid number)
|
|
633
|
+
// Just show success notification - no need to access editor
|
|
234
634
|
CapNotification.success({
|
|
235
635
|
message: intl.formatMessage(messages.labelInserted),
|
|
236
636
|
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
|
|
637
|
+
duration: 2,
|
|
244
638
|
});
|
|
245
639
|
}
|
|
246
640
|
}, [intl, getActiveEditorRef]);
|
|
@@ -259,13 +653,13 @@ const HTMLEditor = ({
|
|
|
259
653
|
|
|
260
654
|
CapNotification.success({
|
|
261
655
|
message: intl.formatMessage(messages.contentSaved),
|
|
262
|
-
duration: 2
|
|
656
|
+
duration: 2,
|
|
263
657
|
});
|
|
264
658
|
} catch (error) {
|
|
265
659
|
CapNotification.error({
|
|
266
660
|
message: intl.formatMessage(messages.saveError),
|
|
267
661
|
description: error.message,
|
|
268
|
-
duration: 4
|
|
662
|
+
duration: 4,
|
|
269
663
|
});
|
|
270
664
|
}
|
|
271
665
|
}, [content, onSave, intl, markAsSaved]);
|
|
@@ -276,21 +670,27 @@ const HTMLEditor = ({
|
|
|
276
670
|
const editorInstance = activeEditorRef.current;
|
|
277
671
|
const { line, column = 1 } = error || {};
|
|
278
672
|
|
|
279
|
-
|
|
673
|
+
// Notify parent that user acknowledged errors by clicking redirection icon
|
|
674
|
+
// This enables the buttons even if we can't navigate to a specific line
|
|
675
|
+
if (onErrorAcknowledged) {
|
|
676
|
+
onErrorAcknowledged();
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (editorInstance) {
|
|
280
680
|
try {
|
|
281
|
-
//
|
|
282
|
-
if (editorInstance?.navigateToLine) {
|
|
681
|
+
// If line number exists, navigate to it; otherwise just focus the editor
|
|
682
|
+
if (line && editorInstance?.navigateToLine) {
|
|
283
683
|
editorInstance.navigateToLine(line, column);
|
|
284
684
|
} else {
|
|
685
|
+
// For API errors without line numbers, just focus the editor
|
|
285
686
|
editorInstance?.focus?.();
|
|
286
|
-
// Fallback: just focus the editor if navigation isn't available
|
|
287
687
|
}
|
|
288
688
|
} catch (err) {
|
|
289
689
|
// Fallback: just focus the editor
|
|
290
690
|
editorInstance?.focus?.();
|
|
291
691
|
}
|
|
292
692
|
}
|
|
293
|
-
}, [getActiveEditorRef]);
|
|
693
|
+
}, [getActiveEditorRef, onErrorAcknowledged]);
|
|
294
694
|
|
|
295
695
|
// Handle fullscreen modal
|
|
296
696
|
const handleOpenFullscreen = useCallback(() => {
|
|
@@ -307,6 +707,7 @@ const HTMLEditor = ({
|
|
|
307
707
|
content,
|
|
308
708
|
layout,
|
|
309
709
|
validation,
|
|
710
|
+
isLiquidEnabled,
|
|
310
711
|
editorRef: getActiveEditorRef(),
|
|
311
712
|
handleLabelInsert,
|
|
312
713
|
handleSave,
|
|
@@ -319,13 +720,14 @@ const HTMLEditor = ({
|
|
|
319
720
|
switchDevice,
|
|
320
721
|
toggleContentSync,
|
|
321
722
|
getDeviceContent,
|
|
322
|
-
layoutType
|
|
323
|
-
})
|
|
723
|
+
layoutType,
|
|
724
|
+
}),
|
|
324
725
|
}), [
|
|
325
726
|
variant,
|
|
326
727
|
content,
|
|
327
728
|
layout,
|
|
328
729
|
validation,
|
|
730
|
+
isLiquidEnabled,
|
|
329
731
|
getActiveEditorRef,
|
|
330
732
|
handleLabelInsert,
|
|
331
733
|
handleSave,
|
|
@@ -336,7 +738,7 @@ const HTMLEditor = ({
|
|
|
336
738
|
switchDevice,
|
|
337
739
|
toggleContentSync,
|
|
338
740
|
getDeviceContent,
|
|
339
|
-
layoutType
|
|
741
|
+
layoutType,
|
|
340
742
|
]);
|
|
341
743
|
|
|
342
744
|
// Loading state
|
|
@@ -348,9 +750,14 @@ const HTMLEditor = ({
|
|
|
348
750
|
);
|
|
349
751
|
}
|
|
350
752
|
|
|
753
|
+
// Add library-mode class when not in full mode
|
|
754
|
+
// Note: isFullMode defaults to true, so library mode is when isFullMode === false
|
|
755
|
+
const isLibraryMode = isFullMode === false;
|
|
756
|
+
const editorClassName = `html-editor html-editor--${variant} ${isLibraryMode ? 'html-editor--library-mode' : ''} ${className}`.trim();
|
|
757
|
+
|
|
351
758
|
return (
|
|
352
759
|
<EditorProvider value={contextValue}>
|
|
353
|
-
<div className={
|
|
760
|
+
<div className={editorClassName} {...props}>
|
|
354
761
|
{/* Editor Toolbar - Conditional based on variant */}
|
|
355
762
|
{variant === HTML_EDITOR_VARIANTS.EMAIL ? (
|
|
356
763
|
<EditorToolbar
|
|
@@ -387,19 +794,23 @@ const HTMLEditor = ({
|
|
|
387
794
|
ref={mainEditorRef}
|
|
388
795
|
readOnly={readOnly}
|
|
389
796
|
onLabelInsert={handleLabelInsert}
|
|
797
|
+
onErrorClick={handleValidationErrorClick}
|
|
798
|
+
tags={tags}
|
|
799
|
+
injectedTags={injectedTags}
|
|
800
|
+
location={location}
|
|
801
|
+
eventContextTags={eventContextTags}
|
|
802
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
803
|
+
channel={channel}
|
|
804
|
+
userLocale={userLocale}
|
|
805
|
+
moduleFilterEnabled={moduleFilterEnabled}
|
|
806
|
+
onTagContextChange={onTagContextChange}
|
|
807
|
+
onTagSelect={onTagSelect}
|
|
808
|
+
onContextChange={handleContextChange}
|
|
390
809
|
/>
|
|
391
810
|
|
|
392
811
|
{/* Preview Pane */}
|
|
393
812
|
<PreviewPane />
|
|
394
813
|
</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
814
|
</CapRow>
|
|
404
815
|
|
|
405
816
|
{/* Fullscreen Modal */}
|
|
@@ -411,17 +822,17 @@ const HTMLEditor = ({
|
|
|
411
822
|
maskClosable={false}
|
|
412
823
|
centered
|
|
413
824
|
closable={false}
|
|
414
|
-
width=
|
|
825
|
+
width="90vw"
|
|
415
826
|
className="html-editor-fullscreen-modal"
|
|
416
827
|
>
|
|
417
|
-
<CapRow className=
|
|
828
|
+
<CapRow className={`html-editor-fullscreen html-editor-fullscreen--${variant} ${isLibraryMode ? 'html-editor-fullscreen--library-mode' : ''}`}>
|
|
418
829
|
{/* Editor Toolbar - Conditional based on variant */}
|
|
419
830
|
{variant === HTML_EDITOR_VARIANTS.EMAIL ? (
|
|
420
831
|
<EditorToolbar
|
|
421
|
-
showFullscreenButton
|
|
832
|
+
showFullscreenButton // Show fullscreen button in modal to allow closing
|
|
422
833
|
onLabelInsert={handleLabelInsert}
|
|
423
834
|
onSave={handleSave}
|
|
424
|
-
isFullscreenMode
|
|
835
|
+
isFullscreenMode
|
|
425
836
|
onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
|
|
426
837
|
/>
|
|
427
838
|
) : (
|
|
@@ -434,10 +845,10 @@ const HTMLEditor = ({
|
|
|
434
845
|
onKeepContentSameChange={toggleContentSync}
|
|
435
846
|
/>
|
|
436
847
|
<EditorToolbar
|
|
437
|
-
showFullscreenButton
|
|
848
|
+
showFullscreenButton // Show fullscreen button in modal to allow closing
|
|
438
849
|
onLabelInsert={handleLabelInsert}
|
|
439
850
|
onSave={handleSave}
|
|
440
|
-
isFullscreenMode
|
|
851
|
+
isFullscreenMode
|
|
441
852
|
onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
|
|
442
853
|
variant={variant}
|
|
443
854
|
showTitle={false} // Hide title in InApp variant
|
|
@@ -452,28 +863,30 @@ const HTMLEditor = ({
|
|
|
452
863
|
<CodeEditorPane
|
|
453
864
|
ref={modalEditorRef}
|
|
454
865
|
readOnly={readOnly}
|
|
455
|
-
isFullscreenMode
|
|
866
|
+
isFullscreenMode
|
|
456
867
|
onLabelInsert={handleLabelInsert}
|
|
868
|
+
onErrorClick={handleValidationErrorClick}
|
|
869
|
+
tags={tags}
|
|
870
|
+
injectedTags={injectedTags}
|
|
871
|
+
location={location}
|
|
872
|
+
eventContextTags={eventContextTags}
|
|
873
|
+
selectedOfferDetails={selectedOfferDetails}
|
|
874
|
+
channel={channel}
|
|
875
|
+
userLocale={userLocale}
|
|
876
|
+
moduleFilterEnabled={moduleFilterEnabled}
|
|
877
|
+
onTagContextChange={onTagContextChange}
|
|
457
878
|
/>
|
|
458
879
|
|
|
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
|
-
/>
|
|
880
|
+
{/* Preview Pane */}
|
|
881
|
+
<PreviewPane isFullscreenMode isModalContext />
|
|
882
|
+
</SplitContainer>
|
|
470
883
|
</CapRow>
|
|
471
884
|
</CapRow>
|
|
472
885
|
</CapModal>
|
|
473
886
|
</div>
|
|
474
887
|
</EditorProvider>
|
|
475
888
|
);
|
|
476
|
-
};
|
|
889
|
+
});
|
|
477
890
|
|
|
478
891
|
HTMLEditor.propTypes = {
|
|
479
892
|
intl: intlShape.isRequired,
|
|
@@ -481,7 +894,7 @@ HTMLEditor.propTypes = {
|
|
|
481
894
|
layoutType: PropTypes.string, // Layout type for InApp variant
|
|
482
895
|
initialContent: PropTypes.oneOfType([
|
|
483
896
|
PropTypes.string,
|
|
484
|
-
PropTypes.objectOf(PropTypes.string) // Per-device content for INAPP variant
|
|
897
|
+
PropTypes.objectOf(PropTypes.string), // Per-device content for INAPP variant
|
|
485
898
|
]),
|
|
486
899
|
onSave: PropTypes.func,
|
|
487
900
|
onContentChange: PropTypes.func,
|
|
@@ -489,11 +902,33 @@ HTMLEditor.propTypes = {
|
|
|
489
902
|
readOnly: PropTypes.bool,
|
|
490
903
|
showFullscreenButton: PropTypes.bool,
|
|
491
904
|
autoSave: PropTypes.bool,
|
|
492
|
-
autoSaveInterval: PropTypes.number
|
|
905
|
+
autoSaveInterval: PropTypes.number,
|
|
906
|
+
// Tag-related props - tags are fetched and managed by parent component
|
|
907
|
+
tags: PropTypes.array,
|
|
908
|
+
injectedTags: PropTypes.object,
|
|
909
|
+
location: PropTypes.object,
|
|
910
|
+
eventContextTags: PropTypes.array,
|
|
911
|
+
selectedOfferDetails: PropTypes.array,
|
|
912
|
+
channel: PropTypes.string,
|
|
913
|
+
userLocale: PropTypes.string,
|
|
914
|
+
moduleFilterEnabled: PropTypes.bool,
|
|
915
|
+
onTagContextChange: PropTypes.func, // Required - parent must handle tag fetching
|
|
916
|
+
onTagSelect: PropTypes.func,
|
|
917
|
+
onContextChange: PropTypes.func, // Deprecated: use globalActions instead
|
|
918
|
+
globalActions: PropTypes.object,
|
|
919
|
+
isLiquidEnabled: PropTypes.bool, // Controls Liquid tab visibility in validation
|
|
920
|
+
isFullMode: PropTypes.bool, // Full mode vs library mode
|
|
921
|
+
onErrorAcknowledged: PropTypes.func, // Callback when user clicks redirection icon to acknowledge errors
|
|
922
|
+
onValidationChange: PropTypes.func, // Callback when validation state changes
|
|
923
|
+
apiValidationErrors: PropTypes.shape({
|
|
924
|
+
liquidErrors: PropTypes.arrayOf(PropTypes.string),
|
|
925
|
+
standardErrors: PropTypes.arrayOf(PropTypes.string),
|
|
926
|
+
}), // API validation errors from validateLiquidTemplateContent
|
|
493
927
|
};
|
|
494
928
|
|
|
495
929
|
HTMLEditor.defaultProps = {
|
|
496
930
|
variant: HTML_EDITOR_VARIANTS.EMAIL, // Default to email variant
|
|
931
|
+
layoutType: null,
|
|
497
932
|
initialContent: null, // Will use default from useEditorContent hook
|
|
498
933
|
onSave: null,
|
|
499
934
|
onContentChange: null,
|
|
@@ -501,8 +936,26 @@ HTMLEditor.defaultProps = {
|
|
|
501
936
|
readOnly: false,
|
|
502
937
|
showFullscreenButton: true,
|
|
503
938
|
autoSave: true,
|
|
504
|
-
autoSaveInterval: 30000
|
|
939
|
+
autoSaveInterval: 30000,
|
|
940
|
+
// Tag-related defaults - tags are fetched and managed by parent component
|
|
941
|
+
tags: [],
|
|
942
|
+
injectedTags: {},
|
|
943
|
+
location: null,
|
|
944
|
+
eventContextTags: [],
|
|
945
|
+
selectedOfferDetails: [],
|
|
946
|
+
channel: null,
|
|
947
|
+
userLocale: 'en',
|
|
948
|
+
moduleFilterEnabled: true,
|
|
949
|
+
onTagContextChange: null, // Parent component should provide this
|
|
950
|
+
onTagSelect: null,
|
|
951
|
+
onContextChange: null,
|
|
952
|
+
globalActions: null, // Redux actions for API calls
|
|
953
|
+
isLiquidEnabled: false,
|
|
954
|
+
isFullMode: true, // Default to full mode
|
|
955
|
+
onErrorAcknowledged: null, // Callback when user clicks redirection icon to acknowledge errors
|
|
956
|
+
onValidationChange: null, // Callback when validation state changes
|
|
957
|
+
apiValidationErrors: null, // API validation errors from validateLiquidTemplateContent
|
|
505
958
|
};
|
|
506
959
|
|
|
507
|
-
// Export with
|
|
508
|
-
export default injectIntl(HTMLEditor
|
|
960
|
+
// Export with injectIntl - HTMLEditor now uses forwardRef internally
|
|
961
|
+
export default injectIntl(HTMLEditor);
|