Merge pull request #901 from dbeaver/fix/sql-editor-link

fix(core-app): CB-1675 link sql editor
This commit is contained in:
Alexey
2022-06-17 14:16:12 +03:00
committed by GitHub
10 changed files with 313 additions and 132 deletions
@@ -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',
});
@@ -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<React.PropsWithChildren<Props>>(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<React.PropsWithChildren<Props>>(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)(
<tools>
<actions>
<fill />
{activeNavNode && nodeInTree && (
<IconButton
name='/icons/link_editor_sm.svg'
title={translate('app_navigationTree_action_link_with_editor') + ` (${KEY_BINDING_LINK_OBJECT.label})`}
style={toolsStyles}
img
onMouseDown={showObject}
/>
)}
<IconButton
name='/icons/collapse_sm.svg'
title={translate('app_navigationTree_action_collapse_all') + ` (${KEY_BINDING_COLLAPSE_ALL.label})`}
style={toolsStyles}
img
onClick={tree.collapse}
{...use({ primary: true })}
/>
<ElementsTreeToolsMenu />
{tree.settings?.configurable && (
<IconButton
name='/icons/settings_cog_sm.svg'
@@ -0,0 +1,88 @@
/*
* 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 { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { MenuBar } from '@cloudbeaver/core-ui';
import { useMenu } from '@cloudbeaver/core-view';
import { MENU_ELEMENTS_TREE_TOOLS } from './MENU_ELEMENTS_TREE_TOOLS';
const MENU_STYLES = css`
menu-bar {
composes: theme-background-surface theme-text-on-surface theme-typography--body2 from global;
display: flex;
box-sizing: border-box;
height: 24px;
}
menu-bar-item {
composes: theme-ripple from global;
padding: 4px;
margin: 2px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
cursor: pointer;
background: transparent;
outline: none;
color: inherit;
&::before {
border-radius: 3px;
}
&[use|hidden] {
display: none;
}
& IconOrImage {
display: block;
width: 16px;
}
& item-label {
display: none;
}
& Loader {
width: 16px;
}
}
MenuSeparator {
composes: theme-border-color-background from global;
height: 100%;
margin: 0;
border: 0 !important;
border-right: 1px solid !important;
}
`;
const style = css`
menu-wrapper {
display: flex;
height: 100%;
}
`;
interface Props {
className?: string;
}
export const ElementsTreeToolsMenu = observer<Props>(function ElementsTreeToolsMenu({ className }) {
const menu = useMenu({ menu: MENU_ELEMENTS_TREE_TOOLS });
return styled(style)(
<menu-wrapper className={className}>
<MenuBar menu={menu} style={[MENU_STYLES]} />
</menu-wrapper>
);
});
@@ -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;
}
}
}
}
@@ -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');
@@ -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<void> {
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<void> { }
}
+2
View File
@@ -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';
@@ -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,
@@ -10,4 +10,5 @@ import { createAction } from '../createAction';
export const ACTION_COLLAPSE_ALL = createAction('collapse-all', {
label: 'collapse-all',
icon: '/icons/collapse_sm.svg',
});
@@ -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<string>) {
const tabs = this.navigationTabsService.findTabs<ISqlEditorTabState>(
isSQLEditorTab(tab => !!tab.handlerState.executionContext)