Merge pull request #1096 from dbeaver/feat/CB-2453/smart-projects-in-nav-tree

feat(plugin-navigation-tree): CB-2453 remove expand from projects in …
This commit is contained in:
Alexey
2022-08-31 18:01:00 +03:00
committed by GitHub
9 changed files with 154 additions and 16 deletions
@@ -231,6 +231,7 @@ export const RenderField = observer<RenderFieldProps>(function RenderField({
</InputField>
);
}
return (
<InputField
type={password ? 'password' : 'text'}
@@ -102,6 +102,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
navNodeFilterCompare,
filters = [],
renderers = [],
expandStateGetters,
style,
className,
getChildren,
@@ -161,7 +162,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
}, [folderExplorer]);
const children = useMapResource(ElementsTree, navTreeResource, root, {
useMapResource(ElementsTree, navTreeResource, root, {
onLoad: async resource => {
let fullPath = folderExplorer.state.fullPath;
const preload = await resource.preloadNodeParents(fullPath);
@@ -199,6 +200,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
localState,
filters: [nameFilter, ...filters, limitFilter],
renderers: [...renderers, elementsTreeLimitRenderer],
expandStateGetters,
getChildren,
loadChildren,
isGroup,
@@ -279,7 +281,8 @@ export const ElementsTree = observer<Props>(function ElementsTree({
}
});
const hasChildren = (children.data?.length || 0) > 0;
const children = tree.getNodeChildren(root);
const hasChildren = children.length > 0;
const loaderAvailable = !foldersTree || context.folderExplorer.root === root;
return styled(useStyles(TREE_NODE_STYLES, styles, style))(
@@ -290,7 +293,7 @@ export const ElementsTree = observer<Props>(function ElementsTree({
root={root}
context={context}
emptyPlaceholder={emptyPlaceholder}
childrenState={children}
childrenState={tree}
hasChildren={hasChildren}
>
<ElementsTreeContext.Provider value={context}>
@@ -104,9 +104,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer<NavTreeCo
treeNodeContext.select(event.ctrlKey || event.metaKey);
}
const expandable = getComputed(() => treeContext?.tree.isNodeExpandable(node.id) ?? true);
return styled(TREE_NODE_STYLES, nodeIconStyle, styles)(
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
<TreeNodeExpand filterActive={treeContext?.tree.filtering} />
{expandable && <TreeNodeExpand filterActive={treeContext?.tree.filtering} />}
<TreeNodeIcon icon={icon} style={nodeIconStyle} {...use({ connected })}>
<ConnectionMark connected={connected} />
</TreeNodeIcon>
@@ -9,7 +9,7 @@
import { action, computed, observable, runInAction } from 'mobx';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { IFolderExplorerContext, useExecutor, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks';
import { IFolderExplorerContext, ILoadableState, useExecutor, useObjectRef, useObservableRef, useUserData } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
@@ -23,6 +23,16 @@ import type { NavigationNodeRendererComponent } from './NavigationNodeComponent'
export type IElementsTreeCustomRenderer = (nodeId: string) => NavigationNodeRendererComponent | undefined;
export interface IElementsTreeNodeExpandedInfo {
expanded: boolean;
expandable?: boolean;
}
export type IElementsTreeNodeExpandInfoGetter = (
nodeId: string,
state: ITreeNodeState
) => IElementsTreeNodeExpandedInfo | null;
export type IElementsTreeFilter = (
filter: string,
node: NavNode,
@@ -56,6 +66,7 @@ export interface IElementsTreeOptions {
disabled?: boolean;
filters?: IElementsTreeFilter[];
renderers?: IElementsTreeCustomRenderer[];
expandStateGetters?: IElementsTreeNodeExpandInfoGetter[];
localState?: MetadataMap<string, ITreeNodeState>;
getChildren: (id: string) => string[] | undefined;
loadChildren: (id: string, manual: boolean) => Promise<boolean>;
@@ -74,7 +85,7 @@ interface IOptions extends IElementsTreeOptions {
folderExplorer: IFolderExplorerContext;
}
export interface IElementsTree {
export interface IElementsTree extends ILoadableState {
actions: ISyncExecutor<IElementsTreeAction>;
settings?: IElementsTreeSettings;
baseRoot: string;
@@ -89,6 +100,7 @@ export interface IElementsTree {
getNodeState: (nodeId: string) => ITreeNodeState;
isNodeExpanded: (nodeId: string, ignoreFilter?: boolean) => boolean;
isNodeExpandable: (nodeId: string) => boolean;
getExpanded: () => string[];
getSelected: () => string[];
isNodeSelected: (nodeId: string) => boolean;
@@ -294,6 +306,12 @@ export function useElementsTree(options: IOptions): IElementsTree {
get filtering() {
return this.filter !== '';
},
isLoading() {
return this.loading;
},
isLoaded() {
return true;
},
getNodeState(nodeId: string) {
return this.state.get(nodeId);
},
@@ -307,8 +325,34 @@ export function useElementsTree(options: IOptions): IElementsTree {
}
const nodeState = this.getNodeState(nodeId);
const expanded = nodeState.expanded || nodeState.showInFilter;
return nodeState.expanded || nodeState.showInFilter;
if (!expanded && options.expandStateGetters?.length) {
return options
.expandStateGetters
.map(getExpandState => getExpandState(nodeId, nodeState))
.filter(stateInfo => stateInfo !== null)
.some(stateInfo => stateInfo?.expanded);
}
return expanded;
},
isNodeExpandable(nodeId: string): boolean {
if (nodeId === this.root) {
return true;
}
if (options.expandStateGetters?.length) {
const nodeState = this.getNodeState(nodeId);
return options
.expandStateGetters
.map(getExpandState => getExpandState(nodeId, nodeState))
.filter(stateInfo => stateInfo !== null)
.every(stateInfo => stateInfo?.expandable !== false);
}
return true;
},
getExpanded(): string[] {
return Array.from(this.state).filter(([key, state]) => state.expanded).map(([key]) => key);
@@ -416,6 +460,10 @@ export function useElementsTree(options: IOptions): IElementsTree {
}
},
async expand(node: NavNode, state: boolean) {
if (!this.isNodeExpandable(node.id)) {
return;
}
const treeNodeState = this.state.get(node.id);
try {
@@ -499,7 +547,7 @@ export function useElementsTree(options: IOptions): IElementsTree {
baseRoot: options.baseRoot,
renderers,
userData,
});
}, ['isLoading', 'isLoaded']);
function exitNodeFolder(nodeId: string) {
runInAction(() => {
@@ -13,7 +13,7 @@ import styled, { css } from 'reshadow';
import { useUserData } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { Translate } from '@cloudbeaver/core-localization';
import { NavNodeInfoResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { usePermission, EPermission } from '@cloudbeaver/core-root';
import { CaptureView } from '@cloudbeaver/core-view';
@@ -26,6 +26,8 @@ import type { IElementsTreeSettings } from './ElementsTree/useElementsTree';
import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettingsId';
import { navigationTreeDuplicateFilter } from './navigationTreeDuplicateIdFilter';
import { NavigationTreeService } from './NavigationTreeService';
import { navigationTreeProjectFilter } from './ProjectsRenderer/navigationTreeProjectFilter';
import { navigationTreeProjectsExpandStateGetter } from './ProjectsRenderer/navigationTreeProjectsExpandStateGetter';
import { navigationTreeProjectsRendererRenderer } from './ProjectsRenderer/navigationTreeProjectsRendererRenderer';
import { useNavigationTree } from './useNavigationTree';
@@ -74,6 +76,7 @@ const elementsTreeStyles = css`
export const NavigationTree = observer(function NavigationTree() {
const navTreeService = useService(NavigationTreeService);
const navNodeInfoResource = useService(NavNodeInfoResource);
const navTreeResource = useService(NavTreeResource);
const navNodeViewService = useService(NavNodeViewService);
const root = ROOT_NODE_PATH;
@@ -94,7 +97,15 @@ export const NavigationTree = observer(function NavigationTree() {
const duplicateFilter = useMemo(() => navigationTreeDuplicateFilter(navNodeViewService), [navNodeViewService]);
const projectsRendererRenderer = useMemo(
() => navigationTreeProjectsRendererRenderer(navNodeInfoResource),
[navNodeViewService]
[navNodeInfoResource]
);
const projectsExpandStateGetter = useMemo(
() => navigationTreeProjectsExpandStateGetter(navNodeInfoResource),
[navNodeInfoResource]
);
const projectFilter = useMemo(
() => navigationTreeProjectFilter(navNodeInfoResource, navTreeResource),
[navNodeInfoResource, navTreeResource]
);
if (!isEnabled) {
@@ -106,8 +117,9 @@ export const NavigationTree = observer(function NavigationTree() {
<ElementsTree
root={root}
localState={navTreeService.treeState}
filters={[duplicateFilter, connectionGroupFilter]}
filters={[duplicateFilter, connectionGroupFilter, projectFilter]}
renderers={[projectsRendererRenderer, navigationTreeConnectionGroupRenderer]}
expandStateGetters={[projectsExpandStateGetter]}
emptyPlaceholder={() => styled(navigationTreeStyles)(
<center>
<message>
@@ -10,12 +10,11 @@ import { observer } from 'mobx-react-lite';
import React, { forwardRef, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { EventContext, EventStopPropagationFlag } from '@cloudbeaver/core-events';
import { NavNodeInfoResource, type INodeActions } from '@cloudbeaver/core-navigation-tree';
import { ElementsTreeContext } from '../ElementsTree/ElementsTreeContext';
import type { NavTreeControlComponent, NavTreeControlProps } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeEditor } from '../ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor';
import { TreeNodeMenu } from '../ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu';
@@ -62,7 +61,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
dndPlaceholder,
}, ref) {
const treeNodeContext = useContext(TreeNodeContext);
const treeContext = useContext(ElementsTreeContext);
const navNodeInfoResource = useService(NavNodeInfoResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading);
const selected = treeNodeContext.selected;
@@ -86,7 +84,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer<Na
return styled(TREE_NODE_STYLES, styles)(
<TreeNodeControl ref={ref} onClick={onClickHandler} {...use({ outdated, editing, dragging: dndElement })}>
<TreeNodeExpand filterActive={treeContext?.tree.filtering} />
<TreeNodeName title={node.name}>
{editing ? (
<NavigationNodeEditor node={node} onClose={() => setEditing(false)} />
@@ -0,0 +1,33 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 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 { NavNode, NavNodeInfoResource, NavTreeResource, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
import type { IElementsTreeFilter } from '../ElementsTree/useElementsTree';
export function navigationTreeProjectFilter(
navNodeInfoResource: NavNodeInfoResource,
navTreeResource: NavTreeResource,
): IElementsTreeFilter {
return (filter: string, node: NavNode, children: string[]) => {
if (node.id !== ROOT_NODE_PATH) {
return children;
}
const nodes = navNodeInfoResource
.get(resourceKeyList(children))
.filter<NavNode>((node => node !== undefined) as (node: NavNode | undefined) => node is NavNode)
.filter(node => node.nodeType !== NAV_NODE_TYPE_PROJECT || navTreeResource.get(node.id)?.length)
.map(node => node.id);
return nodes;
};
}
@@ -0,0 +1,31 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2022 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import type { IElementsTreeNodeExpandInfoGetter } from '../ElementsTree/useElementsTree';
export function navigationTreeProjectsExpandStateGetter(
navNodeInfoResource: NavNodeInfoResource
): IElementsTreeNodeExpandInfoGetter {
return nodeId => {
const node = navNodeInfoResource.get(nodeId);
if (node?.nodeType !== NAV_NODE_TYPE_PROJECT) {
return null;
}
return {
expanded: true,
expandable: false,
};
};
}
@@ -7,19 +7,29 @@
*/
import { observer } from 'mobx-react-lite';
import styled from 'reshadow';
import styled, { css } from 'reshadow';
import { TreeNodeNestedMessage, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { Translate } from '@cloudbeaver/core-localization';
import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree';
import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects';
import { useChildren } from '../../NodesManager/useChildren';
import { useNode } from '../../NodesManager/useNode';
import type { NavigationNodeRendererComponent } from '../ElementsTree/NavigationNodeComponent';
import { NavigationNodeRenderer } from '../ElementsTree/NavigationTreeNode/NavigationNodeRenderer';
import type { IElementsTreeCustomRenderer } from '../ElementsTree/useElementsTree';
import { NavigationNodeProjectControl } from './NavigationNodeProjectControl';
const nestedStyles = css`
TreeNode {
margin-top: 8px;
}
NavigationNodeNested {
padding-left: 8px !important;
}
`;
export function navigationTreeProjectsRendererRenderer(
navNodeInfoResource: NavNodeInfoResource
): IElementsTreeCustomRenderer {
@@ -61,6 +71,7 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag
dragging={dragging}
className={className}
control={NavigationNodeProjectControl}
style={nestedStyles}
component={component}
/>
);