@capillarytech/creatives-library 8.0.246-alpha.0 → 8.0.246
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 +1 -2
- package/initialReducer.js +0 -2
- package/package.json +1 -1
- package/services/api.js +0 -10
- package/services/tests/api.test.js +0 -18
- package/utils/common.js +0 -5
- package/utils/commonUtils.js +5 -28
- package/utils/tests/commonUtil.test.js +0 -224
- package/utils/transformTemplateConfig.js +10 -0
- package/v2Components/CapDeviceContent/index.js +56 -61
- package/v2Components/CapTagList/index.js +1 -6
- package/v2Components/CapTagListWithInput/index.js +1 -5
- package/v2Components/CapTagListWithInput/messages.js +1 -1
- package/v2Components/CapWhatsappCTA/tests/index.test.js +0 -5
- package/v2Components/ErrorInfoNote/index.js +72 -447
- package/v2Components/ErrorInfoNote/messages.js +0 -22
- package/v2Components/ErrorInfoNote/style.scss +4 -280
- package/v2Components/FormBuilder/tests/index.test.js +4 -13
- package/v2Components/HtmlEditor/HTMLEditor.js +94 -642
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +133 -1135
- package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +16 -27
- package/v2Components/HtmlEditor/_htmlEditor.scss +45 -108
- package/v2Components/HtmlEditor/_index.lazy.scss +1 -1
- package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +101 -13
- package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +139 -148
- package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +1 -2
- package/v2Components/HtmlEditor/components/DeviceToggle/index.js +3 -3
- package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +0 -9
- package/v2Components/HtmlEditor/components/EditorToolbar/index.js +1 -1
- package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +0 -22
- package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +7 -4
- package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +45 -35
- package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +3 -1
- package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +33 -33
- package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +6 -7
- package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +6 -3
- package/v2Components/HtmlEditor/components/PreviewPane/index.js +13 -11
- package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +1 -1
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
- package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +31 -49
- package/v2Components/HtmlEditor/constants.js +20 -29
- package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +16 -373
- package/v2Components/HtmlEditor/hooks/useEditorContent.js +2 -5
- package/v2Components/HtmlEditor/hooks/useInAppContent.js +146 -88
- package/v2Components/HtmlEditor/hooks/useValidation.js +45 -150
- package/v2Components/HtmlEditor/index.js +1 -1
- package/v2Components/HtmlEditor/messages.js +85 -95
- package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +102 -134
- package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +25 -23
- package/v2Components/HtmlEditor/utils/validationAdapter.js +41 -66
- package/v2Components/MobilePushPreviewV2/index.js +7 -32
- package/v2Components/TemplatePreview/_templatePreview.scss +24 -44
- package/v2Components/TemplatePreview/index.js +32 -47
- package/v2Components/TemplatePreview/messages.js +0 -4
- package/v2Components/TestAndPreviewSlidebox/_testAndPreviewSlidebox.scss +0 -1
- package/v2Components/TestAndPreviewSlidebox/index.js +25 -31
- package/v2Containers/BeeEditor/index.js +90 -172
- package/v2Containers/CreativesContainer/SlideBoxContent.js +51 -128
- package/v2Containers/CreativesContainer/SlideBoxFooter.js +12 -113
- package/v2Containers/CreativesContainer/SlideBoxHeader.js +1 -2
- package/v2Containers/CreativesContainer/constants.js +0 -1
- package/v2Containers/CreativesContainer/index.js +46 -238
- package/v2Containers/CreativesContainer/messages.js +0 -8
- package/v2Containers/CreativesContainer/tests/SlideBoxFooter.test.js +2 -11
- package/v2Containers/CreativesContainer/tests/__snapshots__/SlideBoxContent.test.js.snap +50 -38
- package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +0 -91
- package/v2Containers/Email/actions.js +0 -7
- package/v2Containers/Email/constants.js +1 -5
- package/v2Containers/Email/index.js +30 -229
- package/v2Containers/Email/messages.js +0 -32
- package/v2Containers/Email/reducer.js +1 -12
- package/v2Containers/Email/sagas.js +7 -61
- package/v2Containers/Email/tests/__snapshots__/reducer.test.js.snap +0 -2
- package/v2Containers/Email/tests/sagas.test.js +1 -1
- package/v2Containers/EmailWrapper/components/EmailWrapperView.js +15 -210
- package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +74 -40
- package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +67 -2
- package/v2Containers/EmailWrapper/constants.js +0 -2
- package/v2Containers/EmailWrapper/hooks/useEmailWrapper.js +77 -629
- package/v2Containers/EmailWrapper/index.js +23 -103
- package/v2Containers/EmailWrapper/messages.js +1 -61
- package/v2Containers/EmailWrapper/tests/EmailWrapperView.test.js +214 -0
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.test.js +77 -509
- package/v2Containers/InApp/actions.js +0 -7
- package/v2Containers/InApp/constants.js +4 -20
- package/v2Containers/InApp/index.js +357 -801
- package/v2Containers/InApp/index.scss +3 -4
- package/v2Containers/InApp/messages.js +3 -7
- package/v2Containers/InApp/reducer.js +3 -21
- package/v2Containers/InApp/sagas.js +9 -29
- package/v2Containers/InApp/selectors.js +5 -25
- package/v2Containers/InApp/tests/index.test.js +50 -154
- package/v2Containers/InApp/tests/reducer.test.js +0 -34
- package/v2Containers/InApp/tests/sagas.test.js +9 -61
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/content.test.js.snap +0 -3
- package/v2Containers/Line/Container/ImageCarousel/tests/__snapshots__/index.test.js.snap +0 -2
- package/v2Containers/Line/Container/Wrapper/tests/__snapshots__/index.test.js.snap +0 -2
- package/v2Containers/Line/Container/tests/__snapshots__/index.test.js.snap +0 -9
- package/v2Containers/Rcs/tests/__snapshots__/index.test.js.snap +0 -12
- package/v2Containers/SmsTrai/Edit/tests/__snapshots__/index.test.js.snap +0 -4
- package/v2Containers/TagList/index.js +19 -62
- package/v2Containers/Templates/_templates.scss +1 -60
- package/v2Containers/Templates/index.js +4 -89
- package/v2Containers/Templates/messages.js +0 -4
- package/v2Containers/Whatsapp/tests/__snapshots__/index.test.js.snap +0 -35
- package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +0 -874
- package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +0 -254
- package/v2Components/HtmlEditor/components/ValidationTabs/index.js +0 -363
- package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +0 -51
- package/v2Components/HtmlEditor/hooks/__tests__/useValidation.apiErrors.test.js +0 -630
- package/v2Containers/BeePopupEditor/constants.js +0 -10
- package/v2Containers/BeePopupEditor/index.js +0 -193
- package/v2Containers/BeePopupEditor/tests/index.test.js +0 -627
- package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +0 -1317
- package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +0 -1605
- package/v2Containers/EmailWrapper/components/__tests__/EmailWrapperView.test.js +0 -520
- package/v2Containers/EmailWrapper/tests/useEmailWrapper.edgeCases.test.js +0 -643
- package/v2Containers/InApp/__tests__/InAppHTMLEditor.test.js +0 -376
- package/v2Containers/InApp/__tests__/sagas.test.js +0 -363
- package/v2Containers/InApp/tests/selectors.test.js +0 -612
- package/v2Containers/InAppWrapper/components/InAppWrapperView.js +0 -162
- package/v2Containers/InAppWrapper/components/__tests__/InAppWrapperView.test.js +0 -267
- package/v2Containers/InAppWrapper/components/inAppWrapperView.scss +0 -9
- package/v2Containers/InAppWrapper/constants.js +0 -16
- package/v2Containers/InAppWrapper/hooks/__tests__/useInAppWrapper.test.js +0 -473
- package/v2Containers/InAppWrapper/hooks/useInAppWrapper.js +0 -198
- package/v2Containers/InAppWrapper/index.js +0 -148
- package/v2Containers/InAppWrapper/messages.js +0 -49
- package/v2Containers/InappAdvance/index.js +0 -1099
- package/v2Containers/InappAdvance/index.scss +0 -10
- package/v2Containers/InappAdvance/tests/index.test.js +0 -448
|
@@ -4,30 +4,133 @@
|
|
|
4
4
|
* Manages separate HTML content for Android and iOS devices with sync functionality
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
useState, useCallback, useRef, useEffect, useMemo,
|
|
9
|
-
} from 'react';
|
|
7
|
+
import { useState, useCallback, useRef, useEffect, useMemo } from 'react';
|
|
10
8
|
import { DEVICE_TYPES, PERFORMANCE } from '../constants';
|
|
11
9
|
|
|
12
10
|
// Constants for better maintainability
|
|
13
11
|
const CONTENT_VALIDATION = {
|
|
14
12
|
MIN_CONTENT_LENGTH: 0,
|
|
15
|
-
DEFAULT_CONTENT_TYPE: 'string'
|
|
13
|
+
DEFAULT_CONTENT_TYPE: 'string'
|
|
16
14
|
};
|
|
17
15
|
|
|
18
16
|
const AUTO_SAVE_CONFIG = {
|
|
19
17
|
DEFAULT_ENABLED: true,
|
|
20
18
|
DEFAULT_INTERVAL: PERFORMANCE.AUTO_SAVE_INTERVAL,
|
|
21
|
-
MIN_AUTO_SAVE_INTERVAL_MS: 1000
|
|
19
|
+
MIN_AUTO_SAVE_INTERVAL_MS: 1000 // Minimum 1 second between auto-saves
|
|
22
20
|
};
|
|
23
21
|
|
|
24
22
|
/**
|
|
25
23
|
* Default InApp content for different devices
|
|
26
|
-
* Empty strings - no default content for new templates
|
|
27
24
|
*/
|
|
28
25
|
const DEFAULT_INAPP_CONTENT = {
|
|
29
|
-
[DEVICE_TYPES.ANDROID]:
|
|
30
|
-
|
|
26
|
+
[DEVICE_TYPES.ANDROID]: `<!DOCTYPE html>
|
|
27
|
+
<html lang="en">
|
|
28
|
+
<head>
|
|
29
|
+
<meta charset="UTF-8">
|
|
30
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
31
|
+
<title>In-App Notification</title>
|
|
32
|
+
<style>
|
|
33
|
+
body {
|
|
34
|
+
margin: 0;
|
|
35
|
+
padding: 16px;
|
|
36
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Roboto', sans-serif;
|
|
37
|
+
background-color: #ffffff;
|
|
38
|
+
color: #212121;
|
|
39
|
+
}
|
|
40
|
+
.notification {
|
|
41
|
+
max-width: 100%;
|
|
42
|
+
background: white;
|
|
43
|
+
border-radius: 8px;
|
|
44
|
+
padding: 16px;
|
|
45
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
46
|
+
}
|
|
47
|
+
.title {
|
|
48
|
+
font-size: 16px;
|
|
49
|
+
font-weight: 500;
|
|
50
|
+
margin: 0 0 8px 0;
|
|
51
|
+
color: #212121;
|
|
52
|
+
}
|
|
53
|
+
.message {
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
line-height: 1.4;
|
|
56
|
+
margin: 0 0 16px 0;
|
|
57
|
+
color: #424242;
|
|
58
|
+
}
|
|
59
|
+
.cta-button {
|
|
60
|
+
background-color: #42b040;
|
|
61
|
+
color: white;
|
|
62
|
+
border: none;
|
|
63
|
+
border-radius: 4px;
|
|
64
|
+
padding: 8px 16px;
|
|
65
|
+
font-size: 12px;
|
|
66
|
+
font-weight: 500;
|
|
67
|
+
cursor: pointer;
|
|
68
|
+
width: 100%;
|
|
69
|
+
}
|
|
70
|
+
</style>
|
|
71
|
+
</head>
|
|
72
|
+
<body>
|
|
73
|
+
<div class="notification">
|
|
74
|
+
<h2 class="title">Sample template</h2>
|
|
75
|
+
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
76
|
+
<button class="cta-button">Add to cart</button>
|
|
77
|
+
</div>
|
|
78
|
+
</body>
|
|
79
|
+
</html>`,
|
|
80
|
+
[DEVICE_TYPES.IOS]: `<!DOCTYPE html>
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="UTF-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
85
|
+
<title>In-App Notification</title>
|
|
86
|
+
<style>
|
|
87
|
+
body {
|
|
88
|
+
margin: 0;
|
|
89
|
+
padding: 16px;
|
|
90
|
+
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
|
|
91
|
+
background-color: #ffffff;
|
|
92
|
+
color: #000000;
|
|
93
|
+
}
|
|
94
|
+
.notification {
|
|
95
|
+
max-width: 100%;
|
|
96
|
+
background: white;
|
|
97
|
+
border-radius: 12px;
|
|
98
|
+
padding: 16px;
|
|
99
|
+
box-shadow: 0 4px 16px rgba(0,0,0,0.1);
|
|
100
|
+
}
|
|
101
|
+
.title {
|
|
102
|
+
font-size: 17px;
|
|
103
|
+
font-weight: 600;
|
|
104
|
+
margin: 0 0 8px 0;
|
|
105
|
+
color: #000000;
|
|
106
|
+
}
|
|
107
|
+
.message {
|
|
108
|
+
font-size: 15px;
|
|
109
|
+
line-height: 1.4;
|
|
110
|
+
margin: 0 0 16px 0;
|
|
111
|
+
color: #3c3c43;
|
|
112
|
+
}
|
|
113
|
+
.cta-button {
|
|
114
|
+
background-color: #007AFF;
|
|
115
|
+
color: white;
|
|
116
|
+
border: none;
|
|
117
|
+
border-radius: 8px;
|
|
118
|
+
padding: 12px 16px;
|
|
119
|
+
font-size: 16px;
|
|
120
|
+
font-weight: 600;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
width: 100%;
|
|
123
|
+
}
|
|
124
|
+
</style>
|
|
125
|
+
</head>
|
|
126
|
+
<body>
|
|
127
|
+
<div class="notification">
|
|
128
|
+
<h2 class="title">Sample template</h2>
|
|
129
|
+
<p class="message">This is a sample template for in-app notification content. This can be triggered on any behavioural event while the user is on the app</p>
|
|
130
|
+
<button class="cta-button">Add to cart</button>
|
|
131
|
+
</div>
|
|
132
|
+
</body>
|
|
133
|
+
</html>`
|
|
31
134
|
};
|
|
32
135
|
|
|
33
136
|
/**
|
|
@@ -47,7 +150,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
47
150
|
autoSave = AUTO_SAVE_CONFIG.DEFAULT_ENABLED,
|
|
48
151
|
autoSaveInterval = AUTO_SAVE_CONFIG.DEFAULT_INTERVAL,
|
|
49
152
|
onSave,
|
|
50
|
-
onChange
|
|
153
|
+
onChange
|
|
51
154
|
} = options;
|
|
52
155
|
|
|
53
156
|
// Destructure device types for cleaner code
|
|
@@ -56,7 +159,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
56
159
|
// Device-specific content state with optional chaining
|
|
57
160
|
const [deviceContent, setDeviceContent] = useState(() => ({
|
|
58
161
|
[ANDROID]: initialContent?.[ANDROID] || DEFAULT_INAPP_CONTENT[ANDROID],
|
|
59
|
-
[IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS]
|
|
162
|
+
[IOS]: initialContent?.[IOS] || DEFAULT_INAPP_CONTENT[IOS]
|
|
60
163
|
}));
|
|
61
164
|
|
|
62
165
|
// Current active device
|
|
@@ -86,67 +189,15 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
86
189
|
deviceContentRef.current = deviceContent;
|
|
87
190
|
}, [deviceContent]);
|
|
88
191
|
|
|
89
|
-
// Ref to track if we've loaded initial content to prevent overriding user edits
|
|
90
|
-
const initialContentLoadedRef = useRef(false);
|
|
91
|
-
const previousContentRef = useRef({ android: '', ios: '' });
|
|
92
|
-
|
|
93
|
-
// Update content when initialContent prop changes (for edit mode)
|
|
94
|
-
// This should only run when loading a template for editing, NOT during active editing
|
|
95
|
-
useEffect(() => {
|
|
96
|
-
const newAndroidContent = initialContent?.[ANDROID];
|
|
97
|
-
const newIosContent = initialContent?.[IOS];
|
|
98
|
-
|
|
99
|
-
// Check if this is meaningful initialContent (has actual content)
|
|
100
|
-
const hasMeaningfulContent = (newAndroidContent && newAndroidContent.trim() !== '')
|
|
101
|
-
|| (newIosContent && newIosContent.trim() !== '');
|
|
102
|
-
|
|
103
|
-
// Check if we're transitioning from empty to meaningful content (library mode scenario)
|
|
104
|
-
const wasEmpty = (!previousContentRef.current.android || previousContentRef.current.android.trim() === '')
|
|
105
|
-
&& (!previousContentRef.current.ios || previousContentRef.current.ios.trim() === '');
|
|
106
|
-
const isTransitioningToContent = wasEmpty && hasMeaningfulContent;
|
|
107
|
-
|
|
108
|
-
// Only update if:
|
|
109
|
-
// 1. We haven't loaded initial content yet (first load), OR
|
|
110
|
-
// 2. We're transitioning from empty to meaningful content (library mode data fetch)
|
|
111
|
-
// This prevents the effect from overriding user edits during active editing
|
|
112
|
-
// while still allowing content to load properly in library mode
|
|
113
|
-
if (!initialContentLoadedRef.current || isTransitioningToContent) {
|
|
114
|
-
if (hasMeaningfulContent) {
|
|
115
|
-
// Mark as loaded to prevent future updates from overriding user edits
|
|
116
|
-
initialContentLoadedRef.current = true;
|
|
117
|
-
|
|
118
|
-
setDeviceContent((prev) => {
|
|
119
|
-
let updated = false;
|
|
120
|
-
const updatedContent = { ...prev };
|
|
121
|
-
|
|
122
|
-
if (newAndroidContent !== undefined && newAndroidContent !== prev[ANDROID]) {
|
|
123
|
-
updatedContent[ANDROID] = newAndroidContent;
|
|
124
|
-
updated = true;
|
|
125
|
-
}
|
|
126
|
-
if (newIosContent !== undefined && newIosContent !== prev[IOS]) {
|
|
127
|
-
updatedContent[IOS] = newIosContent;
|
|
128
|
-
updated = true;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return updated ? updatedContent : prev;
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Update previous content ref
|
|
135
|
-
previousContentRef.current = {
|
|
136
|
-
android: newAndroidContent || '',
|
|
137
|
-
ios: newIosContent || '',
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}, [initialContent, ANDROID, IOS]);
|
|
142
|
-
|
|
143
192
|
// Get current active content
|
|
144
|
-
const currentContent = useMemo(() =>
|
|
193
|
+
const currentContent = useMemo(() => {
|
|
194
|
+
return deviceContent[activeDevice] || '';
|
|
195
|
+
}, [deviceContent, activeDevice]);
|
|
145
196
|
|
|
146
197
|
// Update content for current device
|
|
147
198
|
const updateContent = useCallback((newContent) => {
|
|
148
199
|
// Validate input
|
|
149
|
-
if (typeof newContent !== CONTENT_VALIDATION
|
|
200
|
+
if (typeof newContent !== CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE) {
|
|
150
201
|
console.warn('useInAppContent: newContent must be a string');
|
|
151
202
|
return;
|
|
152
203
|
}
|
|
@@ -158,14 +209,14 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
158
209
|
// When sync is enabled, update both devices with the same content
|
|
159
210
|
updatedDeviceContent = {
|
|
160
211
|
[ANDROID]: newContent,
|
|
161
|
-
[IOS]: newContent
|
|
212
|
+
[IOS]: newContent
|
|
162
213
|
};
|
|
163
214
|
} else {
|
|
164
215
|
// When sync is disabled, update only the current device
|
|
165
|
-
setDeviceContent(
|
|
216
|
+
setDeviceContent(prev => {
|
|
166
217
|
updatedDeviceContent = {
|
|
167
218
|
...prev,
|
|
168
|
-
[activeDevice]: newContent
|
|
219
|
+
[activeDevice]: newContent
|
|
169
220
|
};
|
|
170
221
|
return updatedDeviceContent;
|
|
171
222
|
});
|
|
@@ -174,9 +225,8 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
174
225
|
setIsDirty(true);
|
|
175
226
|
changeTimestampRef.current = Date.now();
|
|
176
227
|
|
|
177
|
-
// Trigger onChange callback
|
|
178
|
-
|
|
179
|
-
onChange?.({ [activeDevice]: newContent }, activeDevice);
|
|
228
|
+
// Trigger onChange callback with optional chaining
|
|
229
|
+
onChange?.(updatedDeviceContent, activeDevice);
|
|
180
230
|
|
|
181
231
|
// Setup auto-save for independent mode
|
|
182
232
|
if (autoSave && autoSaveInterval > AUTO_SAVE_CONFIG.MIN_AUTO_SAVE_INTERVAL_MS && newContent.length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH) {
|
|
@@ -207,7 +257,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
207
257
|
setIsDirty(true);
|
|
208
258
|
changeTimestampRef.current = Date.now();
|
|
209
259
|
|
|
210
|
-
// Trigger onChange callback
|
|
260
|
+
// Trigger onChange callback with optional chaining
|
|
211
261
|
onChange?.(updatedDeviceContent, activeDevice);
|
|
212
262
|
|
|
213
263
|
// Setup auto-save
|
|
@@ -251,7 +301,7 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
251
301
|
const currentActiveContent = deviceContent[activeDevice];
|
|
252
302
|
const syncedContent = {
|
|
253
303
|
[ANDROID]: currentActiveContent,
|
|
254
|
-
[IOS]: currentActiveContent
|
|
304
|
+
[IOS]: currentActiveContent
|
|
255
305
|
};
|
|
256
306
|
|
|
257
307
|
setDeviceContent(syncedContent);
|
|
@@ -275,16 +325,22 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
275
325
|
}, [deviceContent, onSave]);
|
|
276
326
|
|
|
277
327
|
// Check if content exists for current device
|
|
278
|
-
const hasContent = useMemo(() =>
|
|
279
|
-
|
|
328
|
+
const hasContent = useMemo(() => {
|
|
329
|
+
return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE &&
|
|
330
|
+
currentContent.trim().length > CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
331
|
+
}, [currentContent]);
|
|
280
332
|
|
|
281
333
|
// Get content size for current device
|
|
282
|
-
const getContentSize = useCallback(() =>
|
|
283
|
-
|
|
284
|
-
|
|
334
|
+
const getContentSize = useCallback(() => {
|
|
335
|
+
return typeof currentContent === CONTENT_VALIDATION.DEFAULT_CONTENT_TYPE
|
|
336
|
+
? currentContent.length
|
|
337
|
+
: CONTENT_VALIDATION.MIN_CONTENT_LENGTH;
|
|
338
|
+
}, [currentContent]);
|
|
285
339
|
|
|
286
340
|
// Get content for specific device
|
|
287
|
-
const getDeviceContent = useCallback((device) =>
|
|
341
|
+
const getDeviceContent = useCallback((device) => {
|
|
342
|
+
return deviceContent[device] || '';
|
|
343
|
+
}, [deviceContent]);
|
|
288
344
|
|
|
289
345
|
// Set content for specific device
|
|
290
346
|
const setDeviceContent_ = useCallback((device, content) => {
|
|
@@ -304,23 +360,25 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
304
360
|
// Update both devices when sync is enabled
|
|
305
361
|
setDeviceContent({
|
|
306
362
|
[ANDROID]: content,
|
|
307
|
-
[IOS]: content
|
|
363
|
+
[IOS]: content
|
|
308
364
|
});
|
|
309
365
|
} else {
|
|
310
366
|
// Update specific device
|
|
311
|
-
setDeviceContent(
|
|
367
|
+
setDeviceContent(prev => ({
|
|
312
368
|
...prev,
|
|
313
|
-
[device]: content
|
|
369
|
+
[device]: content
|
|
314
370
|
}));
|
|
315
371
|
}
|
|
316
372
|
setIsDirty(true);
|
|
317
373
|
}, [keepContentSame, ANDROID, IOS]);
|
|
318
374
|
|
|
319
375
|
// Cleanup on unmount
|
|
320
|
-
useEffect(() =>
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
return () => {
|
|
378
|
+
if (autoSaveTimerRef.current) {
|
|
379
|
+
clearTimeout(autoSaveTimerRef.current);
|
|
380
|
+
}
|
|
381
|
+
};
|
|
324
382
|
}, []);
|
|
325
383
|
|
|
326
384
|
return {
|
|
@@ -344,6 +402,6 @@ export const useInAppContent = (initialContent = {}, options = {}) => {
|
|
|
344
402
|
toggleContentSync,
|
|
345
403
|
|
|
346
404
|
// Save management
|
|
347
|
-
markAsSaved
|
|
405
|
+
markAsSaved
|
|
348
406
|
};
|
|
349
407
|
};
|
|
@@ -3,9 +3,7 @@
|
|
|
3
3
|
* Manages real-time validation and error display
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
7
|
-
useState, useEffect, useCallback, useRef,
|
|
8
|
-
} from 'react';
|
|
6
|
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
|
9
7
|
import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
|
|
10
8
|
import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
|
|
11
9
|
|
|
@@ -18,46 +16,12 @@ import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/content
|
|
|
18
16
|
* @param {Function} formatValidatorMessage - Message formatter function for validator internationalization
|
|
19
17
|
* @returns {Object} Validation state and methods
|
|
20
18
|
*/
|
|
21
|
-
/**
|
|
22
|
-
* Get line number for a character position in text
|
|
23
|
-
*/
|
|
24
|
-
const getLineNumberFromPosition = (text, position) => {
|
|
25
|
-
if (position === undefined || position < 0) return 1;
|
|
26
|
-
return text.substring(0, position).split('\n').length;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Find line number for a tag or pattern in content
|
|
31
|
-
* Used for API errors to locate where the error occurs
|
|
32
|
-
*/
|
|
33
|
-
const findLineNumberForTag = (content, tagName) => {
|
|
34
|
-
if (!content || !tagName) return null;
|
|
35
|
-
|
|
36
|
-
// Try to find the tag in the content
|
|
37
|
-
// Look for patterns like {{ tagName }}, {{tagName}}, etc.
|
|
38
|
-
const escapedTagName = tagName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
39
|
-
const patterns = [
|
|
40
|
-
new RegExp(`\\{\\{\\s*${escapedTagName}\\s*\\}\\}`, 'g'),
|
|
41
|
-
new RegExp(`\\{%[^%]*${escapedTagName}[^%]*%\\}`, 'g'),
|
|
42
|
-
];
|
|
43
|
-
|
|
44
|
-
for (const pattern of patterns) {
|
|
45
|
-
const match = pattern.exec(content);
|
|
46
|
-
if (match) {
|
|
47
|
-
return getLineNumberFromPosition(content, match.index);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return null;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
19
|
export const useValidation = (content, variant = 'email', options = {}, formatSanitizerMessage = null, formatValidatorMessage = null) => {
|
|
55
20
|
const {
|
|
56
21
|
enableRealTime = true,
|
|
57
22
|
debounceMs = 500,
|
|
58
23
|
enableSanitization = true,
|
|
59
|
-
securityLevel = 'standard'
|
|
60
|
-
apiValidationErrors = null, // API validation errors from validateLiquidTemplateContent
|
|
24
|
+
securityLevel = 'standard'
|
|
61
25
|
} = options;
|
|
62
26
|
|
|
63
27
|
// Validation state
|
|
@@ -78,8 +42,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
78
42
|
totalErrors: 0,
|
|
79
43
|
totalWarnings: 0,
|
|
80
44
|
totalInfo: 0,
|
|
81
|
-
hasSecurityIssues: false
|
|
82
|
-
}
|
|
45
|
+
hasSecurityIssues: false
|
|
46
|
+
}
|
|
83
47
|
});
|
|
84
48
|
|
|
85
49
|
// Refs for debouncing
|
|
@@ -91,7 +55,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
91
55
|
*/
|
|
92
56
|
const performValidation = useCallback(async (htmlContent) => {
|
|
93
57
|
if (!htmlContent) {
|
|
94
|
-
setValidationState(
|
|
58
|
+
setValidationState(prev => ({
|
|
95
59
|
...prev,
|
|
96
60
|
isValidating: false,
|
|
97
61
|
htmlErrors: [],
|
|
@@ -108,13 +72,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
108
72
|
totalErrors: 0,
|
|
109
73
|
totalWarnings: 0,
|
|
110
74
|
totalInfo: 0,
|
|
111
|
-
hasSecurityIssues: false
|
|
112
|
-
}
|
|
75
|
+
hasSecurityIssues: false
|
|
76
|
+
}
|
|
113
77
|
}));
|
|
114
78
|
return;
|
|
115
79
|
}
|
|
116
80
|
|
|
117
|
-
setValidationState(
|
|
81
|
+
setValidationState(prev => ({ ...prev, isValidating: true }));
|
|
118
82
|
|
|
119
83
|
try {
|
|
120
84
|
// 1. HTML Validation
|
|
@@ -157,12 +121,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
157
121
|
totalErrors,
|
|
158
122
|
totalWarnings,
|
|
159
123
|
totalInfo,
|
|
160
|
-
hasSecurityIssues
|
|
161
|
-
}
|
|
124
|
+
hasSecurityIssues
|
|
125
|
+
}
|
|
162
126
|
});
|
|
127
|
+
|
|
163
128
|
} catch (error) {
|
|
164
129
|
console.error('Validation error:', error);
|
|
165
|
-
setValidationState(
|
|
130
|
+
setValidationState(prev => ({
|
|
166
131
|
...prev,
|
|
167
132
|
isValidating: false,
|
|
168
133
|
htmlErrors: [{
|
|
@@ -172,13 +137,13 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
172
137
|
column: 1,
|
|
173
138
|
rule: 'validation-error',
|
|
174
139
|
severity: 'error',
|
|
175
|
-
source: 'validator'
|
|
140
|
+
source: 'validator'
|
|
176
141
|
}],
|
|
177
142
|
isValid: false,
|
|
178
143
|
summary: {
|
|
179
144
|
...prev.summary,
|
|
180
|
-
totalErrors: 1
|
|
181
|
-
}
|
|
145
|
+
totalErrors: 1
|
|
146
|
+
}
|
|
182
147
|
}));
|
|
183
148
|
}
|
|
184
149
|
}, [variant, enableSanitization, securityLevel, formatSanitizerMessage, formatValidatorMessage]);
|
|
@@ -243,8 +208,8 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
243
208
|
totalErrors: 0,
|
|
244
209
|
totalWarnings: 0,
|
|
245
210
|
totalInfo: 0,
|
|
246
|
-
hasSecurityIssues: false
|
|
247
|
-
}
|
|
211
|
+
hasSecurityIssues: false
|
|
212
|
+
}
|
|
248
213
|
});
|
|
249
214
|
}, []);
|
|
250
215
|
|
|
@@ -258,10 +223,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
258
223
|
...validationState.htmlInfo,
|
|
259
224
|
...validationState.cssErrors,
|
|
260
225
|
...validationState.cssWarnings,
|
|
261
|
-
...validationState.cssInfo
|
|
226
|
+
...validationState.cssInfo
|
|
262
227
|
];
|
|
263
228
|
|
|
264
|
-
return allErrors.filter(
|
|
229
|
+
return allErrors.filter(error => error.severity === severity);
|
|
265
230
|
}, [validationState]);
|
|
266
231
|
|
|
267
232
|
/**
|
|
@@ -274,95 +239,32 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
274
239
|
...validationState.htmlInfo,
|
|
275
240
|
...validationState.cssErrors,
|
|
276
241
|
...validationState.cssWarnings,
|
|
277
|
-
...validationState.cssInfo
|
|
242
|
+
...validationState.cssInfo
|
|
278
243
|
];
|
|
279
244
|
|
|
280
|
-
return allErrors.filter(
|
|
245
|
+
return allErrors.filter(error => error.source === source);
|
|
281
246
|
}, [validationState]);
|
|
282
247
|
|
|
283
|
-
/**
|
|
284
|
-
* Extract line number from error message if present
|
|
285
|
-
* API errors might contain line numbers in messages like "Error at line 5" or "Line 10: error"
|
|
286
|
-
* Also tries to find line number by searching for the problematic tag in content
|
|
287
|
-
*/
|
|
288
|
-
const extractLineNumberFromMessage = useCallback((message) => {
|
|
289
|
-
if (!message || typeof message !== 'string') {
|
|
290
|
-
return null;
|
|
291
|
-
}
|
|
292
|
-
// Try to match patterns like "line 5", "Line 10", "at line 15", "line: 20", etc.
|
|
293
|
-
const lineMatch = message.match(/\b(?:line|Line|LINE)\s*:?\s*(\d+)\b/i);
|
|
294
|
-
if (lineMatch && lineMatch[1]) {
|
|
295
|
-
return parseInt(lineMatch[1], 10);
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
// Try to extract tag name from error message (e.g., "Unsupported tags: test" -> "test")
|
|
299
|
-
const tagMatch = message.match(/(?:tag|tags|Tag|Tags)[\s:]+([a-zA-Z_][a-zA-Z0-9_.]*)/i);
|
|
300
|
-
if (tagMatch && tagMatch[1] && content) {
|
|
301
|
-
const tagName = tagMatch[1];
|
|
302
|
-
const lineNumber = findLineNumberForTag(content, tagName);
|
|
303
|
-
if (lineNumber) {
|
|
304
|
-
return lineNumber;
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
return null;
|
|
309
|
-
}, [content]);
|
|
310
|
-
|
|
311
248
|
/**
|
|
312
249
|
* Get all errors and warnings combined
|
|
313
|
-
* Includes both client-side validation errors and API validation errors
|
|
314
250
|
*/
|
|
315
251
|
const getAllIssues = useCallback(() => {
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
message: errorMessage,
|
|
325
|
-
line: extractedLine, // Try to extract line number from message, null if not found
|
|
326
|
-
column: null,
|
|
327
|
-
rule: 'liquid-api-validation',
|
|
328
|
-
severity: 'error',
|
|
329
|
-
source: 'liquid-validator', // This source ensures errors appear in "Liquid Issues" tab
|
|
330
|
-
};
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
// Standard errors (non-liquid endpoint errors) appear in HTML/Label Issues tabs
|
|
334
|
-
const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
|
|
335
|
-
const extractedLine = extractLineNumberFromMessage(errorMessage);
|
|
336
|
-
return {
|
|
337
|
-
type: 'error',
|
|
338
|
-
message: errorMessage,
|
|
339
|
-
line: extractedLine, // Try to extract line number from message, null if not found
|
|
340
|
-
column: null,
|
|
341
|
-
rule: 'standard-api-validation',
|
|
342
|
-
severity: 'error',
|
|
343
|
-
source: 'api-validator', // This source ensures errors appear in HTML/Label Issues tabs
|
|
344
|
-
};
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
const allIssues = [
|
|
348
|
-
...(validationState.htmlErrors || []),
|
|
349
|
-
...(validationState.htmlWarnings || []),
|
|
350
|
-
...(validationState.htmlInfo || []),
|
|
351
|
-
...(validationState.cssErrors || []),
|
|
352
|
-
...(validationState.cssWarnings || []),
|
|
353
|
-
...(validationState.cssInfo || []),
|
|
354
|
-
...(validationState.securityIssues || []).map((issue) => ({
|
|
252
|
+
return [
|
|
253
|
+
...validationState.htmlErrors,
|
|
254
|
+
...validationState.htmlWarnings,
|
|
255
|
+
...validationState.htmlInfo,
|
|
256
|
+
...validationState.cssErrors,
|
|
257
|
+
...validationState.cssWarnings,
|
|
258
|
+
...validationState.cssInfo,
|
|
259
|
+
...validationState.securityIssues.map(issue => ({
|
|
355
260
|
type: 'error',
|
|
356
261
|
message: `Security issue: ${issue.type}`,
|
|
357
262
|
line: 1,
|
|
358
263
|
column: 1,
|
|
359
264
|
rule: 'security-violation',
|
|
360
265
|
severity: 'error',
|
|
361
|
-
source: 'security'
|
|
362
|
-
}))
|
|
363
|
-
// Add API validation errors
|
|
364
|
-
...apiLiquidErrors,
|
|
365
|
-
...apiStandardErrors,
|
|
266
|
+
source: 'security'
|
|
267
|
+
}))
|
|
366
268
|
].sort((a, b) => {
|
|
367
269
|
// Sort by severity (error > warning > info) then by line number
|
|
368
270
|
const severityOrder = { error: 0, warning: 1, info: 2 };
|
|
@@ -371,22 +273,16 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
371
273
|
}
|
|
372
274
|
return (a.line || 0) - (b.line || 0);
|
|
373
275
|
});
|
|
374
|
-
|
|
375
|
-
// Ensure we always return an array
|
|
376
|
-
return Array.isArray(allIssues) ? allIssues : [];
|
|
377
|
-
}, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
|
|
276
|
+
}, [validationState]);
|
|
378
277
|
|
|
379
278
|
/**
|
|
380
279
|
* Check if validation is clean (no errors or warnings)
|
|
381
|
-
* Includes API validation errors in the check
|
|
382
280
|
*/
|
|
383
281
|
const isClean = useCallback(() => {
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
&& !hasApiErrors;
|
|
389
|
-
}, [validationState.summary, apiValidationErrors]);
|
|
282
|
+
return validationState.summary.totalErrors === 0 &&
|
|
283
|
+
validationState.summary.totalWarnings === 0 &&
|
|
284
|
+
!validationState.summary.hasSecurityIssues;
|
|
285
|
+
}, [validationState.summary]);
|
|
390
286
|
|
|
391
287
|
// Effect to validate content when it changes
|
|
392
288
|
useEffect(() => {
|
|
@@ -396,15 +292,14 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
396
292
|
}, [content, validateContent, enableRealTime]);
|
|
397
293
|
|
|
398
294
|
// Cleanup on unmount
|
|
399
|
-
useEffect(() =>
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
295
|
+
useEffect(() => {
|
|
296
|
+
return () => {
|
|
297
|
+
if (debounceRef.current) {
|
|
298
|
+
clearTimeout(debounceRef.current);
|
|
299
|
+
}
|
|
300
|
+
};
|
|
403
301
|
}, []);
|
|
404
302
|
|
|
405
|
-
// Include API validation errors in computed properties
|
|
406
|
-
const hasApiErrors = (apiValidationErrors?.liquidErrors?.length || 0) + (apiValidationErrors?.standardErrors?.length || 0) > 0;
|
|
407
|
-
|
|
408
303
|
return {
|
|
409
304
|
// Validation state
|
|
410
305
|
...validationState,
|
|
@@ -421,10 +316,10 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
|
|
|
421
316
|
isClean,
|
|
422
317
|
|
|
423
318
|
// Computed properties
|
|
424
|
-
hasErrors: validationState.summary.totalErrors > 0
|
|
319
|
+
hasErrors: validationState.summary.totalErrors > 0,
|
|
425
320
|
hasWarnings: validationState.summary.totalWarnings > 0,
|
|
426
|
-
hasSecurityIssues: validationState.summary.hasSecurityIssues
|
|
321
|
+
hasSecurityIssues: validationState.summary.hasSecurityIssues
|
|
427
322
|
};
|
|
428
323
|
};
|
|
429
324
|
|
|
430
|
-
export default useValidation;
|
|
325
|
+
export default useValidation;
|