Skip to content

Commit 0f7204f

Browse files
bytemainerha19
andauthored
feat: improve preference panel (#2089)
* feat: improve preference panel * fix: basic recyletree not work * fix: remove invalid useCallback * fix: remove useless tree model setup * feat: optimize tree behavior * feat: display all settings * refactor: use const enum * feat: optimize style * feat: default display pref name * feat: update style * feat: support scroll to preference * feat: support search in markdown * feat: add known mapping * feat: make search input clearable * feat: tree use min-resize * feat: leave bottom blank * refactor: use data-sp * refactor: rename func * refactor: improve code * fix: range changed correct * feat: optimize scroll * feat: optimize scroll * feat: optimize scroll * feat: always focus item * feat: optimize style * test: fix testcase * feat: reduce search time * test: fix e2e test * test: fix e2e * test: fix e2e * test: fix e2e test Co-authored-by: kuiwu <danwu.wdw@alibaba-inc.com>
1 parent 591149f commit 0f7204f

38 files changed

Lines changed: 1499 additions & 688 deletions

File tree

packages/components/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"react-custom-scrollbars": "^4.2.1",
3232
"react-lifecycles-compat": "^3.0.4",
3333
"react-virtualized-auto-sizer": "^1.0.2",
34+
"react-virtuoso": "^3.1.5",
3435
"react-window": "^1.8.5"
3536
},
3637
"devDependencies": {

packages/components/src/checkbox/style.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
cursor: pointer;
88

99
&-lump {
10-
margin-right: 4px;
10+
margin-right: 6px;
1111
position: relative;
1212
display: inline-flex;
1313
align-items: center;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { marked, Renderer } from 'marked';
2+
import React from 'react';
3+
4+
import { DATA_SET_COMMAND, IOpenerShape, RenderWrapper } from './render';
5+
6+
interface IMarkdownProps {
7+
value: string;
8+
renderer: marked.Renderer;
9+
opener: IOpenerShape;
10+
}
11+
12+
export const linkify = (href: string | null, title: string | null, text: string) =>
13+
`<a rel="noopener" ${DATA_SET_COMMAND}="${href}" title="${title ?? href}">${text}</a>`;
14+
15+
export class DefaultMarkedRenderer extends Renderer {
16+
link(href: string | null, title: string | null, text: string): string {
17+
return linkify(href, title, text);
18+
}
19+
}
20+
21+
export function Markdown(props: IMarkdownProps) {
22+
const parseMarkdown = (text: string, renderer: any) => marked.parse(text, { renderer });
23+
24+
const [htmlContent, setHtmlContent] = React.useState(parseMarkdown(props.value, props.renderer));
25+
26+
React.useEffect(() => {
27+
setHtmlContent(parseMarkdown(props.value, props.renderer));
28+
}, [props.renderer, props.value]);
29+
30+
return <RenderWrapper opener={props.opener} html={htmlContent}></RenderWrapper>;
31+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { RefObject, useEffect, useRef } from 'react';
2+
3+
import type { MaybePromise } from '@opensumi/ide-utils';
4+
5+
export interface IOpenerShape {
6+
open(uri: string): MaybePromise<boolean>;
7+
}
8+
9+
export const DATA_SET_COMMAND = 'data-command';
10+
11+
export const RenderWrapper = (props: { html: string; opener?: IOpenerShape }) => {
12+
const ref = useRef<HTMLDivElement | undefined>();
13+
const { html, opener } = props;
14+
15+
useEffect(() => {
16+
if (ref && ref.current) {
17+
ref.current.addEventListener('click', listenClick);
18+
}
19+
return () => {
20+
if (ref && ref.current) {
21+
ref.current.removeEventListener('click', listenClick);
22+
}
23+
};
24+
}, []);
25+
26+
/**
27+
* 拦截 a 标签的点击事件,触发 commands
28+
*/
29+
const listenClick = (event: PointerEvent) => {
30+
const target = event.target as HTMLElement;
31+
if (target.tagName.toLowerCase() === 'a' && target.hasAttribute(DATA_SET_COMMAND)) {
32+
const dataCommand = target.getAttribute(DATA_SET_COMMAND);
33+
if (dataCommand && opener) {
34+
opener.open(dataCommand);
35+
}
36+
}
37+
};
38+
39+
return (
40+
<div
41+
className='md-renderer-wrap'
42+
dangerouslySetInnerHTML={{ __html: html }}
43+
ref={ref as unknown as RefObject<HTMLDivElement>}
44+
></div>
45+
);
46+
};

packages/components/src/recycle-tree/basic/index.tsx

Lines changed: 72 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import cls from 'classnames';
1+
import throttle from 'lodash/throttle';
22
import React, { useCallback, useRef, useEffect, useState } from 'react';
33
import CtxMenuTrigger from 'react-ctxmenu-trigger';
44

@@ -25,6 +25,7 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
2525
itemHeight = 22,
2626
itemClassname,
2727
indent,
28+
baseIndent,
2829
containerClassname,
2930
onClick,
3031
onContextMenu,
@@ -38,6 +39,8 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
3839
onReady,
3940
contextMenus,
4041
contextMenuActuator,
42+
treeName,
43+
getItemClassName,
4144
}) => {
4245
const [showMenus, setShowMenus] = useState<{
4346
show: boolean;
@@ -49,35 +52,46 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
4952
}>({ show: false });
5053
const [menubarItems, setMenubarItems] = useState<IBasicTreeMenu[]>([]);
5154
const [model, setModel] = useState<BasicTreeModel | undefined>();
52-
const treeService = useRef<BasicTreeService>(new BasicTreeService(treeData, resolveChildren, sortComparator));
55+
const treeService = useRef<BasicTreeService>();
5356
const treeHandle = useRef<IRecycleTreeHandle>();
5457
const wrapperRef: React.RefObject<HTMLDivElement> = React.createRef();
5558

5659
const renderTreeNode = useCallback(
57-
(props: INodeRendererWrapProps) => (
58-
<BasicTreeNodeRenderer
59-
item={props.item as any}
60-
itemType={props.itemType}
61-
itemHeight={itemHeight}
62-
indent={indent}
63-
className={itemClassname}
64-
inlineMenus={inlineMenus}
65-
inlineMenuActuator={inlineMenuActuator}
66-
onClick={handleItemClick}
67-
onDbClick={handleItemDbClick}
68-
onContextMenu={handleContextMenu}
69-
onTwistierClick={handleTwistierClick}
70-
decorations={treeService.current.decorations.getDecorations(props.item as ITreeNodeOrCompositeTreeNode)}
71-
/>
72-
),
73-
[],
60+
(props: INodeRendererWrapProps) => {
61+
let _indent: number | undefined;
62+
if (baseIndent) {
63+
_indent = baseIndent;
64+
}
65+
if (indent) {
66+
_indent = (_indent ?? 0) + indent;
67+
}
68+
69+
return (
70+
<BasicTreeNodeRenderer
71+
item={props.item as any}
72+
itemType={props.itemType}
73+
itemHeight={itemHeight}
74+
indent={_indent}
75+
className={getItemClassName?.(props.item as any) ?? itemClassname}
76+
inlineMenus={inlineMenus}
77+
inlineMenuActuator={inlineMenuActuator}
78+
onClick={handleItemClick}
79+
onDbClick={handleItemDbClick}
80+
onContextMenu={handleContextMenu}
81+
onTwistierClick={handleTwistierClick}
82+
decorations={treeService.current?.decorations.getDecorations(props.item as ITreeNodeOrCompositeTreeNode)}
83+
/>
84+
);
85+
},
86+
[model],
7487
);
7588

7689
useEffect(() => {
77-
ensureLoaded();
78-
const disposable = treeService.current.onDidUpdateTreeModel(async (model?: BasicTreeModel) => {
79-
await model?.ensureReady;
80-
setModel(model);
90+
treeService.current = new BasicTreeService(treeData, resolveChildren, sortComparator, {
91+
treeName,
92+
});
93+
const disposable = treeService.current?.onDidUpdateTreeModel((model?: BasicTreeModel) => {
94+
ensureLoaded(model);
8195
});
8296
const handleBlur = () => {
8397
treeService.current?.enactiveFocusedDecoration();
@@ -91,30 +105,51 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
91105
};
92106
}, []);
93107

94-
const ensureLoaded = async () => {
95-
const model = treeService.current.model;
108+
useEffect(() => {
109+
treeService.current?.updateTreeData(treeData);
110+
}, [treeData]);
111+
112+
const ensureLoaded = async (model?: BasicTreeModel) => {
96113
if (model) {
97114
await model.ensureReady;
98115
}
99116
setModel(model);
100117
};
101118

102-
const handleTreeReady = useCallback((handle: IRecycleTreeHandle) => {
103-
if (onReady) {
104-
onReady(handle);
119+
const selectItem = async (item: BasicCompositeTreeNode | BasicTreeNode) => {
120+
treeService.current?.activeFocusedDecoration(item);
121+
if (BasicCompositeTreeNode.is(item)) {
122+
toggleDirectory(item);
105123
}
106-
treeHandle.current = handle;
107-
}, []);
124+
};
125+
126+
const handleTreeReady = useCallback(
127+
(handle: IRecycleTreeHandle) => {
128+
if (onReady) {
129+
onReady({
130+
...handle,
131+
selectItem,
132+
focusItem: async (nodePath: string) => {
133+
const path = `/${treeName}/${nodePath}`;
134+
await model?.ensureReady;
135+
const node = (await handle.ensureVisible(path, 'auto', true)) as BasicCompositeTreeNode;
136+
if (node) {
137+
treeService.current?.activeFocusedDecoration(node);
138+
}
139+
},
140+
});
141+
}
142+
treeHandle.current = handle;
143+
},
144+
[treeService.current],
145+
);
108146

109147
const handleItemClick = useCallback(
110148
(event: React.MouseEvent, item: BasicCompositeTreeNode | BasicTreeNode) => {
111-
treeService.current?.activeFocusedDecoration(item);
149+
selectItem(item);
112150
if (onClick) {
113151
onClick(event, item);
114152
}
115-
if (BasicCompositeTreeNode.is(item)) {
116-
toggleDirectory(item);
117-
}
118153
},
119154
[onClick],
120155
);
@@ -198,7 +233,7 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
198233
const handleOuterContextMenu = useCallback(
199234
(event: React.MouseEvent, item?: BasicCompositeTreeNode | BasicTreeNode) => {
200235
if (onContextMenu) {
201-
onContextMenu(event);
236+
onContextMenu(event, item);
202237
}
203238
},
204239
[],
@@ -270,7 +305,8 @@ export const BasicRecycleTree: React.FC<IBasicRecycleTreeProps> = ({
270305
itemHeight={itemHeight}
271306
model={model}
272307
onReady={handleTreeReady}
273-
className={cls(containerClassname)}
308+
className={containerClassname}
309+
leaveBottomBlank
274310
>
275311
{renderTreeNode}
276312
</RecycleTree>

packages/components/src/recycle-tree/basic/styles.less

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@
3232
.expansion_toggle {
3333
color: var(--kt-tree-activeSelectionForeground) !important;
3434
}
35+
.icon {
36+
color: var(--kt-tree-activeSelectionForeground) !important;
37+
}
3538
}
3639

3740
&.mod_actived {

packages/components/src/recycle-tree/basic/tree-node.define.ts

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,26 @@ import { ITree } from '../types';
33

44
import { IBasicTreeData } from './types';
55

6+
interface IBasicTreeRootOptions {
7+
treeName?: string;
8+
}
9+
610
export class BasicTreeRoot extends CompositeTreeNode {
711
private _raw: IBasicTreeData;
8-
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData) {
9-
super(tree, parent);
12+
constructor(
13+
tree: ITree,
14+
parent: BasicCompositeTreeNode | undefined,
15+
data: IBasicTreeData,
16+
basicTreeRootOptions = {} as IBasicTreeRootOptions,
17+
) {
18+
super(tree, parent, undefined, {
19+
treeName: basicTreeRootOptions.treeName,
20+
});
1021
this._raw = data;
1122
}
1223

1324
get name() {
14-
return `BasicTreeRoot_${this.id}`;
25+
return this.getMetadata('treeName') ?? `BasicTreeRoot_${this.id}`;
1526
}
1627

1728
get raw() {
@@ -28,11 +39,11 @@ export class BasicCompositeTreeNode extends CompositeTreeNode {
2839
private _raw: IBasicTreeData;
2940

3041
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData, id?: number) {
31-
super(tree, parent, undefined, {});
42+
super(tree, parent, undefined, {
43+
name: data.label,
44+
});
3245
this.isExpanded = data.expanded || false;
3346
this.id = id || this.id;
34-
// 每个节点应该拥有自己独立的路径,不存在重复性
35-
this.name = String(this.id);
3647
this._displayName = data.label;
3748
this._raw = data;
3849
}
@@ -67,10 +78,10 @@ export class BasicTreeNode extends TreeNode {
6778
private _raw: IBasicTreeData;
6879

6980
constructor(tree: ITree, parent: BasicCompositeTreeNode | undefined, data: IBasicTreeData, id?: number) {
70-
super(tree, parent, undefined, {});
81+
super(tree, parent, undefined, {
82+
name: data.label,
83+
});
7184
this.id = id || this.id;
72-
// 每个节点应该拥有自己独立的路径,不存在重复性
73-
this.name = String(this.id);
7485
this._displayName = data.label;
7586
this._raw = data;
7687
}

packages/components/src/recycle-tree/basic/tree-node.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,7 @@ export const BasicTreeNodeRenderer: React.FC<
6767
},
6868
[onClick, onTwistierClick],
6969
);
70-
71-
const paddingLeft = `${8 + (item.depth || 0) * (indent || 0) + (!BasicCompositeTreeNode.is(item) ? 20 : 0)}px`;
70+
const paddingLeft = `${(item.depth || 0) * (indent || 0)}px`;
7271

7372
const editorNodeStyle = {
7473
height: itemHeight,
@@ -78,7 +77,8 @@ export const BasicTreeNodeRenderer: React.FC<
7877

7978
const renderIcon = useCallback(
8079
(node: BasicCompositeTreeNode | BasicTreeNode) => (
81-
<Icon icon={node.icon} className={cls('icon', node.iconClassName)} style={{ maxHeight: itemHeight }} />
80+
// 图标的最大高度设置为 `itemHeight - 8`, 这样在视觉上看起来有一种 padding 的效果
81+
<Icon icon={node.icon} className={cls('icon', node.iconClassName)} style={{ maxHeight: itemHeight - 8 }} />
8282
),
8383
[],
8484
);

0 commit comments

Comments
 (0)