diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ACTION_LINK_OBJECT.ts b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ACTION_LINK_OBJECT.ts index b8a268e952..dcf5ef873a 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ACTION_LINK_OBJECT.ts +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ACTION_LINK_OBJECT.ts @@ -11,4 +11,5 @@ import { createAction } from '@cloudbeaver/core-view'; export const ACTION_LINK_OBJECT = createAction('link-object', { label: 'link-object', + icon: '/icons/link_editor_sm.svg', }); diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx index 9b49f23ac7..8a1ab7c016 100644 --- a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.tsx @@ -11,18 +11,14 @@ import React, { useState } from 'react'; import styled, { css, use } from 'reshadow'; import { IconButton } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; import { useTranslate } from '@cloudbeaver/core-localization'; import { ComponentStyle, useStyles } from '@cloudbeaver/core-theming'; import { useCaptureViewContext } from '@cloudbeaver/core-view'; -import { NavNodeInfoResource } from '../../../shared/NodesManager/NavNodeInfoResource'; -import { ConnectionSchemaManagerService } from '../../../TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService'; import { DATA_CONTEXT_ELEMENTS_TREE } from '../DATA_CONTEXT_ELEMENTS_TREE'; -import { KEY_BINDING_COLLAPSE_ALL } from '../KEY_BINDING_COLLAPSE_ALL'; -import { KEY_BINDING_LINK_OBJECT } from '../KEY_BINDING_LINK_OBJECT'; import type { IElementsTree } from '../useElementsTree'; import { ElementsTreeFilter } from './ElementsTreeFilter'; +import { ElementsTreeToolsMenu } from './ElementsTreeToolsMenu'; import { DATA_CONTEXT_NAV_TREE_ROOT } from './NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT'; import { NavigationTreeSettings } from './NavigationTreeSettings/NavigationTreeSettings'; @@ -73,8 +69,6 @@ export const ElementsTreeTools = observer>(functi children, }) { const translate = useTranslate(); - const navNodeInfoResource = useService(NavNodeInfoResource); - const connectionSchemaManagerService = useService(ConnectionSchemaManagerService); const [opened, setOpen] = useState(false); const styles = useStyles(toolsStyles, style); @@ -83,39 +77,11 @@ export const ElementsTreeTools = observer>(functi context?.set(DATA_CONTEXT_ELEMENTS_TREE, tree); }); - const activeNavNode = connectionSchemaManagerService.activeNavNode; - const nodeInTree = activeNavNode?.path.includes(tree.baseRoot) && navNodeInfoResource.has(activeNavNode.nodeId); - - function showObject() { - if (activeNavNode && nodeInTree) { - tree.show( - activeNavNode.nodeId, - activeNavNode.path - ); - } - } - return styled(styles)( - {activeNavNode && nodeInTree && ( - - )} - + {tree.settings?.configurable && ( (function ElementsTreeToolsMenu({ className }) { + const menu = useMenu({ menu: MENU_ELEMENTS_TREE_TOOLS }); + + return styled(style)( + + + + ); +}); \ No newline at end of file diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts new file mode 100644 index 0000000000..5f3df3fa90 --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.ts @@ -0,0 +1,172 @@ +/* + * 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 { UserDataService } from '@cloudbeaver/core-authentication'; +import { injectable } from '@cloudbeaver/core-di'; +import { LocalizationService } from '@cloudbeaver/core-localization'; +import { ActionService, ACTION_COLLAPSE_ALL, ACTION_FILTER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, KeyBindingService, MenuService } from '@cloudbeaver/core-view'; + +import { NavNodeInfoResource } from '../../../shared/NodesManager/NavNodeInfoResource'; +import { ConnectionSchemaManagerService } from '../../../TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService'; +import { getNavigationTreeUserSettingsId } from '../../getNavigationTreeUserSettingsId'; +import { ACTION_LINK_OBJECT } from '../ACTION_LINK_OBJECT'; +import { DATA_CONTEXT_ELEMENTS_TREE } from '../DATA_CONTEXT_ELEMENTS_TREE'; +import { KEY_BINDING_COLLAPSE_ALL } from '../KEY_BINDING_COLLAPSE_ALL'; +import { KEY_BINDING_LINK_OBJECT } from '../KEY_BINDING_LINK_OBJECT'; +import { MENU_ELEMENTS_TREE_TOOLS } from './MENU_ELEMENTS_TREE_TOOLS'; +import { createElementsTreeSettings, validateElementsTreeSettings } from './NavigationTreeSettings/createElementsTreeSettings'; +import { DATA_CONTEXT_NAV_TREE_ROOT } from './NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT'; +import { KEY_BINDING_ENABLE_FILTER } from './NavigationTreeSettings/KEY_BINDING_ENABLE_FILTER'; + +@injectable() +export class ElementsTreeToolsMenuService { + constructor( + private readonly actionService: ActionService, + private readonly keyBindingService: KeyBindingService, + private readonly userDataService: UserDataService, + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly menuService: MenuService, + private readonly localizationService: LocalizationService, + private readonly navNodeInfoResource: NavNodeInfoResource, + ) {} + + register() { + this.actionService.addHandler({ + id: 'tree-tools-menu-base-handler', + isActionApplicable(context, action) { + return [ACTION_LINK_OBJECT, ACTION_COLLAPSE_ALL].includes(action); + }, + getActionInfo: (context, action) => { + switch (action) { + case ACTION_LINK_OBJECT: { + const tooltip = this.localizationService.translate('app_navigationTree_action_link_with_editor') + ` (${KEY_BINDING_LINK_OBJECT.label})`; + return { + ...action.info, + tooltip, + }; + } + case ACTION_COLLAPSE_ALL: { + const tooltip = this.localizationService.translate('app_navigationTree_action_collapse_all') + ` (${KEY_BINDING_COLLAPSE_ALL.label})`; + return { + ...action.info, + tooltip, + }; + } + } + + return action.info; + }, + isHidden: (context, action) => { + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); + + if (action === ACTION_LINK_OBJECT && tree) { + const navNode = this.connectionSchemaManagerService.activeNavNode; + const nodeInTree = navNode?.path.includes(tree.baseRoot) && this.navNodeInfoResource.has(navNode.nodeId); + return !nodeInTree; + } + + return false; + }, + handler: async (context, action) => { + if (action === ACTION_COLLAPSE_ALL || action === ACTION_LINK_OBJECT) { + this.elementsTreeActionHandler(context, action); + } + }, + }); + + this.menuService.addCreator({ + isApplicable: context => context.get(DATA_CONTEXT_MENU) === MENU_ELEMENTS_TREE_TOOLS, + getItems: (context, items) => [ + ...items, + ACTION_LINK_OBJECT, + ACTION_COLLAPSE_ALL, + ], + }); + + this.registerBindings(); + } + + private registerBindings() { + this.actionService.addHandler({ + id: 'nav-tree-filter', + isActionApplicable: (contexts, action) => ( + action === ACTION_FILTER + && contexts.has(DATA_CONTEXT_NAV_TREE_ROOT) + ), + handler: this.switchFilter.bind(this), + }); + + this.actionService.addHandler({ + id: 'elements-tree-base', + isActionApplicable: (contexts, action) => ( + contexts.has(DATA_CONTEXT_ELEMENTS_TREE) + && [ACTION_COLLAPSE_ALL, ACTION_LINK_OBJECT].includes(action) + ), + handler: this.elementsTreeActionHandler.bind(this), + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'nav-tree-filter', + binding: KEY_BINDING_ENABLE_FILTER, + isBindingApplicable: (contexts, action) => action === ACTION_FILTER, + handler: this.switchFilter.bind(this), + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'elements-tree-collapse', + binding: KEY_BINDING_COLLAPSE_ALL, + isBindingApplicable: (contexts, action) => action === ACTION_COLLAPSE_ALL, + handler: this.elementsTreeActionHandler.bind(this), + }); + + this.keyBindingService.addKeyBindingHandler({ + id: 'elements-tree-link', + binding: KEY_BINDING_LINK_OBJECT, + isBindingApplicable: (contexts, action) => action === ACTION_LINK_OBJECT, + handler: this.elementsTreeActionHandler.bind(this), + }); + } + + private switchFilter(contexts: IDataContextProvider, action: IAction) { + const context = contexts.get(DATA_CONTEXT_NAV_TREE_ROOT); + + if (context === undefined) { + return; + } + + const state = this.userDataService.getUserData( + getNavigationTreeUserSettingsId(context), + createElementsTreeSettings, + validateElementsTreeSettings + ); + + state.filter = !state.filter; + } + + private elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) { + const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE); + + if (tree === undefined) { + return; + } + + switch (action) { + case ACTION_COLLAPSE_ALL: + tree.collapse(); + break; + case ACTION_LINK_OBJECT: { + const navNode = this.connectionSchemaManagerService.activeNavNode; + + if (navNode?.path.includes(tree.baseRoot) && this.navNodeInfoResource.has(navNode.nodeId)) { + tree.show(navNode.nodeId, navNode.path); + } + break; + } + } + } +} \ No newline at end of file diff --git a/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS.ts b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS.ts new file mode 100644 index 0000000000..be54fa6aa8 --- /dev/null +++ b/webapp/packages/core-app/src/NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS.ts @@ -0,0 +1,11 @@ +/* + * 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 { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_ELEMENTS_TREE_TOOLS = createMenu('elements-tree-tools', 'Tree tools menu'); diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeBootstrap.ts b/webapp/packages/core-app/src/NavigationTree/NavigationTreeBootstrap.ts index 95691bfe41..9efa1ee227 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeBootstrap.ts +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeBootstrap.ts @@ -6,113 +6,21 @@ * you may not use this file except in compliance with the License. */ -import { UserDataService } from '@cloudbeaver/core-authentication'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { ActionService, ACTION_COLLAPSE_ALL, ACTION_FILTER, IAction, IDataContextProvider, KeyBindingService } from '@cloudbeaver/core-view'; -import { ConnectionSchemaManagerService } from '../TopNavBar/ConnectionSchemaManager/ConnectionSchemaManagerService'; -import { ACTION_LINK_OBJECT } from './ElementsTree/ACTION_LINK_OBJECT'; -import { DATA_CONTEXT_ELEMENTS_TREE } from './ElementsTree/DATA_CONTEXT_ELEMENTS_TREE'; -import { createElementsTreeSettings, validateElementsTreeSettings } from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/createElementsTreeSettings'; -import { DATA_CONTEXT_NAV_TREE_ROOT } from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/DATA_CONTEXT_NAV_TREE_ROOT'; -import { KEY_BINDING_ENABLE_FILTER } from './ElementsTree/ElementsTreeTools/NavigationTreeSettings/KEY_BINDING_ENABLE_FILTER'; -import { KEY_BINDING_COLLAPSE_ALL } from './ElementsTree/KEY_BINDING_COLLAPSE_ALL'; -import { KEY_BINDING_LINK_OBJECT } from './ElementsTree/KEY_BINDING_LINK_OBJECT'; -import { getNavigationTreeUserSettingsId } from './getNavigationTreeUserSettingsId'; +import { ElementsTreeToolsMenuService } from './ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService'; @injectable() export class NavigationTreeBootstrap extends Bootstrap { constructor( - private readonly actionService: ActionService, - private readonly keyBindingService: KeyBindingService, - private readonly userDataService: UserDataService, - private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly elementsTreeToolsMenuService: ElementsTreeToolsMenuService ) { super(); } register(): void | Promise { - this.actionService.addHandler({ - id: 'nav-tree-filter', - isActionApplicable: (contexts, action) => ( - action === ACTION_FILTER - && contexts.has(DATA_CONTEXT_NAV_TREE_ROOT) - ), - handler: this.switchFilter.bind(this), - }); - - this.actionService.addHandler({ - id: 'elements-tree-base', - isActionApplicable: (contexts, action) => ( - contexts.has(DATA_CONTEXT_ELEMENTS_TREE) - && [ACTION_COLLAPSE_ALL, ACTION_LINK_OBJECT].includes(action) - ), - handler: this.elementsTreeActionHandler.bind(this), - }); - - this.keyBindingService.addKeyBindingHandler({ - id: 'nav-tree-filter', - binding: KEY_BINDING_ENABLE_FILTER, - isBindingApplicable: (contexts, action) => action === ACTION_FILTER, - handler: this.switchFilter.bind(this), - }); - - this.keyBindingService.addKeyBindingHandler({ - id: 'elements-tree-collapse', - binding: KEY_BINDING_COLLAPSE_ALL, - isBindingApplicable: (contexts, action) => action === ACTION_COLLAPSE_ALL, - handler: this.elementsTreeActionHandler.bind(this), - }); - - this.keyBindingService.addKeyBindingHandler({ - id: 'elements-tree-link', - binding: KEY_BINDING_LINK_OBJECT, - isBindingApplicable: (contexts, action) => action === ACTION_LINK_OBJECT, - handler: this.elementsTreeActionHandler.bind(this), - }); + this.elementsTreeToolsMenuService.register(); } - private switchFilter(contexts: IDataContextProvider, action: IAction) { - const context = contexts.get(DATA_CONTEXT_NAV_TREE_ROOT); - - if (context === undefined) { - return; - } - - const state = this.userDataService.getUserData( - getNavigationTreeUserSettingsId(context), - createElementsTreeSettings, - validateElementsTreeSettings - ); - - state.filter = !state.filter; - } - - private elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) { - const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE); - - if (tree === undefined) { - return; - } - - switch (action) { - case ACTION_COLLAPSE_ALL: - tree.collapse(); - break; - case ACTION_LINK_OBJECT: { - const activeNavNode = this.connectionSchemaManagerService.activeNavNode; - - if (activeNavNode && activeNavNode.path.includes(tree.baseRoot)) { - tree.show( - activeNavNode.nodeId, - activeNavNode.path - ); - } - } - break; - } - } - - async load(): Promise { } } \ No newline at end of file diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index bd4829cf4f..98c9013d40 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -51,7 +51,9 @@ export * from './NavigationTree/ElementsTree/MENU_NAV_TREE'; export * from './NavigationTree/ElementsTree/NavigationNodeComponent'; export * from './NavigationTree/ElementsTree/NavNodeFilterCompareFn'; export * from './NavigationTree/ElementsTree/useElementsTree'; +export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TREE_TOOLS'; export * from './NavigationTree/NavigationTreeBootstrap'; +export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService'; export * from './NavigationTree/NavigationTreeService'; export * from './AppLocaleService'; export * from './QuotasService'; diff --git a/webapp/packages/core-bootstrap/src/manifest.ts b/webapp/packages/core-bootstrap/src/manifest.ts index 277707da20..fd6661f64f 100644 --- a/webapp/packages/core-bootstrap/src/manifest.ts +++ b/webapp/packages/core-bootstrap/src/manifest.ts @@ -44,6 +44,7 @@ import { NavigationTreeBootstrap, ToolsPanelService, QuotasService, + ElementsTreeToolsMenuService, } from '@cloudbeaver/core-app'; import { coreAuthenticationManifest } from '@cloudbeaver/core-authentication'; import { BlocksLocaleService } from '@cloudbeaver/core-blocks'; @@ -115,6 +116,7 @@ export const coreManifests: PluginManifest[] = [ NavNodeInfoResource, SqlGeneratorsBootstrap, SqlGeneratorsResource, + ElementsTreeToolsMenuService, NavigationTreeBootstrap, NavTreeResource, DBObjectResource, diff --git a/webapp/packages/core-view/src/Action/Actions/ACTION_COLLAPSE_ALL.ts b/webapp/packages/core-view/src/Action/Actions/ACTION_COLLAPSE_ALL.ts index a9d4d1022f..573eb516d4 100644 --- a/webapp/packages/core-view/src/Action/Actions/ACTION_COLLAPSE_ALL.ts +++ b/webapp/packages/core-view/src/Action/Actions/ACTION_COLLAPSE_ALL.ts @@ -10,4 +10,5 @@ import { createAction } from '../createAction'; export const ACTION_COLLAPSE_ALL = createAction('collapse-all', { label: 'collapse-all', + icon: '/icons/collapse_sm.svg', }); diff --git a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts index f2a7983d85..e9120c2011 100644 --- a/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts +++ b/webapp/packages/plugin-sql-editor-navigation-tab/src/SqlEditorTabService.ts @@ -16,7 +16,11 @@ import { objectCatalogProvider, objectCatalogSetter, objectSchemaSetter, - ITabOptions + ITabOptions, + objectNavNodeProvider, + NodeManagerUtils, + ConnectionSchemaManagerService, + NavNodeManagerService } from '@cloudbeaver/core-app'; import { ConnectionExecutionContextResource, @@ -52,6 +56,8 @@ export class SqlEditorTabService extends Bootstrap { private readonly connectionExecutionContextService: ConnectionExecutionContextService, private readonly connectionExecutionContextResource: ConnectionExecutionContextResource, private readonly connectionInfoResource: ConnectionInfoResource, + private readonly connectionSchemaManagerService: ConnectionSchemaManagerService, + private readonly navNodeManagerService: NavNodeManagerService ) { super(); @@ -66,6 +72,7 @@ export class SqlEditorTabService extends Bootstrap { onClose: this.handleTabClose.bind(this), canClose: this.handleCanTabClose.bind(this), extensions: [ + objectNavNodeProvider(this.getNavNode.bind(this)), connectionProvider(this.getConnectionId.bind(this)), objectCatalogProvider(this.getObjectCatalogId.bind(this)), objectSchemaProvider(this.getObjectSchemaId.bind(this)), @@ -124,6 +131,29 @@ export class SqlEditorTabService extends Bootstrap { } } + private getNavNode() { + const objectCatalog = this.connectionSchemaManagerService.currentObjectCatalog; + const objectSchema = this.connectionSchemaManagerService.currentObjectSchema; + + const nodeId = objectSchema?.id ?? objectCatalog?.id; + + if (!nodeId) { + return; + } + + const parents = NodeManagerUtils.parentsFromPath(nodeId); + const parent = this.navNodeManagerService.getNode(parents[0]); + + if (parent) { + parents.unshift(parent.parentId); + } + + return { + nodeId, + path: parents, + }; + } + private async handleExecutionContextUpdate(key: ResourceKey) { const tabs = this.navigationTabsService.findTabs( isSQLEditorTab(tab => !!tab.handlerState.executionContext)