Merge pull request #469 from dbeaver/feat/objects-rename-delete

Feat/objects rename delete
This commit is contained in:
Alexey
2021-09-09 19:22:57 +03:00
committed by GitHub
36 changed files with 819 additions and 135 deletions
@@ -294,4 +294,9 @@
<rect width="24" height="24" fill="white"/>
</clipPath>
</defs>
</symbol>
<symbol id="delete" viewBox="0 0 32 32">
<rect width="32" height="32" fill="transparent"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M10 1H22V3H29.077H30V5H28.9229L27.1346 28.2216C27.0186 29.7882 25.7137 31 24.1428 31H7.85748C6.28661 31 4.98172 29.7882 4.86568 28.2216L3.07736 5H2V3H2.92334H10V1ZM5.07697 5L6.86022 28.0739C6.8989 28.5961 7.33386 29 7.85748 29H24.1428C24.6664 29 25.1014 28.5961 25.1401 28.0739L26.9233 5H5.07697ZM14 26L14 8H12L12 26H14ZM21 8L21 26H19V8H21Z" fill="#338ECC"/>
</symbol>
</svg>

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

@@ -7,17 +7,19 @@
*/
import { observer } from 'mobx-react-lite';
import { useCallback, useContext } from 'react';
import { useCallback, useContext, useState } from 'react';
import styled, { css, use } from 'reshadow';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES } from '@cloudbeaver/core-blocks';
import { getComputed, TreeNodeContext, TreeNodeControl, TreeNodeExpand, TreeNodeIcon, TreeNodeName, TREE_NODE_STYLES, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import type { NavNode } from '../../../shared/NodesManager/EntityTypes';
import { EObjectFeature } from '../../../shared/NodesManager/EObjectFeature';
import type { INodeActions } from '../../../shared/NodesManager/INodeActions';
import { NavNodeInfoResource } from '../../../shared/NodesManager/NavNodeInfoResource';
import { TreeNodeMenu } from '../TreeNodeMenu/TreeNodeMenu';
import { NavigationNodeEditor } from './NavigationNodeEditor';
const styles = composes(
css`
@@ -77,6 +79,14 @@ export const NavigationNodeControl = observer<Props>(function NavigationNodeCont
const navNodeInfoResource = useService(NavNodeInfoResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !context.loading);
const [editing, setEditing] = useState(false);
const nodeActions = useObjectRef<INodeActions>({
rename: () => {
setEditing(true);
},
});
const connected = node.objectFeatures.includes(EObjectFeature.dataSourceConnected);
const onClickHandler = useCallback((event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
@@ -84,15 +94,19 @@ export const NavigationNodeControl = observer<Props>(function NavigationNodeCont
}, [context]);
return styled(useStyles(TREE_NODE_STYLES, styles))(
<TreeNodeControl onClick={onClickHandler} {...use({ outdated })}>
<TreeNodeControl onClick={onClickHandler} {...use({ outdated, editing })}>
<TreeNodeExpand />
<TreeNodeIcon icon={node.icon}>
<status {...use({ connected })} />
</TreeNodeIcon>
<TreeNodeName>{node.name}</TreeNodeName>
<portal>
<TreeNodeMenu node={node} selected={context.selected} />
</portal>
<TreeNodeName>
{editing ? <NavigationNodeEditor node={node} onClose={() => setEditing(false)} /> : node.name}
</TreeNodeName>
{!editing && (
<portal>
<TreeNodeMenu node={node} actions={nodeActions} selected={context.selected} />
</portal>
)}
</TreeNodeControl>
);
});
@@ -0,0 +1,61 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { useCallback, useState } from 'react';
import { useService } from '@cloudbeaver/core-di';
import { NotificationService } from '@cloudbeaver/core-events';
import { InlineEditor } from '../../../shared/InlineEditor/InlineEditor';
import type { NavNode } from '../../../shared/NodesManager/EntityTypes';
import { NavTreeResource } from '../../../shared/NodesManager/NavTreeResource';
interface Props {
node: NavNode;
onClose: () => void;
}
export const NavigationNodeEditor = observer<Props>(function NavigationNodeEditor({ node, onClose }) {
const navTreeResource = useService(NavTreeResource);
const notificationService = useService(NotificationService);
const [loading, setLoading] = useState(false);
const [name, setName] = useState(node.name || '');
const save = useCallback(async () => {
if (loading) {
return;
}
try {
if (node.name !== name) {
setLoading(true);
await navTreeResource.changeName(node, name);
}
} catch (exception) {
notificationService.logException(exception, 'app_navigationTree_node_change_name_error');
} finally {
setLoading(false);
onClose();
}
}, [name, onClose, node, loading, navTreeResource, notificationService]);
return (
<InlineEditor
value={name}
disabled={loading}
simple
autofocus
onChange={setName}
onSave={save}
onReject={onClose}
onBlur={onClose}
/>
);
});
@@ -16,22 +16,25 @@ import { useService } from '@cloudbeaver/core-di';
import { MenuTrigger } from '@cloudbeaver/core-dialogs';
import type { NavNode } from '../../../shared/NodesManager/EntityTypes';
import type { INodeActions } from '../../../shared/NodesManager/INodeActions';
import { NavNodeContextMenuService } from '../../../shared/NodesManager/NavNodeContextMenuService';
import { treeNodeMenuStyles } from './treeNodeMenuStyles';
interface Props {
node: NavNode;
actions?: INodeActions;
selected?: boolean;
}
export const TreeNodeMenu = observer<Props>(function TreeNodeMenu({
node,
actions,
selected,
}) {
const navNodeContextMenuService = useService(NavNodeContextMenuService);
const menuPanel = useMemo(
() => navNodeContextMenuService.constructMenuWithContext(node),
() => navNodeContextMenuService.constructMenuWithContext(node, actions),
[node]
);
const isHidden = useMemo(
+1
View File
@@ -42,6 +42,7 @@ export * from './shared/TabEntity/TabEntity';
// Enums
export * from './shared/NodesManager/ENodeFeature';
export * from './shared/NodesManager/EObjectFeature';
export * from './shared/NodesManager/INodeActions';
// hooks
export * from './shared/useChildren';
@@ -17,6 +17,7 @@ export default [
['app_navigationTree_connection_view_option_custom', 'Custom'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Show system objects'],
['app_navigationTree_refreshNode', 'Refresh'],
['app_navigationTree_node_change_name_error', 'Error occurred while changing name'],
['app_navigationTree_context_disconnect', 'Disconnect'],
['app_navigationTree_connection_group_user', 'User Connections'],
['app_navigationTree_connection_group_shared', 'Shared Connections'],
@@ -20,6 +20,7 @@ export default [
['app_navigationTree_connection_view_option_custom', 'Настраиваемое'],
['app_navigationTree_connection_view_option_showSystemObjects', 'Показывать системные объекты'],
['app_navigationTree_refreshNode', 'Обновить'],
['app_navigationTree_node_change_name_error', 'Не удалось изменить название'],
['app_navigationTree_context_disconnect', 'Отключить'],
['app_navigationTree_connection_group_user', 'Пользовательские подключения'],
['app_navigationTree_connection_group_shared', 'Общие подключения'],
@@ -13,7 +13,7 @@ import {
import { useImperativeHandle } from 'react';
import styled, { use } from 'reshadow';
import { Icon, IconOrImage, useObjectRef } from '@cloudbeaver/core-blocks';
import { Icon, IconOrImage, Loader, useObjectRef } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { CommonDialogService, DialogueStateResult } from '@cloudbeaver/core-dialogs';
import { useStyles } from '@cloudbeaver/core-theming';
@@ -32,6 +32,7 @@ export interface InlineEditorProps extends Omit<React.InputHTMLAttributes<HTMLIn
edited?: boolean;
autofocus?: boolean;
active?: boolean;
loading?: boolean;
onChange: (value: string) => void;
onSave: () => void;
onReject?: () => void;
@@ -48,6 +49,8 @@ export const InlineEditor = observer<InlineEditorProps, HTMLInputElement | null>
edited = false,
autofocus,
active,
loading,
disabled,
onChange,
onSave,
onUndo,
@@ -107,24 +110,49 @@ export const InlineEditor = observer<InlineEditorProps, HTMLInputElement | null>
lang="en"
value={value}
autoComplete="off"
disabled={disabled}
onChange={handleChange}
onKeyDown={handleKeyDown}
{...rest}
/>
</editor-container>
<editor-actions as="div" {...use({ position: controlsPosition })} onMouseDown={e => e.preventDefault()}>
{!hideSave && <editor-action as="div" onClick={onSave}><Icon name="apply" viewBox="0 0 12 10" /></editor-action>}
{!hideCancel && onReject && <editor-action as="div" onClick={onReject}><Icon name="reject" viewBox="0 0 11 11" /></editor-action>}
{!hideSave && (
<editor-action
as="button"
disabled={disabled}
onClick={onSave}
>
{loading ? <Loader small fullSize /> : <Icon name="apply" viewBox="0 0 12 10" />}
</editor-action>
)}
{!hideCancel && onReject && (
<editor-action
as="button"
disabled={disabled}
onClick={onReject}
>
<Icon name="reject" viewBox="0 0 11 11" />
</editor-action>
)}
{onUndo && (
<editor-action
as="div"
as="button"
disabled={!edited || disabled}
onClick={edited ? onUndo : undefined}
{...use({ disabled: !edited })}
>
<IconOrImage icon="/icons/data_revert.svg" />
</editor-action>
)}
{!simple && <editor-action as="div" onClick={handlePopup}><Icon name="edit" viewBox="0 0 13 13" /></editor-action>}
{!simple && (
<editor-action
as="button"
disabled={disabled}
onClick={handlePopup}
>
<Icon name="edit" viewBox="0 0 13 13" />
</editor-action>
)}
</editor-actions>
</editor>
);
@@ -69,7 +69,6 @@ export const InlineEditorStyles = composes(
height: 100%;
display: flex;
flex-direction: row;
border: solid 1px;
}
@@ -114,22 +113,24 @@ export const InlineEditorStyles = composes(
editor-action {
box-sizing: border-box;
display: flex;
align-items: center;
width: 24px;
padding: 4px;
cursor: pointer;
background: transparent;
& Loader {
width: 100%;
height: 100%;
}
& IconOrImage {
display: block;
width: 100%;
}
&[use|disabled] {
cursor: default;
opacity: 0.65;
&:before {
display: none;
}
&[disabled]::before {
display: none;
}
}
`
@@ -8,23 +8,22 @@
import { ConnectionInfoResource, ConnectionsManagerService, EConnectionFeature } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs';
import { ContextMenuService } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { EMainMenu, MainMenuService } from '../../TopNavBar/MainMenu/MainMenuService';
import type { NavNode } from './EntityTypes';
import { EObjectFeature } from './EObjectFeature';
import { NavNodeContextMenuService } from './NavNodeContextMenuService';
import { INodeMenuData, NavNodeContextMenuService } from './NavNodeContextMenuService';
import { NodeManagerUtils } from './NodeManagerUtils';
@injectable()
export class ConnectionDialogsService extends Bootstrap {
constructor(
private mainMenuService: MainMenuService,
private contextMenuService: ContextMenuService,
private connectionsManagerService: ConnectionsManagerService,
private connectionInfoResource: ConnectionInfoResource,
private notificationService: NotificationService,
private readonly mainMenuService: MainMenuService,
private readonly contextMenuService: ContextMenuService,
private readonly connectionsManagerService: ConnectionsManagerService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly notificationService: NotificationService,
) {
super();
}
@@ -41,22 +40,21 @@ export class ConnectionDialogsService extends Bootstrap {
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'closeConnection',
isPresent:
(context: IMenuContext<NavNode>) => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: (context: IMenuContext<NavNode>) => {
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(context.data.id);
title: 'app_navigationTree_context_disconnect',
isPresent: context => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: context => {
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(context.data.node.id);
const connection = this.connectionInfoResource.get(connectionId);
return !context.data.objectFeatures.includes(EObjectFeature.dataSource)
return !context.data.node.objectFeatures.includes(EObjectFeature.dataSource)
|| !connection?.connected;
},
title: 'app_navigationTree_context_disconnect',
onClick: (context: IMenuContext<NavNode>) => {
const node = context.data;
onClick: context => {
const node = context.data.node;
this.connectionsManagerService.closeConnectionAsync(
NodeManagerUtils.connectionNodeIdToConnectionId(node.id)
);
@@ -64,22 +62,21 @@ export class ConnectionDialogsService extends Bootstrap {
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'deleteConnection',
isPresent:
(context: IMenuContext<NavNode>) => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: (context: IMenuContext<NavNode>) => {
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(context.data.id);
title: 'ui_delete',
isPresent: context => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: context => {
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(context.data.node.id);
const connection = this.connectionInfoResource.get(connectionId);
return !context.data.objectFeatures.includes(EObjectFeature.dataSource)
return !context.data.node.objectFeatures.includes(EObjectFeature.dataSource)
|| !connection?.features.includes(EConnectionFeature.manageable);
},
title: 'ui_delete',
onClick: async (context: IMenuContext<NavNode>) => {
const node = context.data;
onClick: async context => {
const node = context.data.node;
try {
await this.connectionsManagerService.deleteConnection(
NodeManagerUtils.connectionNodeIdToConnectionId(node.id)
@@ -14,4 +14,6 @@ export enum ENodeFeature {
'dataContainer' = 'dataContainer',
'item' = 'item',
'leaf' = 'leaf',
'canDelete' = 'canDelete',
'canRename' = 'canRename'
}
@@ -0,0 +1,12 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0.
* you may not use this file except in compliance with the License.
*/
export interface INodeActions {
readonly rename?: () => void;
}
@@ -6,17 +6,27 @@
* you may not use this file except in compliance with the License.
*/
import { ProcessSnackbar } from '@cloudbeaver/core-blocks';
import { ConnectionInfoResource } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ContextMenuService, IMenuPanel } from '@cloudbeaver/core-dialogs';
import { CommonDialogService, ConfirmationDialog, ContextMenuService, DialogueStateResult, IMenuPanel, RenameDialog } from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { isNavigatorViewSettingsEqual, CONNECTION_NAVIGATOR_VIEW_SETTINGS, NavigatorViewSettings } from '@cloudbeaver/core-root';
import { ENodeFeature } from './ENodeFeature';
import type { NavNode } from './EntityTypes';
import { EObjectFeature } from './EObjectFeature';
import type { INodeActions } from './INodeActions';
import { getNodeName } from './NavNodeInfoResource';
import { NavNodeManagerService } from './NavNodeManagerService';
import { NavTreeResource } from './NavTreeResource';
import { NodeManagerUtils } from './NodeManagerUtils';
export interface INodeMenuData {
node: NavNode;
actions?: INodeActions;
}
@injectable()
export class NavNodeContextMenuService extends Bootstrap {
static nodeContextType = 'NodeWithParent';
@@ -24,10 +34,12 @@ export class NavNodeContextMenuService extends Bootstrap {
private static menuToken = 'navTreeMenu';
constructor(
private contextMenuService: ContextMenuService,
private navNodeManagerService: NavNodeManagerService,
private notificationService: NotificationService,
private connectionInfoResource: ConnectionInfoResource
private readonly contextMenuService: ContextMenuService,
private readonly navNodeManagerService: NavNodeManagerService,
private readonly notificationService: NotificationService,
private readonly commonDialogService: CommonDialogService,
private readonly connectionInfoResource: ConnectionInfoResource,
private readonly navTreeResource: NavTreeResource,
) {
super();
}
@@ -40,12 +52,15 @@ export class NavNodeContextMenuService extends Bootstrap {
return NavNodeContextMenuService.nodeViewMenuItemToken;
}
constructMenuWithContext(node: NavNode): IMenuPanel {
return this.contextMenuService.createContextMenu<NavNode>({
constructMenuWithContext(node: NavNode, actions?: INodeActions): IMenuPanel {
return this.contextMenuService.createContextMenu<INodeMenuData>({
menuId: this.getMenuToken(),
contextId: node.id,
contextType: NavNodeContextMenuService.nodeContextType,
data: node,
data: {
node,
actions,
},
});
}
@@ -74,75 +89,75 @@ export class NavNodeContextMenuService extends Bootstrap {
}
}
registerNodeViewMenuItem() {
this.contextMenuService.addMenuItem<NavNode>(
registerNodeViewMenuItem(): void {
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: this.getNodeViewMenuItemToken(),
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
},
isHidden: context => {
const connection = this.getConnectionFromNodeId(context.data.id);
return !connection?.connected;
},
order: 2,
title: 'app_navigationTree_connection_view',
isPanel: true,
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
isHidden: context => {
const connection = this.getConnectionFromNodeId(context.data.node.id);
return !connection?.connected;
},
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.getNodeViewMenuItemToken(),
{
id: 'simple',
title: 'app_navigationTree_connection_view_option_simple',
type: 'radio',
isChecked: context => this.isSimpleNavigatorView(context.data.id),
isChecked: context => this.isSimpleNavigatorView(context.data.node.id),
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
onClick: async context =>
await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple),
await this.changeConnectionView(context.data.node.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.simple),
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.getNodeViewMenuItemToken(),
{
id: 'advanced',
title: 'app_navigationTree_connection_view_option_advanced',
type: 'radio',
isChecked: context => !this.isSimpleNavigatorView(context.data.id),
separator: true,
isChecked: context => !this.isSimpleNavigatorView(context.data.node.id),
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
onClick: async context =>
await this.changeConnectionView(context.data.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced),
await this.changeConnectionView(context.data.node.id, CONNECTION_NAVIGATOR_VIEW_SETTINGS.advanced),
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.getNodeViewMenuItemToken(),
{
id: 'systemObjects',
title: 'app_navigationTree_connection_view_option_showSystemObjects',
type: 'checkbox',
isChecked: context => !!this.getConnectionFromNodeId(context.data.id)?.navigatorSettings.showSystemObjects,
isChecked: context => !!this.getConnectionFromNodeId(context.data.node.id)?.navigatorSettings.showSystemObjects,
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
onClick: async context => {
const currentSettings = this.getConnectionFromNodeId(context.data.id)?.navigatorSettings;
const currentSettings = this.getConnectionFromNodeId(context.data.node.id)?.navigatorSettings;
if (!currentSettings) {
return;
}
return await this.changeConnectionView(context.data.id, {
return await this.changeConnectionView(context.data.node.id, {
...currentSettings,
showSystemObjects: !currentSettings.showSystemObjects,
});
@@ -152,33 +167,33 @@ export class NavNodeContextMenuService extends Bootstrap {
}
register(): void {
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'openNodeTab',
order: 1,
title: 'app_navigationTree_openNodeTab',
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType;
},
order: 1,
title: 'app_navigationTree_openNodeTab',
onClick: context => {
const node = context.data;
const node = context.data.node;
this.navNodeManagerService.navToNode(node.id, node.parentId);
},
}
);
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'refreshNode',
order: Number.MAX_SAFE_INTEGER,
title: 'app_navigationTree_refreshNode',
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType;
},
order: Number.MAX_SAFE_INTEGER,
title: 'app_navigationTree_refreshNode',
onClick: async context => {
const node = context.data;
const node = context.data.node;
try {
await this.navNodeManagerService.refreshTree(node.id);
} catch (exception) {
@@ -188,6 +203,79 @@ export class NavNodeContextMenuService extends Bootstrap {
}
);
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'rename',
icon: 'edit',
title: 'ui_rename',
isPresent: context => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: context => !context.data.node.features?.includes(ENodeFeature.canRename),
onClick: async context => {
const node = context.data.node;
if (context.data.actions?.rename) {
context.data.actions.rename();
} else {
const name = node.name || '';
const result = await this.commonDialogService.open(RenameDialog, {
value: name,
subTitle: name,
objectName: node.nodeType || 'Object',
icon: node.icon,
});
if (result !== DialogueStateResult.Rejected && result !== DialogueStateResult.Resolved) {
if (name !== result) {
const notification = this.notificationService.processNotification(() => ProcessSnackbar, {}, { title: 'ui_rename_processing' });
try {
await this.navTreeResource.changeName(node, result);
const message = `prev: ${name}\nnew: ${result}`;
notification.controller.resolve(`${node.nodeType} was renamed`, message);
} catch (exception) {
notification.controller.reject(exception, 'Error occured while renaming');
}
}
}
}
},
}
);
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'deleteNode',
title: 'ui_delete',
isPresent: context => context.contextType === NavNodeContextMenuService.nodeContextType,
isHidden: context => !context.data.node.features?.includes(ENodeFeature.canDelete)
|| context.data.node.objectFeatures.includes(EObjectFeature.dataSource),
onClick: async context => {
const node = context.data.node;
const nodeName = getNodeName(node);
const result = await this.commonDialogService.open(ConfirmationDialog, {
title: 'ui_data_delete_confirmation',
subTitle: node.name,
message: `You're going to delete "${nodeName}". Are you sure?`,
confirmActionText: 'ui_delete',
icon: node.icon,
});
if (result === DialogueStateResult.Rejected) {
return;
}
try {
await this.navTreeResource.deleteNode(node.id);
} catch (exception) {
this.notificationService.logException(exception, `Failed to delete "${nodeName}"`);
}
},
}
);
this.registerNodeViewMenuItem();
}
@@ -92,3 +92,7 @@ export class NavNodeInfoResource extends CachedMapResource<string, NavNode> {
return this.navNodeInfoToNavNode(navNodeInfo);
}
}
export function getNodeName(node: NavNode): string {
return `${node.nodeType || 'Object'}${node.name ? ' (' + node.name + ')' : ''}`;
}
@@ -114,7 +114,7 @@ export class NavNodeManagerService extends Bootstrap {
register(): void { }
load(): void {}
load(): void { }
async navToNode(nodeId: string, parentId: string, folderId?: string): Promise<void> {
await this.navigator.execute({
@@ -315,7 +315,7 @@ export class NavNodeManagerService extends Bootstrap {
private async navigateHandler(
data: INodeNavigationData,
contexts: IExecutionContextProvider<INodeNavigationData>
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
): Promise<void> {
const nodeInfo = await contexts.getContext(this.navigationNavNodeContext);
@@ -26,6 +26,7 @@ import {
import { MetadataMap } from '@cloudbeaver/core-utils';
import { CoreSettingsService } from '../../CoreSettingsService';
import type { NavNode } from './EntityTypes';
import { NavNodeInfoResource, ROOT_NODE_PATH } from './NavNodeInfoResource';
import { NodeManagerUtils } from './NodeManagerUtils';
@@ -54,7 +55,7 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
private navNodeInfoResource: NavNodeInfoResource,
private coreSettingsService: CoreSettingsService,
private sessionDataResource: SessionDataResource,
private connectionInfo: ConnectionInfoResource
private connectionInfo: ConnectionInfoResource,
) {
super();
@@ -110,6 +111,35 @@ export class NavTreeResource extends CachedMapResource<string, string[]> {
});
}
async deleteNode(key: ResourceKey<string>): Promise<void> {
const nodePaths = isResourceKeyList(key) ? key.list : [key];
await this.performUpdate(key, [], async () => {
await this.graphQLService.sdk.navDeleteNodes({ nodePaths });
});
runInAction(() => {
for (const path of nodePaths) {
const node = this.navNodeInfoResource.get(path);
if (node) {
this.deleteInNode(node.parentId, [path]);
}
}
});
}
async changeName(node: NavNode, name: string): Promise<void> {
await this.performUpdate(node.id, [], async () => {
await this.graphQLService.sdk.navRenameNode({
nodePath: node.id,
newName: name,
});
});
await this.refreshTree(node.parentId);
}
deleteInNode(key: string, value: string[]): void;
deleteInNode(key: ResourceKeyList<string>, value: string[][]): void;
deleteInNode(keyObject: ResourceKey<string>, valueObject: string[] | string[][]): void {
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" fill="none" viewBox="0 0 40 40">
<circle cx="20" cy="20" r="20" fill="#DF303E" fill-opacity=".2"/>
<path fill="#DF303E" fill-rule="evenodd" d="M20 33c7.18 0 13-5.82 13-13S27.18 7 20 7 7 12.82 7 20s5.82 13 13 13zm-.986-10.668l-.205-9.434h2.373l-.196 9.434h-1.972zm2.49 3.428c0 .869-.596 1.455-1.494 1.455-.908 0-1.514-.586-1.514-1.455 0-.88.606-1.465 1.514-1.465.898 0 1.494.586 1.494 1.465z" clip-rule="evenodd"/>
<svg width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle opacity="0.3" cx="20" cy="20" r="20" fill="#E73E52"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.8741 9.51443C20.4931 8.82852 19.5067 8.82852 19.1257 9.51443L9.12724 27.5152C8.75702 28.1817 9.23899 29.0007 10.0014 29.0007H29.9984C30.7608 29.0007 31.2428 28.1817 30.8726 27.5152L20.8741 9.51443ZM19.2303 22.4125L19.0744 14.9599H20.878L20.7296 22.4125H19.2303ZM21.123 25.1204C21.123 25.807 20.6702 26.2699 19.9874 26.2699C19.2971 26.2699 18.8369 25.807 18.8369 25.1204C18.8369 24.4261 19.2971 23.9632 19.9874 23.9632C20.6702 23.9632 21.123 24.4261 21.123 25.1204Z" fill="#E73E52"/>
</svg>

Before

Width:  |  Height:  |  Size: 492 B

After

Width:  |  Height:  |  Size: 709 B

@@ -21,6 +21,7 @@ import { TableItemContext } from './TableItemContext';
interface Props {
checked?: boolean;
disabled?: boolean;
tooltip?: string;
className?: string;
}
@@ -31,7 +32,7 @@ const checkboxStyles = css`
}
`;
export const TableItemSelect = observer<Props>(function TableItemSelect({ checked, disabled, className }) {
export const TableItemSelect = observer<Props>(function TableItemSelect({ checked, disabled, tooltip, className }) {
const tableContext = useContext(TableContext);
const context = useContext(TableItemContext);
const styles = useStyles();
@@ -52,6 +53,7 @@ export const TableItemSelect = observer<Props>(function TableItemSelect({ checke
return styled(styles, checkboxStyles)(
<Checkbox
className={className}
title={tooltip}
disabled={context.selectDisabled || disabled}
checked={checked || context.isSelected()}
onClick={handleClick}
@@ -50,6 +50,10 @@ export const TREE_NODE_STYLES = composes(
width: 100%;
}
&[|editing]::before {
display: none;
}
& > * {
margin-right: 4px;
margin-left: 4px;
@@ -136,5 +140,4 @@ export const TREE_NODE_STYLES = composes(
node TreeNodeControl[big] + TreeNodeNested {
padding-left: 46px;
}
`
);
`);
@@ -0,0 +1,97 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 } from 'react';
import styled, { css } from 'reshadow';
import { BASE_CONTAINERS_STYLES, Button, Container, InputFieldNew, SubmittingForm, useFocus } from '@cloudbeaver/core-blocks';
import { Translate, useTranslate } from '@cloudbeaver/core-localization';
import { useStyles } from '@cloudbeaver/core-theming';
import { CommonDialogWrapper } from './CommonDialog/CommonDialogWrapper';
import type { DialogComponent } from './CommonDialogService';
const style = css`
footer {
align-items: center;
}
fill {
flex: 1;
}
`;
export interface RenameDialogPayload {
value: string;
objectName: string;
icon?: string;
subTitle?: string;
bigIcon?: boolean;
viewBox?: string;
confirmActionText?: string;
}
export const RenameDialog: DialogComponent<RenameDialogPayload, string> = function RenameDialog({
payload,
resolveDialog,
rejectDialog,
className,
}) {
const translate = useTranslate();
const [focusedRef] = useFocus<HTMLFormElement>({ focusFirstChild: true });
const { icon, subTitle, bigIcon, viewBox, value, objectName, confirmActionText } = payload;
const title = `${translate('ui_rename')} ${objectName}`;
const [name, setName] = useState(value);
return styled(useStyles(style, BASE_CONTAINERS_STYLES))(
<CommonDialogWrapper
size='small'
subTitle={subTitle}
title={title}
icon={icon}
viewBox={viewBox}
bigIcon={bigIcon}
className={className}
style={style}
footer={(
<>
<Button
type="button"
mod={['outlined']}
onClick={rejectDialog}
>
<Translate token='ui_processing_cancel' />
</Button>
<fill />
<Button
type="button"
mod={['unelevated']}
onClick={() => resolveDialog(name)}
>
<Translate token={confirmActionText || 'ui_processing_ok'} />
</Button>
</>
)}
fixedWidth
onReject={rejectDialog}
>
<SubmittingForm ref={focusedRef} onSubmit={() => resolveDialog(name)}>
<Container center>
<InputFieldNew
value={name}
onChange={value => setName(String(value))}
>
{translate('ui_name') + ':'}
</InputFieldNew>
</Container>
</SubmittingForm>
</CommonDialogWrapper>
);
};
@@ -1,6 +1,7 @@
export * from './CommonDialog/CommonDialog/CommonDialogWrapper';
export * from './CommonDialog/CommonDialogService';
export * from './CommonDialog/ConfirmationDialog';
export * from './CommonDialog/RenameDialog';
export * from './CommonDialog/DialogsPortal';
export * from './Menu/IMenuPanel';
@@ -37,6 +37,10 @@ export const defaultENLocale = [
['ui_revoke', 'Revoke'],
['ui_grant', 'Grant'],
['ui_edit', 'Edit'],
['ui_rename', 'Rename'],
['ui_rename_processing', 'Renaming...'],
['ui_name', 'Name'],
['ui_cant_delete_item', "This item can't be deleted"],
['root_permission_denied', 'You have no permissions'],
['app_root_session_expire_warning_title', 'Session is about to expire'],
@@ -35,6 +35,10 @@ export const defaultRULocale = [
['ui_revoke', 'Запретить'],
['ui_grant', 'Разрешить'],
['ui_edit', 'Редактировать'],
['ui_rename', 'Переименовать'],
['ui_rename_processing', 'Переименование...'],
['ui_name', 'Название'],
['ui_cant_delete_item', 'Этот элемент нельзя удалить'],
['root_permission_denied', 'Отказано в доступе'],
['app_root_session_expire_warning_title', 'Ваша сессия скоро закончится'],
@@ -0,0 +1,3 @@
mutation navDeleteNodes($nodePaths: [ID!]!) {
navDeleteNodes(nodePaths: $nodePaths)
}
@@ -0,0 +1,3 @@
mutation navRenameNode($nodePath: ID!, $newName: String!) {
navRenameNode(nodePath: $nodePath, newName: $newName)
}
+40
View File
@@ -297,6 +297,8 @@ export interface Mutation {
createConnectionFromTemplate: ConnectionInfo;
deleteConnection: Scalars['Boolean'];
initConnection: ConnectionInfo;
navDeleteNodes?: Maybe<Scalars['Int']>;
navRenameNode?: Maybe<Scalars['String']>;
/** @deprecated Field no longer supported */
openConnection: ConnectionInfo;
openSession: SessionInfo;
@@ -391,6 +393,15 @@ export interface MutationInitConnectionArgs {
saveCredentials?: Maybe<Scalars['Boolean']>;
}
export interface MutationNavDeleteNodesArgs {
nodePaths: Array<Scalars['ID']>;
}
export interface MutationNavRenameNodeArgs {
nodePath: Scalars['ID'];
newName: Scalars['String'];
}
export interface MutationOpenConnectionArgs {
config: ConnectionConfig;
}
@@ -1783,6 +1794,12 @@ export interface GetDbObjectInfoQuery {
)>; };
}
export type NavDeleteNodesMutationVariables = Exact<{
nodePaths: Array<Scalars['ID']> | Scalars['ID'];
}>;
export type NavDeleteNodesMutation = Pick<Mutation, 'navDeleteNodes'>;
export type NavNodeChildrenQueryVariables = Exact<{
parentPath: Scalars['ID'];
withDetails: Scalars['Boolean'];
@@ -1803,6 +1820,13 @@ export type NavRefreshNodeQueryVariables = Exact<{
export type NavRefreshNodeQuery = Pick<Query, 'navRefreshNode'>;
export type NavRenameNodeMutationVariables = Exact<{
nodePath: Scalars['ID'];
newName: Scalars['String'];
}>;
export type NavRenameNodeMutation = Pick<Mutation, 'navRenameNode'>;
export type QuerySqlCompletionProposalsQueryVariables = Exact<{
connectionId: Scalars['ID'];
contextId: Scalars['ID'];
@@ -2890,6 +2914,11 @@ export const GetDbObjectInfoDocument = `
}
}
${NavNodePropertiesFragmentDoc}`;
export const NavDeleteNodesDocument = `
mutation navDeleteNodes($nodePaths: [ID!]!) {
navDeleteNodes(nodePaths: $nodePaths)
}
`;
export const NavNodeChildrenDocument = `
query navNodeChildren($parentPath: ID!, $withDetails: Boolean!) {
navNodeChildren(parentPath: $parentPath) {
@@ -2912,6 +2941,11 @@ export const NavRefreshNodeDocument = `
navRefreshNode(nodePath: $nodePath)
}
`;
export const NavRenameNodeDocument = `
mutation navRenameNode($nodePath: ID!, $newName: String!) {
navRenameNode(nodePath: $nodePath, newName: $newName)
}
`;
export const QuerySqlCompletionProposalsDocument = `
query querySqlCompletionProposals($connectionId: ID!, $contextId: ID!, $position: Int!, $query: String!, $simple: Boolean, $maxResults: Int) {
sqlCompletionProposals(
@@ -3267,6 +3301,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
getDBObjectInfo(variables: GetDbObjectInfoQueryVariables): Promise<GetDbObjectInfoQuery> {
return withWrapper(() => client.request<GetDbObjectInfoQuery>(GetDbObjectInfoDocument, variables));
},
navDeleteNodes(variables: NavDeleteNodesMutationVariables): Promise<NavDeleteNodesMutation> {
return withWrapper(() => client.request<NavDeleteNodesMutation>(NavDeleteNodesDocument, variables));
},
navNodeChildren(variables: NavNodeChildrenQueryVariables): Promise<NavNodeChildrenQuery> {
return withWrapper(() => client.request<NavNodeChildrenQuery>(NavNodeChildrenDocument, variables));
},
@@ -3276,6 +3313,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper =
navRefreshNode(variables: NavRefreshNodeQueryVariables): Promise<NavRefreshNodeQuery> {
return withWrapper(() => client.request<NavRefreshNodeQuery>(NavRefreshNodeDocument, variables));
},
navRenameNode(variables: NavRenameNodeMutationVariables): Promise<NavRenameNodeMutation> {
return withWrapper(() => client.request<NavRenameNodeMutation>(NavRenameNodeDocument, variables));
},
querySqlCompletionProposals(variables: QuerySqlCompletionProposalsQueryVariables): Promise<QuerySqlCompletionProposalsQuery> {
return withWrapper(() => client.request<QuerySqlCompletionProposalsQuery>(QuerySqlCompletionProposalsDocument, variables));
},
@@ -10,7 +10,7 @@ import {
NavNodeContextMenuService,
EObjectFeature,
NodeManagerUtils,
NavNode
INodeMenuData
} from '@cloudbeaver/core-app';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ContextMenuService } from '@cloudbeaver/core-dialogs';
@@ -27,16 +27,16 @@ export class PublicConnectionFormBootstrap extends Bootstrap {
}
register(): void | Promise<void> {
this.contextMenuService.addMenuItem<NavNode>(this.contextMenuService.getRootMenuToken(), {
this.contextMenuService.addMenuItem<INodeMenuData>(this.contextMenuService.getRootMenuToken(), {
id: 'connection-edit',
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
title: 'connections_public_connection_edit_menu_item_title',
order: 2,
onClick: context => {
const node = context.data;
const node = context.data.node;
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id);
this.publicConnectionFormService.open({ connectionId });
},
@@ -6,7 +6,7 @@
* you may not use this file except in compliance with the License.
*/
import { NavNodeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature } from '@cloudbeaver/core-app';
import { NavNodeContextMenuService, NodeManagerUtils, NavNode, EObjectFeature, INodeMenuData } from '@cloudbeaver/core-app';
import { injectable } from '@cloudbeaver/core-di';
import { IMenuContext, CommonDialogService, ContextMenuService } from '@cloudbeaver/core-dialogs';
import { TableFooterMenuService, ITableFooterMenuContext, IDatabaseDataSource, IDataContainerOptions } from '@cloudbeaver/plugin-data-viewer';
@@ -43,7 +43,7 @@ export class DataExportMenuService {
onClick: this.exportData.bind(this),
});
this.contextMenuService.addMenuItem<NavNode>(
this.contextMenuService.addMenuItem<INodeMenuData>(
this.contextMenuService.getRootMenuToken(),
{
id: 'export',
@@ -51,11 +51,11 @@ export class DataExportMenuService {
title: 'data_transfer_dialog_export',
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataContainer);
&& context.data.node.objectFeatures.includes(EObjectFeature.dataContainer);
},
isHidden: () => this.dataExportSettingsService.settings.getValue('disabled'),
onClick: context => {
const node = context.data;
const node = context.data.node;
const connectionId = NodeManagerUtils.nodeIdToConnectionId(node.id);
this.commonDialogService.open(DataExportDialog, {
connectionId,
@@ -76,14 +76,17 @@ interface Props {
export const Item = observer<Props>(function Item({
objectId, columns,
}) {
const styles = useStyles(itemStyles);
const { node } = useNode(objectId);
const { dbObject } = useDatabaseObjectInfo(objectId);
const styles = useStyles(itemStyles);
if (!node) {
return styled(styles)(
<TableItem item={objectId}>
<TableColumnValue centerContent><TableItemSelect /></TableColumnValue>
<TableColumnValue centerContent>
<TableItemSelect />
</TableColumnValue>
<TableColumnValue>
<icon><placeholder /></icon>
</TableColumnValue>
@@ -97,7 +100,9 @@ export const Item = observer<Props>(function Item({
if (!dbObject?.properties || dbObject.properties.length === 0) {
return styled(styles)(
<TableItem item={objectId}>
<TableColumnValue centerContent><TableItemSelect /></TableColumnValue>
<TableColumnValue centerContent>
<TableItemSelect />
</TableColumnValue>
<TableColumnValue>
<icon>
<StaticImage icon={node.icon} />
@@ -110,7 +115,9 @@ export const Item = observer<Props>(function Item({
return styled(styles)(
<TableItem item={objectId}>
<TableColumnValue centerContent><TableItemSelect /></TableColumnValue>
<TableColumnValue centerContent>
<TableItemSelect />
</TableColumnValue>
<TableColumnValue>
<icon>
<StaticImage icon={node.icon} />
@@ -10,24 +10,42 @@ import { observer } from 'mobx-react-lite';
import styled, { css } from 'reshadow';
import { useDatabaseObjectInfo } from '@cloudbeaver/core-app';
import { TableHeader, TableBody, Table } from '@cloudbeaver/core-blocks';
import { TableHeader, TableBody, Table, useTable } from '@cloudbeaver/core-blocks';
import { composes, useStyles } from '@cloudbeaver/core-theming';
import { Header } from './Header';
import { Item } from './Item';
import { ObjectPropertyTableFooter } from './ObjectPropertyTableFooter';
const style = composes(
css`
TableHeader {
composes: theme-background-surface from global;
}
ObjectPropertyTableFooter {
composes: theme-background-secondary theme-text-on-secondary theme-border-color-background from global;
}
`,
css`
wrapper {
overflow: auto;
height: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
}
table-container {
flex: 1;
overflow: auto;
}
TableHeader {
position: sticky;
top: 0;
z-index: 1;
}
ObjectPropertyTableFooter {
border-top: 1px solid;
}
`,
);
@@ -39,22 +57,30 @@ export const ObjectChildrenPropertyTable = observer<Props>(function ObjectProper
nodeIds,
}) {
const firstChild = nodeIds[0] || '';
const properties = useDatabaseObjectInfo(firstChild).dbObject?.properties;
const dbObject = useDatabaseObjectInfo(firstChild).dbObject;
const properties = dbObject?.properties;
const table = useTable();
return styled(useStyles(style))(
<Table>
<TableHeader>
<Header properties={properties || []} />
</TableHeader>
<TableBody>
{nodeIds.map(id => (
<Item
key={id}
objectId={id}
columns={properties?.length || 0}
/>
))}
</TableBody>
</Table>
<wrapper>
<table-container>
<Table selectedItems={table.selected}>
<TableHeader>
<Header properties={properties || []} />
</TableHeader>
<TableBody>
{nodeIds.map(id => (
<Item
key={id}
objectId={id}
columns={properties?.length || 0}
/>
))}
</TableBody>
</Table>
</table-container>
<ObjectPropertyTableFooter nodeIds={nodeIds} tableState={table} />
</wrapper>
);
});
@@ -20,6 +20,7 @@ import { ObjectChildrenPropertyTable } from './ObjectChildrenPropertyTable';
const styles = css`
div {
flex: auto;
overflow: hidden;
}
`;
@@ -0,0 +1,33 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { ITableState, ToolsPanel } from '@cloudbeaver/core-blocks';
import { useService } from '@cloudbeaver/core-di';
import { ObjectPropertyTableFooterItem } from './ObjectPropertyTableFooterItem';
import { ObjectPropertyTableFooterService } from './ObjectPropertyTableFooterService';
interface Props {
nodeIds: string[];
tableState: ITableState;
className?: string;
}
export const ObjectPropertyTableFooter = observer<Props>(function ObjectPropertyTableFooter({ nodeIds, tableState, className }) {
const service = useService(ObjectPropertyTableFooterService);
return (
<ToolsPanel className={className}>
{service.constructMenuWithContext(nodeIds, tableState).map((topItem, i) => (
<ObjectPropertyTableFooterItem key={i} menuItem={topItem} />
))}
</ToolsPanel>
);
});
@@ -0,0 +1,99 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 type { ButtonHTMLAttributes } from 'react';
import styled, { css, use } from 'reshadow';
import { IconOrImage, ToolsAction } from '@cloudbeaver/core-blocks';
import { IMenuItem, MenuTrigger } from '@cloudbeaver/core-dialogs';
import { useTranslate } from '@cloudbeaver/core-localization';
import { composes, useStyles } from '@cloudbeaver/core-theming';
type Props = ButtonHTMLAttributes<any> & {
menuItem: IMenuItem;
};
const style = composes(
css`
Menu {
composes: theme-text-on-surface from global;
}
MenuTrigger {
composes: theme-ripple from global;
}
`,
css`
MenuTrigger {
height: 100%;
padding: 0 16px;
display: flex;
align-items: center;
cursor: pointer;
&[|hidden] {
display: none;
}
}
ToolsAction[|hidden] {
display: none;
}
menu-trigger-icon IconOrImage {
display: block;
width: 24px;
}
menu-trigger-title {
display: block;
}
menu-trigger-icon + menu-trigger-title {
padding-left: 8px;
}
`
);
export const ObjectPropertyTableFooterItem = observer<Props>(function ObjectPropertyTableFooterItem({
menuItem,
...props
}) {
const translate = useTranslate();
const styles = useStyles(style);
if (!menuItem.panel) {
return styled(styles)(
<ToolsAction
{...props}
{...use({ hidden: menuItem.isHidden })}
title={translate(menuItem.tooltip)}
icon={menuItem.icon}
viewBox="0 0 32 32"
disabled={menuItem.isDisabled}
onClick={() => menuItem.onClick?.()}
>
{translate(menuItem.title)}
</ToolsAction>
);
}
return styled(styles)(
<MenuTrigger
{...props}
{...use({ hidden: menuItem.isHidden })}
title={translate(menuItem.tooltip)}
panel={menuItem.panel}
disabled={menuItem.isDisabled}
style={[style]}
modal
>
{menuItem.icon && (
<menu-trigger-icon>
<IconOrImage icon={menuItem.icon} viewBox="0 0 32 32" />
</menu-trigger-icon>
)}
{menuItem.title && <menu-trigger-title>{translate(menuItem.title)}</menu-trigger-title>}
</MenuTrigger>
);
});
@@ -0,0 +1,111 @@
/*
* CloudBeaver - Cloud Database Manager
* Copyright (C) 2020-2021 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 { ENodeFeature, getNodeName, NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app';
import type { ITableState } from '@cloudbeaver/core-blocks';
import { injectable } from '@cloudbeaver/core-di';
import {
ContextMenuService, IMenuContext, IContextMenuItem, IMenuItem,
CommonDialogService, ConfirmationDialog, DialogueStateResult
} from '@cloudbeaver/core-dialogs';
import { NotificationService } from '@cloudbeaver/core-events';
import { resourceKeyList } from '@cloudbeaver/core-sdk';
interface IObjectPropertyTableFooterContext {
nodeIds: string[];
tableState: ITableState;
}
@injectable()
export class ObjectPropertyTableFooterService {
static objectPropertyContextType = 'objectProperty';
private objectPropertyTableFooterToken = 'objectPropertyTableFooter';
constructor(
private readonly contextMenuService: ContextMenuService,
private readonly navTreeResource: NavTreeResource,
private readonly navNodeInfoResource: NavNodeInfoResource,
private readonly notificationService: NotificationService,
private readonly commonDialogService: CommonDialogService,
) {
this.contextMenuService.addPanel(this.objectPropertyTableFooterToken);
this.registerMenuItem({
id: 'delete',
title: 'ui_delete',
tooltip: 'ui_delete',
icon: 'delete',
order: 0,
isPresent(context) {
return context.contextType === ObjectPropertyTableFooterService.objectPropertyContextType;
},
isDisabled: context => {
if (context.data.tableState.selectedList.length === 0) {
return true;
}
const selectedNodes = this.getSelectedNodes(context.data.tableState.selectedList);
return !selectedNodes.some(node => node.features?.includes(ENodeFeature.canDelete));
},
onClick: async context => {
const nodes = this.getSelectedNodes(context.data.tableState.selectedList)
.filter(node => node.features?.includes(ENodeFeature.canDelete));
const nodeNames = nodes.map(getNodeName);
const result = await this.commonDialogService.open(ConfirmationDialog, {
title: 'ui_data_delete_confirmation',
message: `You're going to delete following items: "${nodeNames.join(', ')}". Are you sure?`,
icon: '/icons/error_icon.svg',
bigIcon: true,
confirmActionText: 'ui_delete',
});
if (result === DialogueStateResult.Rejected) {
return;
}
const deleted: string[] = [];
try {
for (const node of nodes) {
await this.navTreeResource.deleteNode(node.id);
deleted.push(node.id);
}
} catch (exception) {
this.notificationService.logException(exception, 'Failed to delete item');
}
if (deleted.length) {
context.data.tableState.unselect(deleted);
}
},
});
}
registerMenuItem(options: IContextMenuItem<IObjectPropertyTableFooterContext>): void {
this.contextMenuService.addMenuItem<IObjectPropertyTableFooterContext>(
this.objectPropertyTableFooterToken, options
);
}
constructMenuWithContext(nodeIds: string[], tableState: ITableState): IMenuItem[] {
const context: IMenuContext<IObjectPropertyTableFooterContext> = {
menuId: this.objectPropertyTableFooterToken,
contextType: ObjectPropertyTableFooterService.objectPropertyContextType,
data: {
nodeIds,
tableState,
},
};
return this.contextMenuService.createContextMenu(context, this.objectPropertyTableFooterToken).menuItems;
}
private getSelectedNodes(list: string[]) {
return this.navNodeInfoResource.get(resourceKeyList(list)).filter(Boolean) as NavNode[];
}
}
@@ -13,6 +13,7 @@ import { DBObjectPageService } from './ObjectPage/DBObjectPageService';
import { NavNodeMetadataViewBootstrap } from './ObjectPropertiesPage/NavNodeView/NavNodeMetadata/NavNodeMetadataViewBootstrap';
import { VirtualFolderViewBootstrap } from './ObjectPropertiesPage/NavNodeView/VirtualFolder/VirtualFolderViewBootstrap';
import { ObjectPropertiesPageService } from './ObjectPropertiesPage/ObjectPropertiesPageService';
import { ObjectPropertyTableFooterService } from './ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService';
import { ObjectViewerBootstrap } from './ObjectViewerBootstrap';
import { ObjectViewerTabService } from './ObjectViewerTabService';
@@ -27,5 +28,6 @@ export const manifest: PluginManifest = {
ObjectViewerTabService,
DBObjectPageService,
LocaleService,
ObjectPropertyTableFooterService,
],
};
@@ -11,12 +11,12 @@ import {
NavNodeContextMenuService,
EObjectFeature,
NodeManagerUtils,
NavNode, ConnectionSchemaManagerService,
isObjectCatalogProvider, isObjectSchemaProvider
ConnectionSchemaManagerService,
isObjectCatalogProvider, isObjectSchemaProvider, INodeMenuData
} from '@cloudbeaver/core-app';
import { isConnectionProvider } from '@cloudbeaver/core-connections';
import { Bootstrap, injectable } from '@cloudbeaver/core-di';
import { ContextMenuService, IMenuContext } from '@cloudbeaver/core-dialogs';
import { ContextMenuService } from '@cloudbeaver/core-dialogs';
import { ExtensionUtils } from '@cloudbeaver/core-extensions';
import { ActiveViewService } from '@cloudbeaver/core-view';
@@ -44,23 +44,23 @@ export class SqlEditorBootstrap extends Bootstrap {
isDisabled: () => this.isSQLEntryDisabled(),
}
);
this.contextMenuService.addMenuItem<NavNode>(this.contextMenuService.getRootMenuToken(), {
this.contextMenuService.addMenuItem<INodeMenuData>(this.contextMenuService.getRootMenuToken(), {
id: 'open-sql-editor',
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.objectFeatures.includes(EObjectFeature.dataSource);
},
title: 'SQL',
order: 2,
onClick: (context: IMenuContext<NavNode>) => {
const node = context.data;
isPresent(context) {
return context.contextType === NavNodeContextMenuService.nodeContextType
&& context.data.node.objectFeatures.includes(EObjectFeature.dataSource);
},
onClick: context => {
const node = context.data.node;
const connectionId = NodeManagerUtils.connectionNodeIdToConnectionId(node.id);
this.sqlEditorNavigatorService.openNewEditor(connectionId);
},
});
}
load(): void {}
load(): void { }
private isSQLEntryDisabled() {
const activeView = this.activeViewService.view;