diff --git a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx index d1a82fb03c..49834b769d 100644 --- a/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx +++ b/webapp/packages/core-blocks/src/ObjectPropertyInfo/ObjectPropertyInfoForm/RenderField.tsx @@ -231,6 +231,7 @@ export const RenderField = observer(function RenderField({ ); } + return ( (function ElementsTree({ navNodeFilterCompare, filters = [], renderers = [], + expandStateGetters, style, className, getChildren, @@ -161,7 +162,7 @@ export const ElementsTree = observer(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(function ElementsTree({ localState, filters: [nameFilter, ...filters, limitFilter], renderers: [...renderers, elementsTreeLimitRenderer], + expandStateGetters, getChildren, loadChildren, isGroup, @@ -279,7 +281,8 @@ export const ElementsTree = observer(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(function ElementsTree({ root={root} context={context} emptyPlaceholder={emptyPlaceholder} - childrenState={children} + childrenState={tree} hasChildren={hasChildren} > diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx index 2df739a52b..4dbd7248ea 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -104,9 +104,11 @@ export const NavigationNodeControl: NavTreeControlComponent = observer treeContext?.tree.isNodeExpandable(node.id) ?? true); + return styled(TREE_NODE_STYLES, nodeIconStyle, styles)( - + {expandable && } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts index a60a5c551c..112808a1a8 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/useElementsTree.ts @@ -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; getChildren: (id: string) => string[] | undefined; loadChildren: (id: string, manual: boolean) => Promise; @@ -74,7 +85,7 @@ interface IOptions extends IElementsTreeOptions { folderExplorer: IFolderExplorerContext; } -export interface IElementsTree { +export interface IElementsTree extends ILoadableState { actions: ISyncExecutor; 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(() => { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx index d8337f235d..6b268ee0af 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/NavigationTree.tsx @@ -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() { styled(navigationTreeStyles)(
diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx index 6c2d95e1f9..f9a2977de1 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/NavigationNodeProjectControl.tsx @@ -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 navNodeInfoResource.isOutdated(node.id) && !treeNodeContext.loading); const selected = treeNodeContext.selected; @@ -86,7 +84,6 @@ export const NavigationNodeProjectControl: NavTreeControlComponent = observer - {editing ? ( setEditing(false)} /> diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts new file mode 100644 index 0000000000..e0fa9c5edf --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts @@ -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((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; + }; +} diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts new file mode 100644 index 0000000000..419da94971 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsExpandStateGetter.ts @@ -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, + }; + }; +} \ No newline at end of file diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx index 4b3bf515b1..f6839cb2e5 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -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} /> );