@blocklet/editor 2.4.93 → 2.4.95
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/lib/ext/CustomComponent/components/Code.d.ts +1 -1
- package/lib/ext/CustomComponent/components/Code.js +13 -13
- package/lib/ext/CustomComponent/components/Field.d.ts +17 -2
- package/lib/ext/CustomComponent/components/Field.js +107 -5
- package/lib/libs/utm.d.ts +9 -0
- package/lib/libs/utm.js +43 -0
- package/lib/libs/utm.spec.d.ts +1 -0
- package/lib/libs/utm.spec.js +153 -0
- package/lib/main/plugins/ClickableLinkPlugin/index.js +2 -1
- package/lib/main/viewer/index.js +1 -1
- package/package.json +2 -2
|
@@ -82,7 +82,7 @@ export const LANGUAGE_ICON = {
|
|
|
82
82
|
},
|
|
83
83
|
awk: 'vscode-icons:file-type-awk',
|
|
84
84
|
ballerina: 'material-icon-theme:ballerina',
|
|
85
|
-
bash: '
|
|
85
|
+
bash: 'catppuccin:powershell',
|
|
86
86
|
bat: 'catppuccin:batch',
|
|
87
87
|
batch: 'catppuccin:batch',
|
|
88
88
|
beancount: '',
|
|
@@ -141,7 +141,7 @@ export const LANGUAGE_ICON = {
|
|
|
141
141
|
fennel: '',
|
|
142
142
|
fish: '',
|
|
143
143
|
fluent: '',
|
|
144
|
-
fsharp: '
|
|
144
|
+
fsharp: 'logos:fsharp',
|
|
145
145
|
gdresource: '',
|
|
146
146
|
gdscript: 'vscode-icons:file-type-gdscript',
|
|
147
147
|
gdshader: '',
|
|
@@ -198,7 +198,7 @@ export const LANGUAGE_ICON = {
|
|
|
198
198
|
less: 'catppuccin:less',
|
|
199
199
|
liquid: 'logos:shopify',
|
|
200
200
|
lit: 'logos:lit',
|
|
201
|
-
log: '
|
|
201
|
+
log: 'catppuccin:powershell',
|
|
202
202
|
logo: '',
|
|
203
203
|
lua: 'logos:lua',
|
|
204
204
|
luau: 'vscode-icons:file-type-luau',
|
|
@@ -263,11 +263,11 @@ export const LANGUAGE_ICON = {
|
|
|
263
263
|
scala: 'logos:scala',
|
|
264
264
|
scheme: '',
|
|
265
265
|
scss: 'logos:sass',
|
|
266
|
-
sh: '
|
|
266
|
+
sh: 'catppuccin:powershell',
|
|
267
267
|
shaderlab: 'catppuccin:shader',
|
|
268
|
-
shell: '
|
|
269
|
-
shellscript: '
|
|
270
|
-
shellsession: '
|
|
268
|
+
shell: 'catppuccin:powershell',
|
|
269
|
+
shellscript: 'catppuccin:powershell',
|
|
270
|
+
shellsession: 'catppuccin:powershell',
|
|
271
271
|
smalltalk: '',
|
|
272
272
|
solidity: {
|
|
273
273
|
light: 'vscode-icons:file-type-light-solidity',
|
|
@@ -330,14 +330,14 @@ export const LANGUAGE_ICON = {
|
|
|
330
330
|
dark: 'vscode-icons:file-type-yaml-official',
|
|
331
331
|
},
|
|
332
332
|
zenscript: '',
|
|
333
|
-
zig: '
|
|
333
|
+
zig: 'devicon:zig',
|
|
334
334
|
zsh: {
|
|
335
335
|
light: 'devicon:zsh',
|
|
336
336
|
dark: 'devicon-plain:zsh',
|
|
337
337
|
},
|
|
338
338
|
};
|
|
339
339
|
// 多语言翻译
|
|
340
|
-
export const
|
|
340
|
+
export const X_CODE_TRANSLATIONS = {
|
|
341
341
|
en: {
|
|
342
342
|
seeAllLines: 'See all {lines} lines',
|
|
343
343
|
collapseCode: 'Collapse code',
|
|
@@ -361,9 +361,9 @@ function CodeHeader({ title, icon, actions }) {
|
|
|
361
361
|
bgcolor: 'grey.50',
|
|
362
362
|
borderRadius: '8px 8px 0 0',
|
|
363
363
|
}, children: [_jsxs(Box, { sx: { display: 'flex', alignItems: 'center', gap: 1, flex: 1 }, children: [icon && (_jsx(Box, { component: Icon, icon: icon, sx: {
|
|
364
|
-
fontSize: '
|
|
365
|
-
height: '
|
|
366
|
-
lineHeight: '
|
|
364
|
+
fontSize: '16px',
|
|
365
|
+
height: '16px',
|
|
366
|
+
lineHeight: '16px',
|
|
367
367
|
color: 'text.secondary',
|
|
368
368
|
fill: ({ palette }) => palette.text.secondary,
|
|
369
369
|
} })), title && (_jsx(Typography, { variant: "body2", sx: { color: 'text.secondary', fontWeight: 500 }, children: title }))] }), actions && _jsx(Box, { sx: { display: 'flex', alignItems: 'center', gap: 0.5 }, children: actions })] }));
|
|
@@ -407,7 +407,7 @@ function FoldButton({ isCollapsed, onToggle, sx = {} }) {
|
|
|
407
407
|
/** 折叠 Footer */
|
|
408
408
|
function FoldFooter({ remainingLines, isCollapsed, onToggle, }) {
|
|
409
409
|
const { locale = 'en' } = useLocaleContext();
|
|
410
|
-
const t = useMemoizedFn((key, data = {}) => translate(
|
|
410
|
+
const t = useMemoizedFn((key, data = {}) => translate(X_CODE_TRANSLATIONS, key, locale, 'en', data));
|
|
411
411
|
return (_jsx(Box, { sx: {
|
|
412
412
|
display: 'flex',
|
|
413
413
|
alignItems: 'center',
|
|
@@ -1,9 +1,24 @@
|
|
|
1
|
+
export declare const X_FIELD_TRANSLATIONS: {
|
|
2
|
+
en: {
|
|
3
|
+
required: string;
|
|
4
|
+
deprecated: string;
|
|
5
|
+
subfields: string;
|
|
6
|
+
};
|
|
7
|
+
zh: {
|
|
8
|
+
required: string;
|
|
9
|
+
deprecated: string;
|
|
10
|
+
subfields: string;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
1
13
|
export interface FieldProps {
|
|
2
14
|
name: string;
|
|
3
15
|
type: string;
|
|
4
16
|
default?: string;
|
|
5
17
|
required?: 'false' | 'true';
|
|
6
18
|
deprecated?: 'false' | 'true';
|
|
7
|
-
|
|
19
|
+
desc?: string;
|
|
20
|
+
children?: {
|
|
21
|
+
properties: FieldProps;
|
|
22
|
+
}[];
|
|
8
23
|
}
|
|
9
|
-
export default function Field({ name, type, default: defaultVal, required, deprecated,
|
|
24
|
+
export default function Field({ name, type, default: defaultVal, required, deprecated, desc, children }: FieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Stack, Box, styled, alpha } from '@mui/material';
|
|
2
|
+
import { Stack, Box, styled, alpha, Collapse } from '@mui/material';
|
|
3
|
+
import { Add } from '@mui/icons-material';
|
|
4
|
+
import { useState, useEffect } from 'react';
|
|
5
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
6
|
+
import { translate } from '@arcblock/ux/lib/Locale/util';
|
|
7
|
+
import { useMemoizedFn } from 'ahooks';
|
|
3
8
|
import useMobile from '../../../main/hooks/use-mobile';
|
|
9
|
+
// 多语言翻译
|
|
10
|
+
export const X_FIELD_TRANSLATIONS = {
|
|
11
|
+
en: {
|
|
12
|
+
required: 'required',
|
|
13
|
+
deprecated: 'deprecated',
|
|
14
|
+
subfields: '{num} subfields',
|
|
15
|
+
},
|
|
16
|
+
zh: {
|
|
17
|
+
required: '必填',
|
|
18
|
+
deprecated: '废弃',
|
|
19
|
+
subfields: '{num} 个子属性',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
// 利用栈管理高亮的 Field
|
|
23
|
+
const highlightManager = (() => {
|
|
24
|
+
const highlightStack = [];
|
|
25
|
+
const listeners = new Set();
|
|
26
|
+
const removeFromStack = (fieldId) => {
|
|
27
|
+
const index = highlightStack.indexOf(fieldId);
|
|
28
|
+
if (index > -1) {
|
|
29
|
+
highlightStack.splice(index, 1);
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const notifyListeners = () => {
|
|
33
|
+
const currentHighlight = highlightStack.length > 0 ? highlightStack[highlightStack.length - 1] : null;
|
|
34
|
+
listeners.forEach((listener) => listener(currentHighlight));
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
enterField(fieldId) {
|
|
38
|
+
removeFromStack(fieldId);
|
|
39
|
+
highlightStack.push(fieldId);
|
|
40
|
+
notifyListeners();
|
|
41
|
+
},
|
|
42
|
+
leaveField(fieldId) {
|
|
43
|
+
removeFromStack(fieldId);
|
|
44
|
+
notifyListeners();
|
|
45
|
+
},
|
|
46
|
+
getHighlightedFieldId() {
|
|
47
|
+
return highlightStack.length > 0 ? highlightStack[highlightStack.length - 1] : null;
|
|
48
|
+
},
|
|
49
|
+
subscribe(listener) {
|
|
50
|
+
listeners.add(listener);
|
|
51
|
+
return () => listeners.delete(listener);
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
})();
|
|
4
55
|
const Tag = styled(Box)(({ theme }) => ({
|
|
5
56
|
borderRadius: theme.shape.borderRadius * 0.75,
|
|
6
57
|
padding: theme.spacing(0.25, 1),
|
|
@@ -13,20 +64,71 @@ const Bar = styled(Box)(({ theme }) => ({
|
|
|
13
64
|
flexWrap: 'wrap',
|
|
14
65
|
gap: theme.spacing(1.25),
|
|
15
66
|
}));
|
|
16
|
-
export default function Field({ name, type, default: defaultVal, required, deprecated,
|
|
67
|
+
export default function Field({ name, type, default: defaultVal, required, deprecated, desc, children }) {
|
|
68
|
+
const { locale = 'en' } = useLocaleContext();
|
|
69
|
+
const t = useMemoizedFn((key, data = {}) => translate(X_FIELD_TRANSLATIONS, key, locale, 'en', data));
|
|
17
70
|
const isMobile = useMobile();
|
|
18
71
|
const isRequired = required === 'true';
|
|
19
72
|
const isDeprecated = deprecated === 'true';
|
|
20
|
-
const
|
|
73
|
+
const [expanded, setExpanded] = useState(false);
|
|
74
|
+
const [highlightedFieldId, setHighlightedFieldId] = useState(null);
|
|
75
|
+
// 使用 name 作为 fieldId
|
|
76
|
+
const fieldId = name;
|
|
77
|
+
// 订阅全局高亮状态变化
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const unsubscribe = highlightManager.subscribe(setHighlightedFieldId);
|
|
80
|
+
return () => unsubscribe();
|
|
81
|
+
}, []);
|
|
82
|
+
const metaInfo = (_jsxs(_Fragment, { children: [_jsx(Tag, { sx: { backgroundColor: ({ palette }) => alpha(palette.grey[100], 0.6) }, children: type }), isRequired && (_jsx(Tag, { sx: { color: 'error.main', background: ({ palette }) => alpha(palette.error.main, 0.1), fontWeight: 500 }, children: t('required') })), isDeprecated && (_jsx(Tag, { sx: {
|
|
21
83
|
color: 'warning.light',
|
|
22
84
|
background: ({ palette }) => alpha(palette.warning.light, 0.1),
|
|
23
85
|
fontWeight: 500,
|
|
24
|
-
}, children:
|
|
86
|
+
}, children: t('deprecated') })), defaultVal && (_jsxs(Tag, { sx: { backgroundColor: ({ palette }) => alpha(palette.grey[100], 0.6) }, children: [_jsx(Box, { component: "span", sx: { color: 'text.secondary', mr: 0.5 }, children: "default:" }), defaultVal] }))] }));
|
|
87
|
+
// 渲染子 Field
|
|
88
|
+
const renderChildren = () => {
|
|
89
|
+
if (!children || children.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
return (_jsxs(Box, { sx: { mt: 1 }, children: [_jsxs(Box, { sx: {
|
|
92
|
+
display: 'flex',
|
|
93
|
+
alignItems: 'center',
|
|
94
|
+
cursor: 'pointer',
|
|
95
|
+
mb: 1,
|
|
96
|
+
p: 0.5,
|
|
97
|
+
borderRadius: 1,
|
|
98
|
+
'&:hover': {
|
|
99
|
+
backgroundColor: ({ palette }) => alpha(palette.action.hover, 0.04),
|
|
100
|
+
},
|
|
101
|
+
}, onClick: () => setExpanded(!expanded), children: [_jsx(Box, { sx: {
|
|
102
|
+
color: 'text.secondary',
|
|
103
|
+
fontSize: '16px',
|
|
104
|
+
mr: 0.5,
|
|
105
|
+
transform: expanded ? 'rotate(45deg)' : 'rotate(0deg)',
|
|
106
|
+
transition: 'transform 0.2s ease-in-out',
|
|
107
|
+
display: 'flex',
|
|
108
|
+
alignItems: 'center',
|
|
109
|
+
justifyContent: 'center',
|
|
110
|
+
}, children: _jsx(Add, {}) }), _jsx(Box, { sx: { color: 'text.secondary', fontSize: '14px', flex: 1 }, children: t('subfields', { num: children.length }) })] }), _jsx(Collapse, { in: expanded, timeout: "auto", unmountOnExit: true, children: _jsx(Box, { sx: {
|
|
111
|
+
pl: 1.5,
|
|
112
|
+
borderLeft: '1px dashed',
|
|
113
|
+
borderColor: highlightedFieldId === fieldId ? 'primary.main' : 'divider',
|
|
114
|
+
transition: 'border-color 0.2s ease-in-out',
|
|
115
|
+
}, onMouseEnter: () => {
|
|
116
|
+
highlightManager.enterField(fieldId);
|
|
117
|
+
}, onMouseLeave: () => {
|
|
118
|
+
highlightManager.leaveField(fieldId);
|
|
119
|
+
}, children: _jsx(Stack, { spacing: 1, children: children.map((child) => {
|
|
120
|
+
const fullname = `${name}.${child.properties.name}`;
|
|
121
|
+
return _jsx(Field, { ...child.properties, name: fullname }, fullname);
|
|
122
|
+
}) }) }) })] }));
|
|
123
|
+
};
|
|
25
124
|
return (_jsxs(Stack, { className: "x-param-field", sx: {
|
|
26
125
|
borderBottom: '1px solid',
|
|
27
126
|
borderColor: 'divider',
|
|
28
127
|
pt: 1.25,
|
|
29
128
|
pb: 2.5,
|
|
30
129
|
my: 1.25,
|
|
31
|
-
|
|
130
|
+
'&:last-child': {
|
|
131
|
+
borderBottom: 'none',
|
|
132
|
+
},
|
|
133
|
+
}, children: [_jsxs(Bar, { children: [_jsx(Box, { sx: { color: 'primary.main', fontWeight: 500 }, children: name }), !isMobile && metaInfo] }), isMobile && _jsx(Bar, { sx: { mt: 1.25 }, children: metaInfo }), desc && _jsx(Box, { sx: { color: 'text.secondary', mt: 2 }, children: desc }), renderChildren()] }));
|
|
32
134
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* 为外部 URL 添加 UTM 追踪参数
|
|
4
|
+
* @param url - 原始 URL 地址
|
|
5
|
+
* @returns 添加 UTM 参数后的 URL
|
|
6
|
+
* @see https://team.arcblock.io/comment/discussions/7504c5ce-7453-4223-a539-27620efcf38e#6fd5adb6-49ab-4ead-9233-240aa53d1058
|
|
7
|
+
*
|
|
8
|
+
*/
|
|
9
|
+
export declare function getUTMUrl(url: string): string;
|
package/lib/libs/utm.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { withQuery, getQuery } from 'ufo';
|
|
2
|
+
function isExternal(url) {
|
|
3
|
+
try {
|
|
4
|
+
const u = new URL(url);
|
|
5
|
+
return u.origin !== window.location.origin;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
*
|
|
13
|
+
* 为外部 URL 添加 UTM 追踪参数
|
|
14
|
+
* @param url - 原始 URL 地址
|
|
15
|
+
* @returns 添加 UTM 参数后的 URL
|
|
16
|
+
* @see https://team.arcblock.io/comment/discussions/7504c5ce-7453-4223-a539-27620efcf38e#6fd5adb6-49ab-4ead-9233-240aa53d1058
|
|
17
|
+
*
|
|
18
|
+
*/
|
|
19
|
+
export function getUTMUrl(url) {
|
|
20
|
+
try {
|
|
21
|
+
if (!isExternal(url)) {
|
|
22
|
+
return url;
|
|
23
|
+
}
|
|
24
|
+
const type = window.__discussKitPostType__ ?? 'discussion';
|
|
25
|
+
const existsQueryParams = getQuery(url);
|
|
26
|
+
const queryParams = {
|
|
27
|
+
// 自身站点的 hostname
|
|
28
|
+
utm_source: window.location.hostname,
|
|
29
|
+
// 标记导航点击
|
|
30
|
+
utm_medium: `${type}_link`,
|
|
31
|
+
// 全局导航统一标签
|
|
32
|
+
utm_campaign: 'default',
|
|
33
|
+
// 目标站点标识
|
|
34
|
+
utm_content: new URL(url).hostname,
|
|
35
|
+
...existsQueryParams,
|
|
36
|
+
};
|
|
37
|
+
return withQuery(url, queryParams);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('Failed to generate UTM URL:', error);
|
|
41
|
+
return url;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { describe, expect, it, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { getUTMUrl } from './utm';
|
|
3
|
+
// Mock window 对象
|
|
4
|
+
const mockWindow = {
|
|
5
|
+
location: {
|
|
6
|
+
origin: 'https://example.com',
|
|
7
|
+
hostname: 'example.com',
|
|
8
|
+
},
|
|
9
|
+
__discussKitPostType__: 'discussion',
|
|
10
|
+
};
|
|
11
|
+
describe('getUTMUrl', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// @ts-ignore
|
|
14
|
+
global.window = mockWindow;
|
|
15
|
+
vi.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// @ts-ignore
|
|
19
|
+
delete global.window;
|
|
20
|
+
});
|
|
21
|
+
describe('Internal Link Handling', () => {
|
|
22
|
+
it('should return original URL when URL is internal link', () => {
|
|
23
|
+
const internalUrl = 'https://example.com/path';
|
|
24
|
+
expect(getUTMUrl(internalUrl)).toBe(internalUrl);
|
|
25
|
+
});
|
|
26
|
+
it('should return original URL when URL is relative path', () => {
|
|
27
|
+
const relativeUrl = '/path/to/page';
|
|
28
|
+
expect(getUTMUrl(relativeUrl)).toBe(relativeUrl);
|
|
29
|
+
});
|
|
30
|
+
it('should return original URL when URL format is invalid', () => {
|
|
31
|
+
const invalidUrl = 'not-a-valid-url';
|
|
32
|
+
expect(getUTMUrl(invalidUrl)).toBe(invalidUrl);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
describe('External Link UTM Parameter Addition', () => {
|
|
36
|
+
it('should add UTM parameters to external URL', () => {
|
|
37
|
+
const externalUrl = 'https://external.com/path';
|
|
38
|
+
const result = getUTMUrl(externalUrl);
|
|
39
|
+
expect(result).toContain('utm_source=example.com');
|
|
40
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
41
|
+
expect(result).toContain('utm_campaign=default');
|
|
42
|
+
expect(result).toContain('utm_content=external.com');
|
|
43
|
+
});
|
|
44
|
+
it('should preserve existing query parameters', () => {
|
|
45
|
+
const urlWithParams = 'https://external.com/path?existing=param&another=value';
|
|
46
|
+
const result = getUTMUrl(urlWithParams);
|
|
47
|
+
expect(result).toContain('existing=param');
|
|
48
|
+
expect(result).toContain('another=value');
|
|
49
|
+
expect(result).toContain('utm_source=example.com');
|
|
50
|
+
});
|
|
51
|
+
it('should handle different port numbers', () => {
|
|
52
|
+
const urlWithPort = 'https://external.com:8080/path';
|
|
53
|
+
const result = getUTMUrl(urlWithPort);
|
|
54
|
+
// URL constructor treats port as part of hostname, but ufo library may only take hostname
|
|
55
|
+
expect(result).toContain('utm_content=external.com');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('Different Post Type Handling', () => {
|
|
59
|
+
it('should use default value when __discussKitPostType__ is undefined', () => {
|
|
60
|
+
// @ts-ignore
|
|
61
|
+
delete global.window.__discussKitPostType__;
|
|
62
|
+
const externalUrl = 'https://external.com/path';
|
|
63
|
+
const result = getUTMUrl(externalUrl);
|
|
64
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
65
|
+
});
|
|
66
|
+
it('should use specified post type', () => {
|
|
67
|
+
// @ts-ignore
|
|
68
|
+
global.window.__discussKitPostType__ = 'blog';
|
|
69
|
+
const externalUrl = 'https://external.com/path';
|
|
70
|
+
const result = getUTMUrl(externalUrl);
|
|
71
|
+
expect(result).toContain('utm_medium=blog_link');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('Error Handling', () => {
|
|
75
|
+
it('should return original URL when error occurs', () => {
|
|
76
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
77
|
+
// Mock scenario that causes error - break window.location.hostname access
|
|
78
|
+
const originalWindow = global.window;
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
global.window = {
|
|
81
|
+
location: {
|
|
82
|
+
origin: 'https://example.com',
|
|
83
|
+
get hostname() {
|
|
84
|
+
throw new Error('Hostname access error');
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
__discussKitPostType__: 'discussion',
|
|
88
|
+
};
|
|
89
|
+
const url = 'https://external.com/path';
|
|
90
|
+
const result = getUTMUrl(url);
|
|
91
|
+
expect(result).toBe(url);
|
|
92
|
+
expect(consoleSpy).toHaveBeenCalledWith('Failed to generate UTM URL:', expect.any(Error));
|
|
93
|
+
// @ts-ignore
|
|
94
|
+
global.window = originalWindow;
|
|
95
|
+
consoleSpy.mockRestore();
|
|
96
|
+
});
|
|
97
|
+
it('should handle empty string URL', () => {
|
|
98
|
+
const result = getUTMUrl('');
|
|
99
|
+
expect(result).toBe('');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
describe('URL Construction Validation', () => {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
// Reset window object state
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
global.window = {
|
|
107
|
+
location: {
|
|
108
|
+
origin: 'https://example.com',
|
|
109
|
+
hostname: 'example.com',
|
|
110
|
+
},
|
|
111
|
+
__discussKitPostType__: 'discussion',
|
|
112
|
+
};
|
|
113
|
+
});
|
|
114
|
+
it('should generate valid URL format', () => {
|
|
115
|
+
const externalUrl = 'https://external.com/path';
|
|
116
|
+
const result = getUTMUrl(externalUrl);
|
|
117
|
+
// Verify result is a valid URL
|
|
118
|
+
expect(() => new URL(result)).not.toThrow();
|
|
119
|
+
const parsedUrl = new URL(result);
|
|
120
|
+
expect(parsedUrl.searchParams.get('utm_source')).toBe('example.com');
|
|
121
|
+
expect(parsedUrl.searchParams.get('utm_medium')).toBe('discussion_link');
|
|
122
|
+
expect(parsedUrl.searchParams.get('utm_campaign')).toBe('default');
|
|
123
|
+
expect(parsedUrl.searchParams.get('utm_content')).toBe('external.com');
|
|
124
|
+
});
|
|
125
|
+
it('should correctly handle URLs with existing UTM parameters', () => {
|
|
126
|
+
const urlWithUTM = 'https://external.com/path?utm_source=existing&other=param';
|
|
127
|
+
const result = getUTMUrl(urlWithUTM);
|
|
128
|
+
const parsedUrl = new URL(result);
|
|
129
|
+
// According to code logic, existsQueryParams comes last, overriding new UTM parameters
|
|
130
|
+
expect(parsedUrl.searchParams.get('utm_source')).toBe('existing');
|
|
131
|
+
expect(parsedUrl.searchParams.get('other')).toBe('param');
|
|
132
|
+
// Verify other UTM parameters are correctly added
|
|
133
|
+
expect(result).toContain('utm_medium=discussion_link');
|
|
134
|
+
expect(result).toContain('utm_campaign=default');
|
|
135
|
+
expect(result).toContain('utm_content=external.com');
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
describe('Special Character and Encoding Handling', () => {
|
|
139
|
+
it('should correctly handle hostname with special characters', () => {
|
|
140
|
+
// @ts-ignore
|
|
141
|
+
global.window.location.hostname = 'test-site.co.uk';
|
|
142
|
+
const externalUrl = 'https://external.com/path';
|
|
143
|
+
const result = getUTMUrl(externalUrl);
|
|
144
|
+
expect(result).toContain('utm_source=test-site.co.uk');
|
|
145
|
+
});
|
|
146
|
+
it('should correctly handle Chinese domain names', () => {
|
|
147
|
+
const externalUrl = 'https://测试.com/path';
|
|
148
|
+
const result = getUTMUrl(externalUrl);
|
|
149
|
+
// Verify result contains encoded content
|
|
150
|
+
expect(result).toContain('utm_content=xn--');
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -5,6 +5,7 @@ import { $getNearestNodeFromDOMNode, $getSelection, $isElementNode, $isRangeSele
|
|
|
5
5
|
import { useEffect } from 'react';
|
|
6
6
|
import { useParams, useNavigate } from 'react-router-dom';
|
|
7
7
|
import { useEditorConfig } from '../../../config';
|
|
8
|
+
import { getUTMUrl } from '../../../libs/utm';
|
|
8
9
|
function findMatchingDOM(startNode, predicate) {
|
|
9
10
|
let node = startNode;
|
|
10
11
|
while (node != null) {
|
|
@@ -93,7 +94,7 @@ export default function LexicalClickableLinkPlugin({ newTab = true }) {
|
|
|
93
94
|
}
|
|
94
95
|
}
|
|
95
96
|
else {
|
|
96
|
-
window.open(url, newTab || isMiddle || event.metaKey || event.ctrlKey || urlTarget === '_blank' ? '_blank' : '_self');
|
|
97
|
+
window.open(getUTMUrl(url), newTab || isMiddle || event.metaKey || event.ctrlKey || urlTarget === '_blank' ? '_blank' : '_self');
|
|
97
98
|
}
|
|
98
99
|
event.preventDefault();
|
|
99
100
|
};
|
package/lib/main/viewer/index.js
CHANGED
|
@@ -22,5 +22,5 @@ export function BlockletEditorViewer({ editorState, markdown, ...props }) {
|
|
|
22
22
|
},
|
|
23
23
|
theme,
|
|
24
24
|
};
|
|
25
|
-
return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(EditorRoot, { className: cx(props.className, 'be-shell'), "data-mode": muiTheme.palette.mode, children: _jsx(EditorViewer, { ...props }) }) }
|
|
25
|
+
return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(EditorRoot, { className: cx(props.className, 'be-shell'), "data-mode": muiTheme.palette.mode, children: _jsx(EditorViewer, { ...props }) }) }));
|
|
26
26
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@blocklet/editor",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.95",
|
|
4
4
|
"main": "lib/index.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"ufo": "^1.5.4",
|
|
74
74
|
"url-join": "^4.0.1",
|
|
75
75
|
"zustand": "^4.5.5",
|
|
76
|
-
"@blocklet/pdf": "2.4.
|
|
76
|
+
"@blocklet/pdf": "2.4.95"
|
|
77
77
|
},
|
|
78
78
|
"devDependencies": {
|
|
79
79
|
"@babel/core": "^7.25.2",
|