CB-4541 refactor: tree (#2326)

* CB-4541 refactor: tree

* CB-4541 chore: revert test code
This commit is contained in:
Alexey
2024-01-24 12:31:48 +03:00
committed by GitHub
parent 47f1267957
commit b0a34c7d75
30 changed files with 927 additions and 4 deletions
@@ -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';
@@ -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;
@@ -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;
}
@@ -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);
@@ -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';