diff --git a/webapp/packages/core-app/public/icons/icons.svg b/webapp/packages/core-app/public/icons/icons.svg
index d43f0cf69b..8bd9e9de6f 100644
--- a/webapp/packages/core-app/public/icons/icons.svg
+++ b/webapp/packages/core-app/public/icons/icons.svg
@@ -294,4 +294,9 @@
+
+
+
+
+
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx
index fce9735aac..33d4823fd5 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx
@@ -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(function NavigationNodeCont
const navNodeInfoResource = useService(NavNodeInfoResource);
const outdated = getComputed(() => navNodeInfoResource.isOutdated(node.id) && !context.loading);
+ const [editing, setEditing] = useState(false);
+
+ const nodeActions = useObjectRef({
+ rename: () => {
+ setEditing(true);
+ },
+ });
+
const connected = node.objectFeatures.includes(EObjectFeature.dataSourceConnected);
const onClickHandler = useCallback((event: React.MouseEvent) => {
@@ -84,15 +94,19 @@ export const NavigationNodeControl = observer(function NavigationNodeCont
}, [context]);
return styled(useStyles(TREE_NODE_STYLES, styles))(
-
+
- {node.name}
-
-
-
+
+ {editing ? setEditing(false)} /> : node.name}
+
+ {!editing && (
+
+
+
+ )}
);
});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx
new file mode 100644
index 0000000000..87a626bca8
--- /dev/null
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx
@@ -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(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 (
+
+ );
+});
diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
index 22d0c87983..035636e51e 100644
--- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
+++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx
@@ -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(function TreeNodeMenu({
node,
+ actions,
selected,
}) {
const navNodeContextMenuService = useService(NavNodeContextMenuService);
const menuPanel = useMemo(
- () => navNodeContextMenuService.constructMenuWithContext(node),
+ () => navNodeContextMenuService.constructMenuWithContext(node, actions),
[node]
);
const isHidden = useMemo(
diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts
index 7f822b44e8..67949fbf65 100644
--- a/webapp/packages/core-app/src/index.ts
+++ b/webapp/packages/core-app/src/index.ts
@@ -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';
diff --git a/webapp/packages/core-app/src/locales/en.ts b/webapp/packages/core-app/src/locales/en.ts
index ab7371bb09..4ea0af3006 100644
--- a/webapp/packages/core-app/src/locales/en.ts
+++ b/webapp/packages/core-app/src/locales/en.ts
@@ -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'],
diff --git a/webapp/packages/core-app/src/locales/ru.ts b/webapp/packages/core-app/src/locales/ru.ts
index 36d39a8b49..b192c2e050 100644
--- a/webapp/packages/core-app/src/locales/ru.ts
+++ b/webapp/packages/core-app/src/locales/ru.ts
@@ -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', 'Общие подключения'],
diff --git a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx
index caff87e6ec..dd917c7c06 100644
--- a/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx
+++ b/webapp/packages/core-app/src/shared/InlineEditor/InlineEditor.tsx
@@ -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 void;
onSave: () => void;
onReject?: () => void;
@@ -48,6 +49,8 @@ export const InlineEditor = observer
edited = false,
autofocus,
active,
+ loading,
+ disabled,
onChange,
onSave,
onUndo,
@@ -107,24 +110,49 @@ export const InlineEditor = observer
lang="en"
value={value}
autoComplete="off"
+ disabled={disabled}
onChange={handleChange}
onKeyDown={handleKeyDown}
{...rest}
/>
e.preventDefault()}>
- {!hideSave && }
- {!hideCancel && onReject && }
+ {!hideSave && (
+
+ {loading ? : }
+
+ )}
+ {!hideCancel && onReject && (
+
+
+
+ )}
{onUndo && (
)}
- {!simple && }
+ {!simple && (
+
+
+
+ )}
);
diff --git a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts
index 27d4f82228..45912845aa 100644
--- a/webapp/packages/core-app/src/shared/InlineEditor/styles.ts
+++ b/webapp/packages/core-app/src/shared/InlineEditor/styles.ts
@@ -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;
}
}
`
diff --git a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts
index 369c7645e9..b897c43bbe 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts
@@ -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(
+ this.contextMenuService.addMenuItem(
this.contextMenuService.getRootMenuToken(),
{
id: 'closeConnection',
- isPresent:
- (context: IMenuContext) => context.contextType === NavNodeContextMenuService.nodeContextType,
- isHidden: (context: IMenuContext) => {
- 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) => {
- 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(
+ this.contextMenuService.addMenuItem(
this.contextMenuService.getRootMenuToken(),
{
id: 'deleteConnection',
- isPresent:
- (context: IMenuContext) => context.contextType === NavNodeContextMenuService.nodeContextType,
- isHidden: (context: IMenuContext) => {
- 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) => {
- const node = context.data;
+ onClick: async context => {
+ const node = context.data.node;
try {
await this.connectionsManagerService.deleteConnection(
NodeManagerUtils.connectionNodeIdToConnectionId(node.id)
diff --git a/webapp/packages/core-app/src/shared/NodesManager/ENodeFeature.ts b/webapp/packages/core-app/src/shared/NodesManager/ENodeFeature.ts
index 13f6977f53..bcb647a694 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/ENodeFeature.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/ENodeFeature.ts
@@ -14,4 +14,6 @@ export enum ENodeFeature {
'dataContainer' = 'dataContainer',
'item' = 'item',
'leaf' = 'leaf',
+ 'canDelete' = 'canDelete',
+ 'canRename' = 'canRename'
}
diff --git a/webapp/packages/core-app/src/shared/NodesManager/INodeActions.ts b/webapp/packages/core-app/src/shared/NodesManager/INodeActions.ts
new file mode 100644
index 0000000000..2e805b1eab
--- /dev/null
+++ b/webapp/packages/core-app/src/shared/NodesManager/INodeActions.ts
@@ -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;
+
+}
diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts
index 1776d65a2c..3964828b37 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts
@@ -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({
+ constructMenuWithContext(node: NavNode, actions?: INodeActions): IMenuPanel {
+ return this.contextMenuService.createContextMenu({
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(
+ registerNodeViewMenuItem(): void {
+ this.contextMenuService.addMenuItem(
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(
+ this.contextMenuService.addMenuItem(
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(
+ this.contextMenuService.addMenuItem(
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(
+ this.contextMenuService.addMenuItem(
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(
+ this.contextMenuService.addMenuItem(
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(
+ this.contextMenuService.addMenuItem(
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(
+ 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(
+ 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();
}
diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts
index cfa8c2960b..61d192e856 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts
@@ -92,3 +92,7 @@ export class NavNodeInfoResource extends CachedMapResource {
return this.navNodeInfoToNavNode(navNodeInfo);
}
}
+
+export function getNodeName(node: NavNode): string {
+ return `${node.nodeType || 'Object'}${node.name ? ' (' + node.name + ')' : ''}`;
+}
diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts
index e0a826b59a..fd1bdc990b 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts
@@ -114,7 +114,7 @@ export class NavNodeManagerService extends Bootstrap {
register(): void { }
- load(): void {}
+ load(): void { }
async navToNode(nodeId: string, parentId: string, folderId?: string): Promise {
await this.navigator.execute({
@@ -315,7 +315,7 @@ export class NavNodeManagerService extends Bootstrap {
private async navigateHandler(
data: INodeNavigationData,
contexts: IExecutionContextProvider
- // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
+ // eslint-disable-next-line @typescript-eslint/no-invalid-void-type
): Promise {
const nodeInfo = await contexts.getContext(this.navigationNavNodeContext);
diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts
index b148304968..baf63c5ee5 100644
--- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts
+++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts
@@ -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 {
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 {
});
}
+ async deleteNode(key: ResourceKey): Promise {
+ 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 {
+ 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, value: string[][]): void;
deleteInNode(keyObject: ResourceKey, valueObject: string[] | string[][]): void {
diff --git a/webapp/packages/core-blocks/public/icons/error_icon.svg b/webapp/packages/core-blocks/public/icons/error_icon.svg
index f1c955fbb7..1f73884696 100644
--- a/webapp/packages/core-blocks/public/icons/error_icon.svg
+++ b/webapp/packages/core-blocks/public/icons/error_icon.svg
@@ -1,4 +1,4 @@
-