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