diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx
index 48c8a99f15..ca935d7c26 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTree.tsx
@@ -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)(
-
-
-
+
+
+
);
}
return styled(navigationTreeStyles)(
-
-
+
+
No connections.
Use the top menu to setup connection to your database.
-
-
+
+
);
}
return styled(navigationTreeStyles)(
-
- {nodeChildren.children.map(id => (
-
- ))}
-
+
+
+ {nodeChildren.children.map(id => (
+
+ ))}
+
+
);
});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx
new file mode 100644
index 0000000000..9e44eaa66c
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx
@@ -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 (
+
+
+
+
+ );
+});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx
deleted file mode 100644
index 6d60dc7920..0000000000
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx
+++ /dev/null
@@ -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) => {
- e.stopPropagation();
- controller.handleExpand();
- },
- [controller.handleExpand]
- );
-
- const handleSelect = useCallback(
- (e: MouseEvent) => {
- controller.handleSelect(e.ctrlKey);
- },
- [controller.handleSelect]
- );
-
- const handleEnter = useCallback(
- (event: React.KeyboardEvent) => {
- switch ((event as unknown as KeyboardEvent).code) {
- case KEY.ENTER:
- controller.handleSelect(event.ctrlKey);
- break;
- }
- return true;
- },
- [controller.handleSelect]
- );
-
- return styled(styles)(
- <>
-
-
-
- {controller.isLoading && }
- {!controller.isLoading && }
-
-
- {node.name}
-
-
-
-
-
- {children}
- >
- );
-});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx
new file mode 100644
index 0000000000..e9cfd7478b
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx
@@ -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 = observer(function NavigationNodeControl({
+ node,
+}) {
+ const context = useContext(TreeNodeContext);
+ return styled(useStyles(TREE_NODE_STYLES, styles))(
+
+
+
+ {node.name}
+
+
+
+
+ );
+});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx
new file mode 100644
index 0000000000..43655a892b
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx
@@ -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 = 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)(
+
+ {children.children.map(child => )}
+
+ );
+});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/useNavigationNode.ts
deleted file mode 100644
index f78cdcca19..0000000000
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/useNavigationNode.ts
+++ /dev/null
@@ -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);
-}
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx
deleted file mode 100644
index 2bdf17e8b7..0000000000
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx
+++ /dev/null
@@ -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 => (
-
- ))}
- >
- );
-});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx
new file mode 100644
index 0000000000..e4303c879d
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx
@@ -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 ;
+});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx
deleted file mode 100644
index 2674c9e69e..0000000000
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx
+++ /dev/null
@@ -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 (
-
-
-
- );
-});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
index d796cbabd9..7b9cfd449b 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
@@ -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)(
-
+
);
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts
index 18e4bdd272..a7ee21c53a 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/treeNodeMenuStyles.ts
@@ -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;
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts
new file mode 100644
index 0000000000..d1d559602b
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts
@@ -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);
+}
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts
index 85158a20e4..a0d391e032 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeService.ts
@@ -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]);
}
diff --git a/webapp/packages/core-app/src/shared/NodesManager/useNode.ts b/webapp/packages/core-app/src/shared/NodesManager/useNode.ts
index a0f103dee9..3409d2db13 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/useNode.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/useNode.ts
@@ -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,
diff --git a/webapp/packages/core-app/src/shared/useChildren.ts b/webapp/packages/core-app/src/shared/useChildren.ts
index 160dbc9d88..56084b6980 100644
--- a/webapp/packages/core-app/src/shared/useChildren.ts
+++ b/webapp/packages/core-app/src/shared/useChildren.ts
@@ -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,
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx
new file mode 100644
index 0000000000..f3e7a51812
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx
@@ -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 = 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(source => source, {
+ loading,
+ selected,
+ expanded,
+ leaf,
+ expand: handleExpand,
+ select: handleSelect,
+ open: handleOpen,
+ });
+ // const nodeContext = useMemo(() => ({
+ // loading,
+ // selected,
+ // expanded,
+ // leaf,
+ // expand: handleExpand,
+ // select: handleSelect,
+ // open: handleOpen,
+ // }), [loading, selected, expanded, leaf, handleExpand, handleSelect, handleOpen]);
+
+ return styled(useStyles(TREE_NODE_STYLES))(
+
+
+ {children}
+
+
+ );
+});
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts
new file mode 100644
index 0000000000..81356db434
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts
@@ -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(null);
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx
new file mode 100644
index 0000000000..1f2bedc771
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx
@@ -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 = observer(function TreeNodeControl({
+ className,
+ children,
+}) {
+ const context = useContext(TreeNodeContext);
+
+ const handleEnter = useCallback(
+ (event: React.KeyboardEvent) => {
+ 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) => {
+ event.stopPropagation();
+ context?.select();
+ },
+ [context]
+ );
+
+ if (!context) {
+ throw new Error('Context not provided');
+ }
+
+ return (
+
+ {children}
+
+ );
+});
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx
new file mode 100644
index 0000000000..302838e0c7
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx
@@ -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 = observer(function TreeNodeExpand({
+ className,
+}) {
+ const context = useContext(TreeNodeContext);
+
+ if (!context) {
+ throw new Error('Context not provided');
+ }
+
+ const handleExpand = (event: React.MouseEvent) => {
+ context.expand();
+ };
+
+ const preventDoubleClick = (event: React.MouseEvent) => {
+ event.stopPropagation();
+ event.preventDefault();
+ };
+
+ return styled(styles)(
+
+ {context.loading && }
+ {!context.loading && !context.leaf && }
+
+ );
+});
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx
new file mode 100644
index 0000000000..f2487a93a6
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx
@@ -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 = function TreeNodeIcon({
+ icon,
+ className,
+}) {
+ return styled(styles)(
+
+ );
+};
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx
new file mode 100644
index 0000000000..c6f89ccc29
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx
@@ -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 = function TreeNodeName({
+ className,
+ children,
+}) {
+ return {children}
;
+};
diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx
new file mode 100644
index 0000000000..735ca81290
--- /dev/null
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx
@@ -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 = function TreeNodeNested({
+ className,
+ children,
+}) {
+ return {children}
;
+};
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts
similarity index 63%
rename from webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts
rename to webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts
index ef2d59099f..a363137294 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts
+++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts
@@ -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;
}
`
diff --git a/webapp/packages/core-blocks/src/index.ts b/webapp/packages/core-blocks/src/index.ts
index 32b5f3cd5c..bfd0344c94 100644
--- a/webapp/packages/core-blocks/src/index.ts
+++ b/webapp/packages/core-blocks/src/index.ts
@@ -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';
diff --git a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx
index 02324e6b9f..ce4ae4024c 100644
--- a/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx
+++ b/webapp/packages/core-dialogs/src/Menu/MenuPanel/MenuTrigger.tsx
@@ -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)(