mirror of
https://github.com/dbeaver/cloudbeaver.git
synced 2026-09-24 16:04:36 +08:00
Merge pull request #191 from dbeaver/refactor/navigation-tree
refactor(core-app): CB-339 NavigationTree
This commit is contained in:
@@ -14,16 +14,23 @@ import { useService } from '@cloudbeaver/core-di';
|
||||
import { usePermission, EPermission } from '@cloudbeaver/core-root';
|
||||
import { useActiveView } from '@cloudbeaver/core-view';
|
||||
|
||||
import { ROOT_NODE_PATH } from '../shared/NodesManager/NavNodeInfoResource';
|
||||
import { useChildren } from '../shared/useChildren';
|
||||
import { NavigationTreeNode } from './NavigationTreeNode/NavigationTreeNode';
|
||||
import { NavigationNodeElement } from './NavigationTreeNode/NavigationNodeElement';
|
||||
import { NavigationTreeService } from './NavigationTreeService';
|
||||
|
||||
const navigationTreeStyles = css`
|
||||
tree {
|
||||
padding-top: 16px;
|
||||
inside-box {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-width: 240px;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
tree {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
padding-top: 16px;
|
||||
min-width: 100%;
|
||||
width: max-content;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@@ -32,7 +39,7 @@ const navigationTreeStyles = css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
& tree {
|
||||
& message {
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
@@ -56,31 +63,33 @@ export const NavigationTree = observer(function NavigationTree() {
|
||||
}
|
||||
|
||||
if (!nodeChildren.children || nodeChildren.children.length === 0) {
|
||||
if (nodeChildren.isLoading) {
|
||||
if (nodeChildren.isLoading()) {
|
||||
return styled(navigationTreeStyles)(
|
||||
<center as="div">
|
||||
<tree as="div"><Loader /></tree>
|
||||
</center>
|
||||
<inside-box as="div">
|
||||
<center as="div"><Loader /></center>
|
||||
</inside-box>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(navigationTreeStyles)(
|
||||
<center as="div">
|
||||
<tree as="div">
|
||||
<inside-box as="div">
|
||||
<center as="div">
|
||||
<message as="div">
|
||||
No connections.<br />
|
||||
Use the top menu to setup connection to your database.
|
||||
</message>
|
||||
</tree>
|
||||
</center>
|
||||
</center>
|
||||
</inside-box>
|
||||
);
|
||||
}
|
||||
|
||||
return styled(navigationTreeStyles)(
|
||||
<tree ref={ref} as="div" tabIndex={0}>
|
||||
{nodeChildren.children.map(id => (
|
||||
<NavigationTreeNode key={id} id={id} parentId={ROOT_NODE_PATH} />
|
||||
))}
|
||||
</tree>
|
||||
<inside-box as='div'>
|
||||
<tree ref={ref} as="div" tabIndex={0}>
|
||||
{nodeChildren.children.map(id => (
|
||||
<NavigationNodeElement key={id} nodeId={id} />
|
||||
))}
|
||||
</tree>
|
||||
</inside-box>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
|
||||
import { TreeNode } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { NavNode } from '../../shared/NodesManager/EntityTypes';
|
||||
import { NavigationNodeControl } from './NavigationNode/NavigationNodeControl';
|
||||
import { NavigationNodeNested } from './NavigationNode/NavigationNodeNested';
|
||||
import { useNavigationNode } from './useNavigationNode';
|
||||
|
||||
interface NavigationTreeNodeProps {
|
||||
node: NavNode;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const NavigationNode = observer(function NavigationNode({
|
||||
node,
|
||||
component,
|
||||
}: NavigationTreeNodeProps) {
|
||||
const {
|
||||
loading,
|
||||
selected,
|
||||
expanded,
|
||||
leaf,
|
||||
handleExpand,
|
||||
handleOpen,
|
||||
handleSelect,
|
||||
} = useNavigationNode(node);
|
||||
|
||||
return (
|
||||
<TreeNode
|
||||
loading={loading}
|
||||
selected={selected}
|
||||
expanded={expanded}
|
||||
leaf={leaf}
|
||||
onExpand={handleExpand}
|
||||
onOpen={handleOpen}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
<NavigationNodeControl node={node} />
|
||||
<NavigationNodeNested nodeId={node.id} component={component} />
|
||||
</TreeNode>
|
||||
);
|
||||
});
|
||||
-97
@@ -1,97 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import {
|
||||
MouseEvent, useCallback, PropsWithChildren
|
||||
} from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { Icon, StaticImage, Loader } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { NavNode } from '../../../shared/NodesManager/EntityTypes';
|
||||
import { TreeNodeMenu } from '../TreeNodeMenu/TreeNodeMenu';
|
||||
import { navigationNodeStyles } from './navigationNodeStyles';
|
||||
import { useNavigationNode } from './useNavigationNode';
|
||||
|
||||
type NodeProps = PropsWithChildren<{
|
||||
node: NavNode;
|
||||
isLoading: boolean;
|
||||
isLoaded: boolean;
|
||||
isOutdated: boolean;
|
||||
}>;
|
||||
|
||||
const KEY = {
|
||||
ENTER: 'Enter',
|
||||
};
|
||||
|
||||
export const NavigationNode = observer(function NavigationNode({
|
||||
node,
|
||||
isLoading,
|
||||
isLoaded,
|
||||
isOutdated,
|
||||
|
||||
children,
|
||||
}: NodeProps) {
|
||||
const controller = useNavigationNode(node, isLoading, isLoaded, isOutdated);
|
||||
const styles = useStyles(navigationNodeStyles);
|
||||
|
||||
const handleExpand = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
controller.handleExpand();
|
||||
},
|
||||
[controller.handleExpand]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(e: MouseEvent<HTMLDivElement>) => {
|
||||
controller.handleSelect(e.ctrlKey);
|
||||
},
|
||||
[controller.handleSelect]
|
||||
);
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
switch ((event as unknown as KeyboardEvent).code) {
|
||||
case KEY.ENTER:
|
||||
controller.handleSelect(event.ctrlKey);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[controller.handleSelect]
|
||||
);
|
||||
|
||||
return styled(styles)(
|
||||
<>
|
||||
<node as="div" {...use({ isExpanded: controller.isExpanded })}>
|
||||
<control
|
||||
tabIndex={0}
|
||||
aria-selected={controller.isSelected}
|
||||
as="div"
|
||||
onClick={handleSelect}
|
||||
onKeyDown={handleEnter}
|
||||
onDoubleClick={controller.handleDoubleClick}
|
||||
>
|
||||
<arrow as="div" hidden={!controller.isExpandable} onClick={handleExpand}>
|
||||
{controller.isLoading && <Loader small />}
|
||||
{!controller.isLoading && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</arrow>
|
||||
<icon as="div"><StaticImage icon={node.icon} /></icon>
|
||||
<name as="div">{node.name}</name>
|
||||
<portal as="div">
|
||||
<TreeNodeMenu node={node} isSelected={controller.isSelected} />
|
||||
</portal>
|
||||
</control>
|
||||
</node>
|
||||
<nested as="div">{children}</nested>
|
||||
</>
|
||||
);
|
||||
});
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import { useContext } from 'react';
|
||||
import styled, { css } from 'reshadow';
|
||||
|
||||
import { TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { NavNode } from '../../../shared/NodesManager/EntityTypes';
|
||||
import { TreeNodeMenu } from '../TreeNodeMenu/TreeNodeMenu';
|
||||
|
||||
const styles = css`
|
||||
TreeNodeControl:hover > portal,
|
||||
TreeNodeControl:global([aria-selected=true]) > portal,
|
||||
portal:focus-within {
|
||||
visibility: visible;
|
||||
}
|
||||
portal {
|
||||
box-sizing: border-box;
|
||||
margin-left: auto;
|
||||
margin-right: 16px;
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
node: NavNode;
|
||||
}
|
||||
|
||||
export const NavigationNodeControl: React.FC<Props> = observer(function NavigationNodeControl({
|
||||
node,
|
||||
}) {
|
||||
const context = useContext(TreeNodeContext);
|
||||
return styled(useStyles(TREE_NODE_STYLES, styles))(
|
||||
<TreeNodeControl>
|
||||
<TreeNodeExpand />
|
||||
<TreeNodeIcon icon={node.icon} />
|
||||
<TreeNodeName>{node.name}</TreeNodeName>
|
||||
<portal as="div">
|
||||
<TreeNodeMenu node={node} selected={context?.selected} />
|
||||
</portal>
|
||||
</TreeNodeControl>
|
||||
);
|
||||
});
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
|
||||
import { TreeNodeContext, TreeNodeNested, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { useChildren } from '../../../shared/useChildren';
|
||||
|
||||
interface Props {
|
||||
nodeId: string;
|
||||
component: React.FC<{
|
||||
nodeId: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export const NavigationNodeNested: React.FC<Props> = observer(function NavigationNodeNested({
|
||||
nodeId,
|
||||
component,
|
||||
}) {
|
||||
const styles = useStyles(TREE_NODE_STYLES);
|
||||
const context = useContext(TreeNodeContext);
|
||||
const children = useChildren(nodeId);
|
||||
|
||||
if (!children.children || !context?.expanded) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const NavigationNode = component;
|
||||
|
||||
return styled(styles)(
|
||||
<TreeNodeNested>
|
||||
{children.children.map(child => <NavigationNode key={child} nodeId={child} />)}
|
||||
</TreeNodeNested>
|
||||
);
|
||||
});
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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 { useState, useCallback, useEffect } from 'react';
|
||||
|
||||
import { useConnectionInfo } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { NavNode } from '../../../shared/NodesManager/EntityTypes';
|
||||
import { EObjectFeature } from '../../../shared/NodesManager/EObjectFeature';
|
||||
import { NodeManagerUtils } from '../../../shared/NodesManager/NodeManagerUtils';
|
||||
import { useChildren } from '../../../shared/useChildren';
|
||||
import { NavigationTreeService } from '../../NavigationTreeService';
|
||||
|
||||
export function useNavigationNode(node: NavNode, nodeLoading: boolean, nodeLoaded: boolean, nodeOutdated: boolean) {
|
||||
const navigationTreeService = useService(NavigationTreeService);
|
||||
const [isExpanded, switchExpand] = useState(false);
|
||||
const [isSelected, switchSelect] = useState(false);
|
||||
const [isProcessing, setProcessing] = useState(false);
|
||||
const children = useChildren(node.id);
|
||||
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id);
|
||||
const { connectionInfo } = useConnectionInfo(connectionId);
|
||||
|
||||
const isLoading = nodeLoading || children.isLoading || isProcessing;
|
||||
const isLoaded = children.isLoaded || nodeLoaded;
|
||||
let isExpandable = isExpandableFilter(node) && (
|
||||
!isLoaded || children.isOutdated || !children.children || children.children.length > 0
|
||||
);
|
||||
let isExpandedActually = isExpanded && (children.children?.length || 0) > 0;
|
||||
|
||||
if (node.objectFeatures.includes(EObjectFeature.dataSource) && !connectionInfo?.connected) {
|
||||
isExpandable = true;
|
||||
isExpandedActually = false;
|
||||
}
|
||||
|
||||
const handleDoubleClick = useCallback(
|
||||
async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await navigationTreeService.navToNode(node.id, node.parentId);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
},
|
||||
[navigationTreeService, node]
|
||||
);
|
||||
|
||||
const handleExpand = useCallback(
|
||||
async () => {
|
||||
if (!isExpandedActually) {
|
||||
setProcessing(true);
|
||||
const state = await navigationTreeService.loadNestedNodes(node.id);
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
switchExpand(!isExpandedActually);
|
||||
},
|
||||
[isExpandedActually, node]
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(isMultiple?: boolean) => {
|
||||
switchSelect(navigationTreeService.selectNode(node.id, isMultiple));
|
||||
},
|
||||
[isSelected, node]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isExpandable || !node.hasChildren) {
|
||||
switchExpand(false);
|
||||
}
|
||||
}, [isExpandable, node.hasChildren]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isExpandedActually && children.isOutdated && !children.isLoading && children.isLoaded && !nodeOutdated) {
|
||||
setProcessing(true);
|
||||
navigationTreeService
|
||||
.loadNestedNodes(node.id)
|
||||
.then(state => {
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [isExpandedActually, children.isOutdated, children.isLoading, children.isLoaded, nodeOutdated, node]);
|
||||
|
||||
// Here we subscribe to selected nodes if current node selected (mobx)
|
||||
if (isSelected && !navigationTreeService.isNodeSelected(node.id)) {
|
||||
switchSelect(false);
|
||||
}
|
||||
|
||||
useEffect(() => () => {
|
||||
if (navigationTreeService.isNodeSelected(node.id)) {
|
||||
navigationTreeService.selectNode(node.id, true);
|
||||
}
|
||||
}, [navigationTreeService, node]);
|
||||
|
||||
return {
|
||||
isExpanded: isExpandedActually,
|
||||
isLoaded,
|
||||
isLoading,
|
||||
isExpandable,
|
||||
isSelected,
|
||||
handleDoubleClick,
|
||||
handleSelect,
|
||||
handleExpand,
|
||||
};
|
||||
}
|
||||
|
||||
export function isExpandableFilter(node: NavNode) {
|
||||
return node.hasChildren && !node.objectFeatures.includes(EObjectFeature.entity);
|
||||
}
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
|
||||
import { useChildren } from '../../shared/useChildren';
|
||||
|
||||
type NavigationNodeChildrenProps = PropsWithChildren<{
|
||||
parentId: string;
|
||||
component: React.ElementType<{
|
||||
id: string;
|
||||
parentId: string;
|
||||
}>;
|
||||
}>;
|
||||
|
||||
export const NavigationNodeChildren = observer(function NavigationNodeChildren({
|
||||
parentId,
|
||||
component,
|
||||
}: NavigationNodeChildrenProps) {
|
||||
const { children } = useChildren(parentId);
|
||||
|
||||
if (!children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const NavigationNodeChild = component;
|
||||
return (
|
||||
<>
|
||||
{children.map(id => (
|
||||
<NavigationNodeChild key={id} id={id} parentId={parentId} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
});
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
|
||||
import { useNode } from '../../shared/NodesManager/useNode';
|
||||
import { NavigationNode } from './NavigationNode';
|
||||
|
||||
interface NavigationTreeNodeProps {
|
||||
nodeId: string;
|
||||
}
|
||||
|
||||
export const NavigationNodeElement = observer(function NavigationNodeElement({
|
||||
nodeId,
|
||||
}: NavigationTreeNodeProps) {
|
||||
const node = useNode(nodeId);
|
||||
|
||||
if (!node.node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <NavigationNode node={node.node} component={NavigationNodeElement} />;
|
||||
});
|
||||
@@ -1,40 +0,0 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
|
||||
import { useNode } from '../../shared/NodesManager/useNode';
|
||||
import { NavigationNode } from './NavigationNode/NavigationNode';
|
||||
import { NavigationNodeChildren } from './NavigationNodeChildren';
|
||||
|
||||
interface NavigationTreeNodeProps {
|
||||
id: string;
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
export const NavigationTreeNode = observer(function NavigationTreeNodeFn({
|
||||
id,
|
||||
parentId,
|
||||
}: NavigationTreeNodeProps) {
|
||||
const {
|
||||
node, isLoaded, isLoading, isOutdated,
|
||||
} = useNode(id);
|
||||
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<NavigationNode node={node} isLoaded={isLoaded} isLoading={isLoading} isOutdated={isOutdated}>
|
||||
<NavigationNodeChildren
|
||||
parentId={id}
|
||||
component={NavigationTreeNode}
|
||||
/>
|
||||
</NavigationNode>
|
||||
);
|
||||
});
|
||||
+3
-3
@@ -21,12 +21,12 @@ import { treeNodeMenuStyles } from './treeNodeMenuStyles';
|
||||
|
||||
interface TreeNodeMenuProps {
|
||||
node: NavNode;
|
||||
isSelected: boolean;
|
||||
selected?: boolean;
|
||||
}
|
||||
|
||||
export const TreeNodeMenu = observer(function TreeNodeMenu({
|
||||
node,
|
||||
isSelected,
|
||||
selected,
|
||||
}: TreeNodeMenuProps) {
|
||||
const navigationTreeContextMenuService = useService(NavigationTreeContextMenuService);
|
||||
|
||||
@@ -45,7 +45,7 @@ export const TreeNodeMenu = observer(function TreeNodeMenu({
|
||||
}
|
||||
|
||||
return styled(treeNodeMenuStyles)(
|
||||
<MenuTrigger panel={menuPanel} {...use({ isSelected })} modal>
|
||||
<MenuTrigger panel={menuPanel} {...use({ selected })}>
|
||||
<Icon name="snack" viewBox="0 0 16 10" />
|
||||
</MenuTrigger>
|
||||
);
|
||||
|
||||
+5
-1
@@ -14,6 +14,10 @@ export const treeNodeMenuStyles = css`
|
||||
padding: 0;
|
||||
height: 20px;
|
||||
|
||||
&:before {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& Icon {
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
@@ -21,7 +25,7 @@ export const treeNodeMenuStyles = css`
|
||||
fill: #dedede;
|
||||
}
|
||||
|
||||
&[use|isSelected] Icon,
|
||||
&[|selected] Icon,
|
||||
&:hover Icon,
|
||||
&:focus Icon {
|
||||
fill: #338fcc;
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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 { useEffect, useState } from 'react';
|
||||
|
||||
import { useConnectionInfo } from '@cloudbeaver/core-connections';
|
||||
import { useService } from '@cloudbeaver/core-di';
|
||||
|
||||
import { NavNode } from '../../shared/NodesManager/EntityTypes';
|
||||
import { EObjectFeature } from '../../shared/NodesManager/EObjectFeature';
|
||||
import { NodeManagerUtils } from '../../shared/NodesManager/NodeManagerUtils';
|
||||
import { useNode } from '../../shared/NodesManager/useNode';
|
||||
import { useChildren } from '../../shared/useChildren';
|
||||
import { NavigationTreeService } from '../NavigationTreeService';
|
||||
|
||||
interface INavigationNode {
|
||||
loading: boolean;
|
||||
selected: boolean;
|
||||
expanded: boolean;
|
||||
leaf: boolean;
|
||||
handleExpand: () => void;
|
||||
handleOpen: () => void;
|
||||
handleSelect: (isMultiple?: boolean) => void;
|
||||
}
|
||||
|
||||
export function useNavigationNode(node: NavNode): INavigationNode {
|
||||
const navigationTreeService = useService(NavigationTreeService);
|
||||
const [selected, switchSelect] = useState(false);
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const [isExpanded, switchExpand] = useState(false);
|
||||
const { isLoading, isOutdated } = useNode(node.id);
|
||||
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id);
|
||||
const { connectionInfo } = useConnectionInfo(connectionId);
|
||||
const children = useChildren(node.id);
|
||||
const loading = isLoading() || children.isLoading() || processing;
|
||||
|
||||
let leaf = isLeaf(node) || children.children?.length === 0;
|
||||
let expanded = isExpanded && !leaf;
|
||||
|
||||
if (node.objectFeatures.includes(EObjectFeature.dataSource) && !connectionInfo?.connected) {
|
||||
leaf = false;
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
const handleExpand = async () => {
|
||||
if (!expanded) {
|
||||
const timeout = setTimeout(() => setProcessing(true), 1);
|
||||
const state = await navigationTreeService.loadNestedNodes(node.id);
|
||||
clearTimeout(timeout);
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
switchExpand(!expanded);
|
||||
};
|
||||
|
||||
const handleOpen = async () => {
|
||||
setProcessing(true);
|
||||
try {
|
||||
await navigationTreeService.navToNode(node.id, node.parentId);
|
||||
} finally {
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (multiple?: boolean) => {
|
||||
switchSelect(navigationTreeService.selectNode(node.id, multiple));
|
||||
};
|
||||
|
||||
// TODO: probably should be refactored
|
||||
useEffect(() => {
|
||||
if (expanded && children.isOutdated() && !children.isLoading() && children.isLoaded() && !isOutdated()) {
|
||||
setProcessing(true);
|
||||
navigationTreeService
|
||||
.loadNestedNodes(node.id)
|
||||
.then(state => {
|
||||
setProcessing(false);
|
||||
if (!state) {
|
||||
switchExpand(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [expanded, children.isOutdated(), children.isLoading(), children.isLoaded(), isOutdated(), node]);
|
||||
|
||||
// Here we subscribe to selected nodes if current node selected (mobx)
|
||||
if (selected && !navigationTreeService.isNodeSelected(node.id)) {
|
||||
switchSelect(false);
|
||||
}
|
||||
|
||||
useEffect(() => () => {
|
||||
if (navigationTreeService.isNodeSelected(node.id)) {
|
||||
navigationTreeService.selectNode(node.id, true);
|
||||
}
|
||||
}, [navigationTreeService, node]);
|
||||
|
||||
return {
|
||||
loading,
|
||||
selected,
|
||||
expanded,
|
||||
leaf,
|
||||
handleExpand,
|
||||
handleOpen,
|
||||
handleSelect,
|
||||
};
|
||||
}
|
||||
|
||||
export function isLeaf(node: NavNode): boolean {
|
||||
return !node.hasChildren || node.objectFeatures.includes(EObjectFeature.entity);
|
||||
}
|
||||
@@ -52,8 +52,8 @@ export class NavigationTreeService {
|
||||
return false;
|
||||
}
|
||||
|
||||
selectNode(id: string, isMultiple?: boolean) {
|
||||
if (!isMultiple) {
|
||||
selectNode(id: string, multiple?: boolean) {
|
||||
if (!multiple) {
|
||||
for (const id of this.selectedNodes) {
|
||||
this.nodeSelectSubject.next([id, false]);
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ import { NavNodeInfoResource } from './NavNodeInfoResource';
|
||||
export function useNode(navNodeId: string) {
|
||||
const navNodeInfoResource = useService(NavNodeInfoResource);
|
||||
const node = navNodeInfoResource.get(navNodeId);
|
||||
const isLoading = navNodeInfoResource.isDataLoading(navNodeId);
|
||||
const isLoaded = navNodeInfoResource.isLoaded(navNodeId);
|
||||
const isOutdated = navNodeInfoResource.isOutdated(navNodeId);
|
||||
const isLoading = () => navNodeInfoResource.isDataLoading(navNodeId);
|
||||
const isLoaded = () => navNodeInfoResource.isLoaded(navNodeId);
|
||||
const isOutdated = () => navNodeInfoResource.isOutdated(navNodeId);
|
||||
|
||||
return {
|
||||
node, isLoading, isLoaded, isOutdated,
|
||||
|
||||
@@ -13,17 +13,17 @@ import { NavTreeResource } from './NodesManager/NavTreeResource';
|
||||
|
||||
interface Hook {
|
||||
children: string[] | undefined;
|
||||
isLoaded: boolean;
|
||||
isLoading: boolean;
|
||||
isOutdated: boolean;
|
||||
isLoaded: () => boolean;
|
||||
isLoading: () => boolean;
|
||||
isOutdated: () => boolean;
|
||||
}
|
||||
|
||||
export function useChildren(navNodeId = ROOT_NODE_PATH): Hook {
|
||||
const navTreeResource = useService(NavTreeResource);
|
||||
const children = navTreeResource.get(navNodeId);
|
||||
const isLoading = navTreeResource.isDataLoading(navNodeId);
|
||||
const isLoaded = navTreeResource.isLoaded(navNodeId);
|
||||
const isOutdated = navTreeResource.isOutdated(navNodeId);
|
||||
const isLoading = () => navTreeResource.isDataLoading(navNodeId);
|
||||
const isLoaded = () => navTreeResource.isLoaded(navNodeId);
|
||||
const isOutdated = () => navTreeResource.isOutdated(navNodeId);
|
||||
|
||||
return {
|
||||
children,
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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, useLocalStore } from 'mobx-react';
|
||||
import { useRef } from 'react';
|
||||
import styled, { use } from 'reshadow';
|
||||
|
||||
import { useStyles } from '@cloudbeaver/core-theming';
|
||||
|
||||
import { ITreeNodeContext, TreeNodeContext } from './TreeNodeContext';
|
||||
import { TREE_NODE_STYLES } from './TreeNodeStyles';
|
||||
|
||||
interface Props {
|
||||
loading?: boolean;
|
||||
selected?: boolean;
|
||||
expanded?: boolean;
|
||||
leaf?: boolean;
|
||||
className?: string;
|
||||
onExpand?: () => void;
|
||||
onSelect?: (multiple?: boolean) => void;
|
||||
onOpen?: () => void;
|
||||
}
|
||||
|
||||
export const TreeNode: React.FC<Props> = observer(function TreeNode({
|
||||
loading = false,
|
||||
selected = false,
|
||||
expanded = false,
|
||||
leaf = false,
|
||||
className,
|
||||
children,
|
||||
...handlers
|
||||
}) {
|
||||
const handlersRef = useRef(handlers);
|
||||
handlersRef.current = handlers;
|
||||
|
||||
const handleExpand = () => {
|
||||
handlersRef.current.onExpand?.();
|
||||
};
|
||||
|
||||
const handleSelect = (multiple?: boolean) => {
|
||||
handlersRef.current.onSelect?.(multiple);
|
||||
};
|
||||
|
||||
const handleOpen = () => {
|
||||
handlersRef.current.onOpen?.();
|
||||
};
|
||||
|
||||
const nodeContext = useLocalStore<ITreeNodeContext, ITreeNodeContext>(source => source, {
|
||||
loading,
|
||||
selected,
|
||||
expanded,
|
||||
leaf,
|
||||
expand: handleExpand,
|
||||
select: handleSelect,
|
||||
open: handleOpen,
|
||||
});
|
||||
// const nodeContext = useMemo<ITreeNodeContext>(() => ({
|
||||
// loading,
|
||||
// selected,
|
||||
// expanded,
|
||||
// leaf,
|
||||
// expand: handleExpand,
|
||||
// select: handleSelect,
|
||||
// open: handleOpen,
|
||||
// }), [loading, selected, expanded, leaf, handleExpand, handleSelect, handleOpen]);
|
||||
|
||||
return styled(useStyles(TREE_NODE_STYLES))(
|
||||
<node as="div" {...use({ expanded })} className={className}>
|
||||
<TreeNodeContext.Provider value={nodeContext}>
|
||||
{children}
|
||||
</TreeNodeContext.Provider>
|
||||
</node>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
|
||||
export interface ITreeNodeContext {
|
||||
expanded: boolean;
|
||||
loading: boolean;
|
||||
selected: boolean;
|
||||
leaf: boolean;
|
||||
select: (multiple?: boolean) => void;
|
||||
expand: () => void;
|
||||
open: () => void;
|
||||
}
|
||||
|
||||
export const TreeNodeContext = createContext<ITreeNodeContext | null>(null);
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import { useCallback, useContext } from 'react';
|
||||
|
||||
import { TreeNodeContext } from './TreeNodeContext';
|
||||
|
||||
const KEY = {
|
||||
ENTER: 'Enter',
|
||||
};
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeControl: React.FC<Props> = observer(function TreeNodeControl({
|
||||
className,
|
||||
children,
|
||||
}) {
|
||||
const context = useContext(TreeNodeContext);
|
||||
|
||||
const handleEnter = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
switch ((event as unknown as KeyboardEvent).code) {
|
||||
case KEY.ENTER:
|
||||
context?.select(event.ctrlKey || event.metaKey);
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
context?.select();
|
||||
},
|
||||
[context]
|
||||
);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Context not provided');
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
tabIndex={0}
|
||||
aria-selected={context.selected}
|
||||
className={className}
|
||||
onClick={handleClick}
|
||||
onKeyDown={handleEnter}
|
||||
onDoubleClick={context.open}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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';
|
||||
import { useContext } from 'react';
|
||||
import styled from 'reshadow';
|
||||
import { css } from 'reshadow';
|
||||
|
||||
import { Icon, Loader } from '@cloudbeaver/core-blocks';
|
||||
|
||||
import { TreeNodeContext } from './TreeNodeContext';
|
||||
|
||||
const styles = css`
|
||||
Icon {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeExpand: React.FC<Props> = observer(function TreeNodeExpand({
|
||||
className,
|
||||
}) {
|
||||
const context = useContext(TreeNodeContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error('Context not provided');
|
||||
}
|
||||
|
||||
const handleExpand = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
context.expand();
|
||||
};
|
||||
|
||||
const preventDoubleClick = (event: React.MouseEvent<HTMLDivElement>) => {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
return styled(styles)(
|
||||
<arrow as="div" className={className} onClick={handleExpand} onDoubleClick={preventDoubleClick}>
|
||||
{context.loading && <Loader small />}
|
||||
{!context.loading && !context.leaf && <Icon name="arrow" viewBox="0 0 16 16" />}
|
||||
</arrow>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 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 styled, { css } from 'reshadow';
|
||||
|
||||
import { StaticImage } from '@cloudbeaver/core-blocks';
|
||||
|
||||
const styles = css`
|
||||
StaticImage {
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
icon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeIcon: React.FC<Props> = function TreeNodeIcon({
|
||||
icon,
|
||||
className,
|
||||
}) {
|
||||
return styled(styles)(
|
||||
<icon as="div" className={className}><StaticImage icon={icon} /></icon>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeName: React.FC<Props> = function TreeNodeName({
|
||||
className,
|
||||
children,
|
||||
}) {
|
||||
return <div className={className}>{children}</div>;
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* cloudbeaver - Cloud Database Manager
|
||||
* Copyright (C) 2020 DBeaver Corp and others
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0.
|
||||
* you may not use this file except in compliance with the License.
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const TreeNodeNested: React.FC<Props> = function TreeNodeNested({
|
||||
className,
|
||||
children,
|
||||
}) {
|
||||
return <div className={className}>{children}</div>;
|
||||
};
|
||||
+19
-35
@@ -10,9 +10,9 @@ import { css } from 'reshadow';
|
||||
|
||||
import { composes } from '@cloudbeaver/core-theming';
|
||||
|
||||
export const navigationNodeStyles = composes(
|
||||
export const TREE_NODE_STYLES = composes(
|
||||
css`
|
||||
control {
|
||||
TreeNodeControl {
|
||||
composes: theme-ripple theme-ripple-selectable from global;
|
||||
}
|
||||
`,
|
||||
@@ -21,34 +21,27 @@ export const navigationNodeStyles = composes(
|
||||
box-sizing: border-box;
|
||||
width: fit-content;
|
||||
min-width: 100%;
|
||||
height: 20px;
|
||||
|
||||
&[use|isExpanded] {
|
||||
& + nested {
|
||||
&[use|expanded] {
|
||||
& > TreeNodeNested {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& arrow Icon {
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
& > TreeNodeControl > TreeNodeExpand {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
|
||||
& control {
|
||||
& TreeNodeControl {
|
||||
box-sizing: border-box;
|
||||
height: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 5px;
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
height: inherit;
|
||||
position: initial;
|
||||
outline: none;
|
||||
|
||||
&:hover > portal,
|
||||
&[aria-selected=true] > portal {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
&::before {
|
||||
left: 0;
|
||||
@@ -59,41 +52,32 @@ export const navigationNodeStyles = composes(
|
||||
}
|
||||
}
|
||||
|
||||
arrow {
|
||||
cursor: pointer;
|
||||
TreeNodeExpand {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.5;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
&[hidden] + icon {
|
||||
margin-left: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
icon {
|
||||
TreeNodeIcon {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
margin-left: 4px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
|
||||
& StaticImage {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
name {
|
||||
TreeNodeName {
|
||||
box-sizing: border-box;
|
||||
margin-left: 4px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
portal {
|
||||
margin-left: auto;
|
||||
margin-right: 16px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
nested {
|
||||
TreeNodeNested {
|
||||
box-sizing: border-box;
|
||||
padding: 2px 0;
|
||||
padding-left: 16px;
|
||||
padding-left: 20px;
|
||||
display: none;
|
||||
}
|
||||
`
|
||||
@@ -12,6 +12,14 @@ export * from './SanitizedHTML/SanitizedHTML';
|
||||
export * from './Split';
|
||||
export * from './Table';
|
||||
export * from './Tabs';
|
||||
export * from './Tree/TreeNode/TreeNode';
|
||||
export * from './Tree/TreeNode/TreeNodeContext';
|
||||
export * from './Tree/TreeNode/TreeNodeControl';
|
||||
export * from './Tree/TreeNode/TreeNodeExpand';
|
||||
export * from './Tree/TreeNode/TreeNodeIcon';
|
||||
export * from './Tree/TreeNode/TreeNodeName';
|
||||
export * from './Tree/TreeNode/TreeNodeNested';
|
||||
export * from './Tree/TreeNode/TreeNodeStyles';
|
||||
export * from './Button';
|
||||
export * from './FormControls/Checkboxes/Checkbox';
|
||||
export * from './FormControls/Checkboxes/FieldCheckbox';
|
||||
|
||||
@@ -64,9 +64,15 @@ const MenuPanel = observer(function MenuPanel({
|
||||
menu,
|
||||
style = [],
|
||||
}: MenuPanelProps) {
|
||||
return styled(useStyles(menuPanelStyles, ...style))(
|
||||
const styles = useStyles(menuPanelStyles, ...style);
|
||||
|
||||
if (!menu.visible) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return styled(styles)(
|
||||
<Menu {...menu} aria-label={panel.id}>
|
||||
{menu.visible && panel.menuItems.map(item => (
|
||||
{panel.menuItems.map(item => (
|
||||
<MenuPanelElement key={item.id} item={item} menu={menu} style={style} />
|
||||
))}
|
||||
</Menu>
|
||||
|
||||
Reference in New Issue
Block a user