mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
CB-4541 refactor: tree (#2326)
* CB-4541 refactor: tree * CB-4541 chore: revert test code
This commit is contained in:
@@ -56,8 +56,8 @@ type DatabaseObjectInfo {
|
||||
type NavigatorNodeInfo {
|
||||
# Node ID - generally a full path to the node from root of tree
|
||||
id: ID!
|
||||
# # Node URI - a unique path to a node including all parent nodes
|
||||
# uri: ID! @since(version: "23.3.1")
|
||||
# Node URI - a unique path to a node including all parent nodes
|
||||
uri: ID! @since(version: "23.3.1")
|
||||
# Node human readable name
|
||||
name: String
|
||||
#Node full name
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
.treeNodeNested {
|
||||
box-sizing: border-box;
|
||||
padding-left: 8px;
|
||||
display: none;
|
||||
|
||||
&.root {
|
||||
padding: 0;
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createDataContext } from '@cloudbeaver/core-data-context';
|
||||
|
||||
export const DATA_CONTEXT_NAV_NODE_ID = createDataContext<string>('nav-node-id');
|
||||
@@ -5,6 +5,7 @@ export * from './NodesManager/extensions/IObjectNavNodeProvider';
|
||||
export * from './NodesManager/DBObjectResource';
|
||||
export * from './NodesManager/DATA_CONTEXT_ACTIVE_NODE';
|
||||
export * from './NodesManager/DATA_CONTEXT_NAV_NODE';
|
||||
export * from './NodesManager/DATA_CONTEXT_NAV_NODE_ID';
|
||||
export * from './NodesManager/DATA_CONTEXT_NAV_NODES';
|
||||
export * from './NodesManager/NavNodeInfoResource';
|
||||
export * from './NodesManager/NavNodeManagerService';
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export type TreeDataTransformer<T> = (nodeId: string, data: T) => T;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { TreeDataTransformer } from './TreeDataTransformer';
|
||||
|
||||
export function applyTransforms<T>(id: string, data: T, transformers?: TreeDataTransformer<T>[]) {
|
||||
if (!transformers) {
|
||||
return data;
|
||||
}
|
||||
|
||||
for (const transformer of transformers) {
|
||||
data = transformer(id, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { INode } from '../INode';
|
||||
import type { INodeState } from '../INodeState';
|
||||
import type { TreeDataTransformer } from './TreeDataTransformer';
|
||||
|
||||
export function rootNodeStateTransformer(root: string): TreeDataTransformer<INodeState> {
|
||||
return function rootNodeStateTransformer(nodeId, data) {
|
||||
if (nodeId === root) {
|
||||
return {
|
||||
...data,
|
||||
expanded: true,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
|
||||
export function rootNodeTransformer(root: string): TreeDataTransformer<INode> {
|
||||
return function rootNodeTransformer(nodeId, data) {
|
||||
if (nodeId === root) {
|
||||
return {
|
||||
...data,
|
||||
leaf: false,
|
||||
};
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface INode {
|
||||
name: string;
|
||||
tooltip?: string;
|
||||
icon?: string;
|
||||
leaf?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface INodeComponentBaseProps {
|
||||
nodeId: string;
|
||||
offsetHeight: number;
|
||||
}
|
||||
|
||||
export interface INodeComponentProps extends INodeComponentBaseProps {
|
||||
childrenRenderer: React.FC<INodeComponentBaseProps>;
|
||||
}
|
||||
|
||||
export type NodeComponent = React.FC<INodeComponentProps>;
|
||||
|
||||
export type INodeRenderer = (nodeId: string) => NodeComponent | null;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
export interface INodeState {
|
||||
selected: boolean;
|
||||
expanded: boolean;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { TreeNode } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { TreeContext } from './contexts/TreeContext';
|
||||
import { TreeDataContext } from './contexts/TreeDataContext';
|
||||
import type { NodeComponent } from './INodeRenderer';
|
||||
import { NodeControl } from './NodeControl';
|
||||
import { useNodeDnD } from './useNodeDnD';
|
||||
|
||||
export const Node: NodeComponent = observer(function Node({ nodeId, offsetHeight, childrenRenderer }) {
|
||||
const tree = useContext(TreeContext)!;
|
||||
const data = useContext(TreeDataContext)!;
|
||||
|
||||
const { expanded, selected } = data.getState(nodeId);
|
||||
|
||||
const dndData = useNodeDnD(nodeId, () => {
|
||||
if (!selected) {
|
||||
tree.selectNode(nodeId, true);
|
||||
}
|
||||
});
|
||||
|
||||
function handleOpen() {
|
||||
return tree.openNode(nodeId);
|
||||
}
|
||||
|
||||
function handleToggleExpand() {
|
||||
return tree.expandNode(nodeId, !expanded);
|
||||
}
|
||||
|
||||
function handleSelect() {
|
||||
tree.selectNode(nodeId, !selected);
|
||||
}
|
||||
|
||||
const NavigationTreeChildrenNew = childrenRenderer;
|
||||
|
||||
return (
|
||||
<TreeNode selected={selected} expanded={expanded} onExpand={handleToggleExpand} onOpen={handleOpen} onSelect={handleSelect}>
|
||||
<NodeControl ref={dndData.setTargetRef} nodeId={nodeId} />
|
||||
{expanded && <NavigationTreeChildrenNew nodeId={nodeId} offsetHeight={offsetHeight + tree.getNodeHeight(nodeId)} />}
|
||||
</TreeNode>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext, useId } from 'react';
|
||||
|
||||
import { getComputed, TreeNodeNested } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { NodeSizeCacheContext } from './contexts/NodeSizeCacheContext';
|
||||
import { TreeDataContext } from './contexts/TreeDataContext';
|
||||
import { TreeVirtualizationContext } from './contexts/TreeVirtualizationContext';
|
||||
import { NodeRenderer } from './NodeRenderer';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
offsetHeight: number;
|
||||
root?: boolean;
|
||||
}
|
||||
|
||||
const OVERSCAN = 128;
|
||||
|
||||
function getPositionWithOverscan(position: number, forward: boolean) {
|
||||
if (forward) {
|
||||
return position - (position % OVERSCAN) + OVERSCAN;
|
||||
}
|
||||
|
||||
return position - (position % OVERSCAN);
|
||||
}
|
||||
|
||||
const NodeChildrenObserved = observer<Props>(function NodeChildren({ nodeId, offsetHeight, root }) {
|
||||
const data = useContext(TreeDataContext)!;
|
||||
const optimization = useContext(TreeVirtualizationContext)!;
|
||||
const sizeCache = useContext(NodeSizeCacheContext)!;
|
||||
const firstId = useId();
|
||||
const lastId = useId();
|
||||
const viewPortFrom = getComputed(() => getPositionWithOverscan(optimization.viewPort.from, false)) - offsetHeight;
|
||||
const viewPortTo = getComputed(() => getPositionWithOverscan(optimization.viewPort.to, true)) - offsetHeight;
|
||||
|
||||
const children = data.getChildren(nodeId);
|
||||
|
||||
function renderChildren() {
|
||||
let offset = 0;
|
||||
let postFillHeight = 0;
|
||||
|
||||
const elements = [];
|
||||
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
const child = children[i];
|
||||
const size = sizeCache.getSize(child);
|
||||
|
||||
if (offset + size < viewPortFrom) {
|
||||
offset += size;
|
||||
} else if (offset < viewPortTo) {
|
||||
if (offset > 0 && elements.length === 0) {
|
||||
elements.push(<div key={firstId} style={{ height: offset }} />);
|
||||
}
|
||||
|
||||
elements.push(<NodeRenderer key={child} nodeId={child} offsetHeight={offset + offsetHeight} childrenRenderer={NodeChildrenObserved} />);
|
||||
offset += size;
|
||||
} else {
|
||||
postFillHeight += size;
|
||||
}
|
||||
}
|
||||
|
||||
if (postFillHeight > 0) {
|
||||
elements.push(<div key={lastId} style={{ height: postFillHeight }} />);
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
return <TreeNodeNested root={root}>{renderChildren()}</TreeNodeNested>;
|
||||
});
|
||||
|
||||
export const NodeChildren = NodeChildrenObserved;
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { forwardRef, useContext } from 'react';
|
||||
|
||||
import { TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { TreeContext } from './contexts/TreeContext';
|
||||
import { TreeDataContext } from './contexts/TreeDataContext';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
export const NodeControl = observer<Props, HTMLDivElement>(
|
||||
forwardRef(function NodeControl({ nodeId }, ref) {
|
||||
const data = useContext(TreeDataContext)!;
|
||||
const tree = useContext(TreeContext)!;
|
||||
|
||||
const node = data.getNode(nodeId);
|
||||
const height = tree.getNodeHeight(nodeId);
|
||||
|
||||
return (
|
||||
<TreeNodeControl ref={ref} style={{ height }}>
|
||||
<TreeNodeExpand leaf={node.leaf} />
|
||||
<TreeNodeIcon icon={node.icon} />
|
||||
<TreeNodeName title={node.tooltip}>{node.name}</TreeNodeName>
|
||||
</TreeNodeControl>
|
||||
);
|
||||
}),
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { TreeContext } from './contexts/TreeContext';
|
||||
import type { NodeComponent } from './INodeRenderer';
|
||||
import { Node } from './Node';
|
||||
|
||||
export const NodeRenderer: NodeComponent = observer(function NodeRenderer(props) {
|
||||
const tree = useContext(TreeContext)!;
|
||||
const NodeComponent = tree.getNodeComponent(props.nodeId) ?? Node;
|
||||
|
||||
return <NodeComponent {...props} />;
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import type { IDataContext } from '@cloudbeaver/core-data-context';
|
||||
|
||||
import { NodeSizeCacheContext } from './contexts/NodeSizeCacheContext';
|
||||
import { TreeContext } from './contexts/TreeContext';
|
||||
import { TreeDataContext } from './contexts/TreeDataContext';
|
||||
import { TreeDnDContext } from './contexts/TreeDnDContext';
|
||||
import { TreeVirtualizationContext } from './contexts/TreeVirtualizationContext';
|
||||
import type { INodeRenderer } from './INodeRenderer';
|
||||
import { NodeChildren } from './NodeChildren';
|
||||
import { useNodeSizeCache } from './useNodeSizeCache';
|
||||
import { useTree } from './useTree';
|
||||
import type { ITreeData } from './useTreeData';
|
||||
import { useTreeDnD } from './useTreeDnD';
|
||||
import { useTreeVirtualization } from './useTreeVirtualization';
|
||||
|
||||
export interface NavigationTreeNewProps {
|
||||
data: ITreeData;
|
||||
nodeRenderers?: INodeRenderer[];
|
||||
onNodeDoubleClick?(id: string): void | Promise<void>;
|
||||
getNodeDnDContext?(id: string, context: IDataContext): void;
|
||||
getNodeHeight(id: string): number;
|
||||
}
|
||||
|
||||
export const Tree = observer<NavigationTreeNewProps>(function Tree({ data, nodeRenderers, onNodeDoubleClick, getNodeDnDContext, getNodeHeight }) {
|
||||
const tree = useTree({
|
||||
data,
|
||||
nodeRenderers,
|
||||
onNodeDoubleClick,
|
||||
getNodeHeight,
|
||||
});
|
||||
const mountOptimization = useTreeVirtualization();
|
||||
const elementsSizeCache = useNodeSizeCache(tree, data);
|
||||
const treeDnD = useTreeDnD({
|
||||
getContext: getNodeDnDContext,
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={mountOptimization.setRootRef} style={{ overflow: 'auto', position: 'relative' }}>
|
||||
<NodeSizeCacheContext.Provider value={elementsSizeCache}>
|
||||
<TreeDataContext.Provider value={data}>
|
||||
<TreeVirtualizationContext.Provider value={mountOptimization}>
|
||||
<TreeContext.Provider value={tree}>
|
||||
<TreeDnDContext.Provider value={treeDnD}>
|
||||
<NodeChildren nodeId={data.rootId} offsetHeight={0} root />
|
||||
</TreeDnDContext.Provider>
|
||||
</TreeContext.Provider>
|
||||
</TreeVirtualizationContext.Provider>
|
||||
</TreeDataContext.Provider>
|
||||
</NodeSizeCacheContext.Provider>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { importLazyComponent } from '@cloudbeaver/core-utils';
|
||||
|
||||
export const Tree = importLazyComponent(() => import('./Tree').then(m => m.Tree));
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import type { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { INodeState } from './INodeState';
|
||||
|
||||
export type TreeState = MetadataMap<string, INodeState>;
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { INodeSizeCache } from '../useNodeSizeCache';
|
||||
|
||||
export const NodeSizeCacheContext = createContext<INodeSizeCache | undefined>(undefined);
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ITree } from '../useTree';
|
||||
|
||||
export const TreeContext = createContext<ITree | undefined>(undefined);
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ITreeData } from '../useTreeData';
|
||||
|
||||
export const TreeDataContext = createContext<ITreeData | undefined>(undefined);
|
||||
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ITreeDnD } from '../useTreeDnD';
|
||||
|
||||
export const TreeDnDContext = createContext<ITreeDnD | undefined>(undefined);
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { createContext } from 'react';
|
||||
|
||||
import type { ITreeVirtualization } from '../useTreeVirtualization';
|
||||
|
||||
export const TreeVirtualizationContext = createContext<ITreeVirtualization | undefined>(undefined);
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { useDataContext } from '@cloudbeaver/core-data-context';
|
||||
import { useDNDData } from '@cloudbeaver/core-ui';
|
||||
|
||||
import { TreeDnDContext } from './contexts/TreeDnDContext';
|
||||
|
||||
export function useNodeDnD(nodeId: string, onDragStart: () => void) {
|
||||
const treeDnD = useContext(TreeDnDContext)!;
|
||||
const context = useDataContext();
|
||||
|
||||
const dndData = useDNDData(context, {
|
||||
canDrag: () => true,
|
||||
onDragStart: () => {
|
||||
treeDnD.getContext(nodeId, context);
|
||||
onDragStart();
|
||||
},
|
||||
onDragEnd: () => {
|
||||
treeDnD.getContext(nodeId, context);
|
||||
},
|
||||
});
|
||||
|
||||
return dndData;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { computed, type IComputedValue } from 'mobx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { ITree } from './useTree';
|
||||
import type { ITreeData } from './useTreeData';
|
||||
|
||||
export interface INodeSizeCache {
|
||||
getSize(id: string): number;
|
||||
}
|
||||
|
||||
export function useNodeSizeCache(tree: ITree, treeData: ITreeData): INodeSizeCache {
|
||||
const [sizeRangeCache] = useState(
|
||||
() =>
|
||||
new MetadataMap<string, IComputedValue<number>>((id, metadata) =>
|
||||
computed(() => {
|
||||
let size = tree.getNodeHeight(id);
|
||||
const expanded = treeData.getState(id).expanded;
|
||||
|
||||
if (expanded) {
|
||||
const children = treeData.getChildren(id);
|
||||
|
||||
for (const child of children) {
|
||||
size += metadata.get(child).get();
|
||||
}
|
||||
}
|
||||
|
||||
return size;
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return useObjectRef(
|
||||
() => ({
|
||||
getSize(id: string): number {
|
||||
return sizeRangeCache.get(id).get();
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observable } from 'mobx';
|
||||
|
||||
import { useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import type { INodeRenderer, NodeComponent } from './INodeRenderer';
|
||||
import type { ITreeData } from './useTreeData';
|
||||
|
||||
interface IOptions {
|
||||
data: ITreeData;
|
||||
nodeRenderers?: INodeRenderer[];
|
||||
onNodeDoubleClick?(id: string): void | Promise<void>;
|
||||
getNodeHeight(id: string): number;
|
||||
}
|
||||
|
||||
export interface ITree {
|
||||
getNodeComponent(id: string): NodeComponent | null;
|
||||
getNodeHeight(id: string): number;
|
||||
|
||||
openNode(id: string): Promise<void>;
|
||||
expandNode(id: string, state: boolean): Promise<void>;
|
||||
selectNode(id: string, state: boolean): void;
|
||||
}
|
||||
|
||||
export function useTree(options: IOptions): ITree {
|
||||
options = useObservableRef(options, {
|
||||
data: observable.ref,
|
||||
nodeRenderers: observable.ref,
|
||||
onNodeDoubleClick: observable.ref,
|
||||
getNodeHeight: observable.ref,
|
||||
});
|
||||
|
||||
const data = useObservableRef(
|
||||
() => ({
|
||||
getNodeComponent(id: string): NodeComponent | null {
|
||||
if (!options.nodeRenderers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const renderer of options.nodeRenderers) {
|
||||
const component = renderer(id);
|
||||
|
||||
if (component) {
|
||||
return component;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
getNodeHeight(id: string): number {
|
||||
return options.getNodeHeight(id);
|
||||
},
|
||||
async openNode(id: string) {
|
||||
await options.onNodeDoubleClick?.(id);
|
||||
},
|
||||
async expandNode(id: string, state: boolean) {
|
||||
try {
|
||||
options.data.updateState(id, { expanded: state });
|
||||
if (state) {
|
||||
await options.data.load(id, true);
|
||||
const children = options.data.getChildren(id);
|
||||
|
||||
if (children.length === 0) {
|
||||
options.data.updateState(id, { expanded: false });
|
||||
}
|
||||
}
|
||||
} catch (exception) {}
|
||||
},
|
||||
selectNode(id: string, state: boolean) {
|
||||
options.data.updateState(id, { selected: state });
|
||||
},
|
||||
}),
|
||||
{},
|
||||
{},
|
||||
);
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { computed, IComputedValue, observable } from 'mobx';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import { applyTransforms } from './DataTransformers/applyTransforms';
|
||||
import { rootNodeStateTransformer, rootNodeTransformer } from './DataTransformers/rootTransformers';
|
||||
import type { TreeDataTransformer } from './DataTransformers/TreeDataTransformer';
|
||||
import type { INode } from './INode';
|
||||
import type { INodeState } from './INodeState';
|
||||
import type { TreeState } from './TreeState';
|
||||
import { useTreeState } from './useTreeState';
|
||||
|
||||
interface IOptions {
|
||||
rootId: string;
|
||||
externalState?: TreeState;
|
||||
getNode(id: string): INode;
|
||||
getChildren: (node: string) => string[];
|
||||
load(nodeId: string, manual: boolean): Promise<void>;
|
||||
|
||||
childrenTransformers?: TreeDataTransformer<string[]>[];
|
||||
nodeTransformers?: TreeDataTransformer<INode>[];
|
||||
stateTransformers?: TreeDataTransformer<INodeState>[];
|
||||
}
|
||||
|
||||
export interface ITreeData {
|
||||
rootId: string;
|
||||
|
||||
getNode(id: string): INode;
|
||||
getChildren: (node: string) => string[];
|
||||
getState(id: string): Readonly<INodeState>;
|
||||
|
||||
updateState(id: string, state: Partial<INodeState>): void;
|
||||
load(nodeId: string, manual: boolean): Promise<void>;
|
||||
update(): Promise<void>;
|
||||
}
|
||||
|
||||
export function useTreeData(options: IOptions): ITreeData {
|
||||
options = useObservableRef(
|
||||
{
|
||||
...options,
|
||||
childrenTransformers: [...(options.childrenTransformers || [])],
|
||||
nodeTransformers: [...(options.nodeTransformers || [])],
|
||||
stateTransformers: [...(options.stateTransformers || [])],
|
||||
},
|
||||
{
|
||||
rootId: observable.ref,
|
||||
getNode: observable.ref,
|
||||
getChildren: observable.ref,
|
||||
load: observable.ref,
|
||||
|
||||
childrenTransformers: observable.ref,
|
||||
nodeTransformers: observable.ref,
|
||||
stateTransformers: observable.ref,
|
||||
},
|
||||
);
|
||||
const state = useTreeState(options.externalState);
|
||||
const [nodeCache] = useState(
|
||||
() =>
|
||||
new MetadataMap<string, IComputedValue<INode>>(id =>
|
||||
computed(() => applyTransforms(id, options.getNode(id), [rootNodeTransformer(options.rootId), ...(options.nodeTransformers || [])])),
|
||||
),
|
||||
);
|
||||
const [childrenCache] = useState(
|
||||
() =>
|
||||
new MetadataMap<string, IComputedValue<string[]>>(id =>
|
||||
computed(() => applyTransforms(id, options.getChildren(id), options.childrenTransformers)),
|
||||
),
|
||||
);
|
||||
const [stateCache] = useState(
|
||||
() =>
|
||||
new MetadataMap<string, IComputedValue<INodeState>>(id =>
|
||||
computed(() => applyTransforms(id, state.getState(id), [rootNodeStateTransformer(options.rootId), ...(options.stateTransformers || [])])),
|
||||
),
|
||||
);
|
||||
|
||||
const treeData = useObservableRef(
|
||||
() => ({
|
||||
getNode(id: string): INode {
|
||||
return nodeCache.get(id).get();
|
||||
},
|
||||
getChildren(nodeId: string): string[] {
|
||||
return childrenCache.get(nodeId).get();
|
||||
},
|
||||
getState(id: string): Readonly<INodeState> {
|
||||
return stateCache.get(id).get();
|
||||
},
|
||||
updateState(id: string, state: Partial<INodeState>) {
|
||||
this.state.updateState(id, state);
|
||||
},
|
||||
async load(nodeId: string, manual: boolean) {
|
||||
await options.load(nodeId, manual);
|
||||
},
|
||||
async update() {
|
||||
const nodes = [this.rootId];
|
||||
|
||||
while (nodes.length > 0) {
|
||||
const nodeId = nodes.shift()!;
|
||||
const state = this.state.getState(nodeId);
|
||||
|
||||
if (!state.expanded) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await options.load(nodeId, false);
|
||||
|
||||
const children = this.getChildren(nodeId);
|
||||
|
||||
if (children.length === 0) {
|
||||
this.state.updateState(nodeId, { expanded: false });
|
||||
continue;
|
||||
}
|
||||
|
||||
nodes.push(...children);
|
||||
}
|
||||
},
|
||||
}),
|
||||
{ state: observable.ref, rootId: observable.ref },
|
||||
{ state, rootId: options.rootId },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
treeData.update();
|
||||
}, [options.rootId]);
|
||||
|
||||
return treeData;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
import type { IDataContext } from '@cloudbeaver/core-data-context';
|
||||
|
||||
interface IOptions {
|
||||
getContext?(id: string, context: IDataContext): void;
|
||||
}
|
||||
|
||||
export interface ITreeDnD {
|
||||
getContext(id: string, context: IDataContext): void;
|
||||
canDrop(moveContext: IDataContext): boolean;
|
||||
}
|
||||
|
||||
export function useTreeDnD(options: IOptions): ITreeDnD {
|
||||
options = useObjectRef(options);
|
||||
|
||||
return useObjectRef(
|
||||
() => ({
|
||||
getContext(id: string, context: IDataContext): void {
|
||||
options.getContext?.(id, context);
|
||||
},
|
||||
canDrop(moveContext: IDataContext): boolean {
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observable } from 'mobx';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useObservableRef } from '@cloudbeaver/core-blocks';
|
||||
import { MetadataMap } from '@cloudbeaver/core-utils';
|
||||
|
||||
import type { INodeState } from './INodeState';
|
||||
import type { TreeState } from './TreeState';
|
||||
|
||||
export interface ITreeState {
|
||||
getState(id: string): Readonly<INodeState>;
|
||||
updateState(id: string, state: Partial<INodeState>): void;
|
||||
}
|
||||
|
||||
export function useTreeState(externalState?: TreeState): ITreeState {
|
||||
const [innerState] = useState(
|
||||
() =>
|
||||
new MetadataMap<string, INodeState>(() =>
|
||||
observable({
|
||||
expanded: false,
|
||||
selected: false,
|
||||
showInFilter: false,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const state = externalState ?? innerState;
|
||||
|
||||
return useObservableRef(
|
||||
() => ({
|
||||
getState(id: string): Readonly<INodeState> {
|
||||
return state.get(id);
|
||||
},
|
||||
updateState(id: string, state: Partial<INodeState>): void {
|
||||
Object.assign(this.getState(id), state);
|
||||
},
|
||||
}),
|
||||
{
|
||||
state: observable.ref,
|
||||
},
|
||||
{ state },
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* CloudBeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020-2024 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
import { observable, runInAction } from 'mobx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useObjectRef } from '@cloudbeaver/core-blocks';
|
||||
|
||||
interface IPrivate extends ITreeVirtualization {
|
||||
observer: ResizeObserver | null;
|
||||
element: HTMLElement | null;
|
||||
handleScroll(event: Event): void;
|
||||
handleResize(): void;
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
export interface ITreeVirtualization {
|
||||
viewPort: { from: number; to: number };
|
||||
setRootRef(element: HTMLElement | null): void;
|
||||
}
|
||||
|
||||
export function useTreeVirtualization(): ITreeVirtualization {
|
||||
const mountOptimization = useObjectRef<IPrivate>(
|
||||
() => ({
|
||||
observer: null,
|
||||
element: null,
|
||||
viewPort: observable({ from: 0, to: 0 }),
|
||||
setRootRef(element: HTMLElement | null) {
|
||||
if (this.element === element) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
this.element = element;
|
||||
if (element) {
|
||||
this.observer = new ResizeObserver(this.handleResize);
|
||||
element.addEventListener('scroll', this.handleScroll);
|
||||
this.observer.observe(element);
|
||||
|
||||
runInAction(() => {
|
||||
this.viewPort.from = element.scrollTop;
|
||||
this.viewPort.to = element.scrollTop + element.clientHeight;
|
||||
});
|
||||
}
|
||||
},
|
||||
dispose() {
|
||||
if (this.element) {
|
||||
this.element.removeEventListener('scroll', this.handleScroll);
|
||||
}
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = null;
|
||||
}
|
||||
},
|
||||
handleScroll(event) {
|
||||
runInAction(() => {
|
||||
const target = event.target as HTMLElement;
|
||||
|
||||
this.viewPort.from = target.scrollTop;
|
||||
this.viewPort.to = target.scrollTop + target.clientHeight;
|
||||
});
|
||||
},
|
||||
handleResize() {
|
||||
runInAction(() => {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
this.viewPort.from = this.element.scrollTop;
|
||||
this.viewPort.to = this.element.scrollTop + this.element.clientHeight;
|
||||
});
|
||||
},
|
||||
}),
|
||||
false,
|
||||
['setRootRef', 'dispose', 'handleScroll', 'handleResize'],
|
||||
);
|
||||
|
||||
useEffect(() => () => mountOptimization.dispose(), []);
|
||||
|
||||
return mountOptimization;
|
||||
}
|
||||
@@ -36,6 +36,9 @@ export { default as NavigationNodeControlRendererStyles } from './NavigationTree
|
||||
export { default as NavigationNodeControlStyles } from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.m.css';
|
||||
|
||||
export * from './NavigationTree/NavigationTreeLoader';
|
||||
export * from './TreeNew/TreeLazy';
|
||||
export * from './TreeNew/useTreeData';
|
||||
export * from './TreeNew/INode';
|
||||
|
||||
export * from './NavigationTree/getNavigationTreeUserSettingsId';
|
||||
export * from './NodesManager/NavNodeView/IFolderTransform';
|
||||
|
||||
Reference in New Issue
Block a user