Merge pull request #191 from dbeaver/refactor/navigation-tree

refactor(core-app): CB-339 NavigationTree
This commit is contained in:
Alexey
2020-10-18 17:14:43 +03:00
committed by GitHub
25 changed files with 660 additions and 369 deletions
@@ -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>
);
});
@@ -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>
</>
);
});
@@ -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>
);
});
@@ -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>
);
});
@@ -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);
}
@@ -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} />
))}
</>
);
});
@@ -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>
);
});
@@ -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>
);
@@ -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>;
};
@@ -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;
}
`
+8
View File
@@ -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>