From e4f63f17049318eb2586fd55d5888f4fcad601c4 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 18 Oct 2020 17:02:15 +0300 Subject: [PATCH 1/2] refactor(core-app): CB-339 NavigationTree --- .../src/NavigationTree/NavigationTree.tsx | 47 ++++--- .../NavigationTreeNode/NavigationNode.tsx | 53 ++++++++ .../NavigationNode/NavigationNode.tsx | 97 -------------- .../NavigationNode/NavigationNodeControl.tsx | 51 ++++++++ .../NavigationNode/NavigationNodeNested.tsx | 44 +++++++ .../NavigationNode/useNavigationNode.ts | 121 ------------------ .../NavigationNodeChildren.tsx | 40 ------ .../NavigationNodeElement.tsx | 28 ++++ .../NavigationTreeNode/NavigationTreeNode.tsx | 40 ------ .../TreeNodeMenu/TreeNodeMenu.tsx | 6 +- .../TreeNodeMenu/treeNodeMenuStyles.ts | 6 +- .../NavigationTreeNode/useNavigationNode.ts | 116 +++++++++++++++++ .../NavigationTree/NavigationTreeService.ts | 4 +- .../src/shared/NodesManager/useNode.ts | 6 +- .../core-app/src/shared/useChildren.ts | 12 +- .../src/Tree/TreeNode/TreeNode.tsx | 79 ++++++++++++ .../src/Tree/TreeNode/TreeNodeContext.ts | 21 +++ .../src/Tree/TreeNode/TreeNodeControl.tsx | 65 ++++++++++ .../src/Tree/TreeNode/TreeNodeExpand.tsx | 53 ++++++++ .../src/Tree/TreeNode/TreeNodeIcon.tsx | 31 +++++ .../src/Tree/TreeNode/TreeNodeName.tsx | 18 +++ .../src/Tree/TreeNode/TreeNodeNested.tsx | 18 +++ .../src/Tree/TreeNode/TreeNodeStyles.ts} | 51 +++----- webapp/packages/core-blocks/src/index.ts | 8 ++ .../src/Menu/MenuPanel/MenuTrigger.tsx | 10 +- 25 files changed, 658 insertions(+), 367 deletions(-) create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode.tsx delete mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNode.tsx create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeNested.tsx delete mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/useNavigationNode.ts delete mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeChildren.tsx create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNodeElement.tsx delete mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationTreeNode.tsx create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/useNavigationNode.ts create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNode.tsx create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeContext.ts create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeControl.tsx create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeIcon.tsx create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeName.tsx create mode 100644 webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeNested.tsx rename webapp/packages/{core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/navigationNodeStyles.ts => core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts} (66%) 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)( - <> - - - - - {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..35eede6c0b --- /dev/null +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.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 { 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 { + 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 66% 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..c00b72c174 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,39 +52,31 @@ export const navigationNodeStyles = composes( } } - arrow { + TreeNodeExpand { + box-sizing: border-box; + flex-shrink: 0; cursor: pointer; 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; 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)( - {menu.visible && panel.menuItems.map(item => ( + {panel.menuItems.map(item => ( ))} From 92dc8d02dc4f4de3508290d90e9eaa68c8559df9 Mon Sep 17 00:00:00 2001 From: Wroud Date: Sun, 18 Oct 2020 17:13:34 +0300 Subject: [PATCH 2/2] chore(core-blocks): CB-339 Tree improve styles --- .../packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx | 1 + .../packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx index 35eede6c0b..302838e0c7 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeExpand.tsx @@ -17,6 +17,7 @@ import { TreeNodeContext } from './TreeNodeContext'; const styles = css` Icon { + cursor: pointer; height: 100%; width: 100%; } diff --git a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts index c00b72c174..a363137294 100644 --- a/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts +++ b/webapp/packages/core-blocks/src/Tree/TreeNode/TreeNodeStyles.ts @@ -55,7 +55,6 @@ export const TREE_NODE_STYLES = composes( TreeNodeExpand { box-sizing: border-box; flex-shrink: 0; - cursor: pointer; opacity: 0.5; width: 16px; height: 16px; @@ -78,7 +77,7 @@ export const TREE_NODE_STYLES = composes( TreeNodeNested { box-sizing: border-box; padding: 2px 0; - padding-left: 16px; + padding-left: 20px; display: none; } `