From 27fef71b7cf1d0cd2ca0dc11ae654324f7b0f5c9 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 9 Sep 2021 12:29:30 +0300 Subject: [PATCH 1/4] feat(plugin-object-viewer): CB-1216, CB-1217 rename/delete from navigator and metadata editor --- .../packages/core-app/public/icons/icons.svg | 5 + .../NavigationNode/NavigationNodeControl.tsx | 28 ++- .../NavigationNode/NavigationNodeEditor.tsx | 61 +++++++ .../TreeNodeMenu/TreeNodeMenu.tsx | 5 +- webapp/packages/core-app/src/locales/en.ts | 1 + webapp/packages/core-app/src/locales/ru.ts | 1 + .../src/shared/InlineEditor/InlineEditor.tsx | 40 ++++- .../src/shared/InlineEditor/styles.ts | 17 +- .../NodesManager/ConnectionDialogsService.ts | 43 +++-- .../src/shared/NodesManager/ENodeFeature.ts | 2 + .../NodesManager/NavNodeContextMenuService.ts | 166 ++++++++++++++---- .../NodesManager/NavNodeInfoResource.ts | 9 + .../NodesManager/NavNodeManagerService.ts | 9 +- .../shared/NodesManager/NavTreeResource.ts | 16 ++ .../core-blocks/src/Table/TableItemSelect.tsx | 4 +- .../src/Tree/TreeNode/TreeNodeStyles.ts | 7 +- .../src/CommonDialog/RenameDialog.tsx | 97 ++++++++++ webapp/packages/core-dialogs/src/index.ts | 1 + .../core-localization/src/locales/en.ts | 4 + .../core-localization/src/locales/ru.ts | 4 + .../navigation-tree/navDeleteNodes.gql | 3 + .../queries/navigation-tree/navRenameNode.gql | 3 + webapp/packages/core-sdk/src/sdk.ts | 40 +++++ .../PublicConnectionFormBootstrap.ts | 9 +- .../src/DataExportMenuService.ts | 8 +- .../plugin-object-viewer/src/INodeActions.ts | 12 ++ .../ObjectPropertyTable/Item.tsx | 22 ++- .../ObjectChildrenPropertyTable.tsx | 58 ++++-- .../ObjectPropertyTable.tsx | 1 + .../ObjectPropertyTableFooter.tsx | 33 ++++ .../ObjectPropertyTableFooterItem.tsx | 99 +++++++++++ .../ObjectPropertyTableFooterService.ts | 121 +++++++++++++ .../plugin-object-viewer/src/index.ts | 1 + .../plugin-object-viewer/src/manifest.ts | 2 + .../src/SqlEditorBootstrap.ts | 22 +-- 35 files changed, 826 insertions(+), 128 deletions(-) create mode 100644 webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx create mode 100644 webapp/packages/core-dialogs/src/CommonDialog/RenameDialog.tsx create mode 100644 webapp/packages/core-sdk/src/queries/navigation-tree/navDeleteNodes.gql create mode 100644 webapp/packages/core-sdk/src/queries/navigation-tree/navRenameNode.gql create mode 100644 webapp/packages/plugin-object-viewer/src/INodeActions.ts create mode 100644 webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx create mode 100644 webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx create mode 100644 webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts 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..cccdca51e5 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 { INodeActions } from '@cloudbeaver/plugin-object-viewer'; import type { NavNode } from '../../../shared/NodesManager/EntityTypes'; import { EObjectFeature } from '../../../shared/NodesManager/EObjectFeature'; 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..cb965158e1 --- /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 { NavNodeManagerService } from '../../../shared/NodesManager/NavNodeManagerService'; + +interface Props { + node: NavNode; + onClose: () => void; +} + +export const NavigationNodeEditor = observer(function NavigationNodeEditor({ node, onClose }) { + const navNodeManagerService = useService(NavNodeManagerService); + 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 navNodeManagerService.changeName(name, node); + } + } catch (exception) { + notificationService.logException(exception, 'app_navigationTree_node_change_name_error'); + } finally { + setLoading(false); + onClose(); + } + }, [name, onClose, node, loading, navNodeManagerService, 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..c2ce2a556b 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx @@ -14,6 +14,7 @@ import styled, { use } from 'reshadow'; import { Icon } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuTrigger } from '@cloudbeaver/core-dialogs'; +import type { INodeActions } from '@cloudbeaver/plugin-object-viewer'; import type { NavNode } from '../../../shared/NodesManager/EntityTypes'; import { NavNodeContextMenuService } from '../../../shared/NodesManager/NavNodeContextMenuService'; @@ -21,17 +22,19 @@ 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/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..ea6c26dbc0 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,22 @@ 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); + 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 +63,22 @@ 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); + 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/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts index 1776d65a2c..62166b4a15 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts @@ -6,17 +6,26 @@ * 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 type { INodeActions } from '@cloudbeaver/plugin-object-viewer'; +import { ENodeFeature } from './ENodeFeature'; import type { NavNode } from './EntityTypes'; import { EObjectFeature } from './EObjectFeature'; 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 +33,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 +51,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 +88,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 +166,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 +202,80 @@ 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.navNodeManagerService.changeName(result, node); + + 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 = `${node.nodeType || 'Object'}${node.name ? ' (' + node.name + ')' : ''}`; + + 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); + this.notificationService.logSuccess({ title: 'Object was deleted', message: nodeName }); + } 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..0f6ebf1686 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts @@ -82,6 +82,15 @@ export class NavNodeInfoResource extends CachedMapResource { }; } + async changeName(name: string, node: NavNodeInfo): Promise { + await this.performUpdate(node.id, [], async () => { + await this.graphQLService.sdk.navRenameNode({ + nodePath: node.id, + newName: name, + }); + }); + } + private async loadNodeInfo(nodePath: string): Promise { const metadata = this.metadata.get(nodePath); const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({ diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts index e0a826b59a..447134ff83 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({ @@ -312,10 +312,15 @@ export class NavNodeManagerService extends Bootstrap { }; }; + async changeName(name: string, node: NavNode): Promise { + await this.navNodeInfoResource.changeName(name, node); + await this.refreshTree(node.parentId); + } + 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..7faaf525b9 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -110,6 +110,22 @@ 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 }); + }); + + for (const path of nodePaths) { + const node = this.navNodeInfoResource.get(path); + + if (node) { + this.deleteInNode(node.parentId, [path]); + } + } + } + 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/src/Table/TableItemSelect.tsx b/webapp/packages/core-blocks/src/Table/TableItemSelect.tsx index 63af7915eb..ebd5b637d8 100644 --- a/webapp/packages/core-blocks/src/Table/TableItemSelect.tsx +++ b/webapp/packages/core-blocks/src/Table/TableItemSelect.tsx @@ -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(function TableItemSelect({ checked, disabled, className }) { +export const TableItemSelect = observer(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(function TableItemSelect({ checke return styled(styles, checkboxStyles)( * { margin-right: 4px; margin-left: 4px; @@ -136,5 +140,4 @@ export const TREE_NODE_STYLES = composes( node TreeNodeControl[big] + TreeNodeNested { padding-left: 46px; } - ` -); +`); diff --git a/webapp/packages/core-dialogs/src/CommonDialog/RenameDialog.tsx b/webapp/packages/core-dialogs/src/CommonDialog/RenameDialog.tsx new file mode 100644 index 0000000000..16c867d77b --- /dev/null +++ b/webapp/packages/core-dialogs/src/CommonDialog/RenameDialog.tsx @@ -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 = function RenameDialog({ + payload, + resolveDialog, + rejectDialog, + className, +}) { + const translate = useTranslate(); + const [focusedRef] = useFocus({ 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))( + + + + + + )} + fixedWidth + onReject={rejectDialog} + > + resolveDialog(name)}> + + setName(String(value))} + > + {translate('ui_name') + ':'} + + + + + ); +}; diff --git a/webapp/packages/core-dialogs/src/index.ts b/webapp/packages/core-dialogs/src/index.ts index 57cc7b0add..4ae83c1656 100644 --- a/webapp/packages/core-dialogs/src/index.ts +++ b/webapp/packages/core-dialogs/src/index.ts @@ -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'; diff --git a/webapp/packages/core-localization/src/locales/en.ts b/webapp/packages/core-localization/src/locales/en.ts index 65076d62c8..6f5ce6bf7a 100644 --- a/webapp/packages/core-localization/src/locales/en.ts +++ b/webapp/packages/core-localization/src/locales/en.ts @@ -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'], diff --git a/webapp/packages/core-localization/src/locales/ru.ts b/webapp/packages/core-localization/src/locales/ru.ts index f99f218444..2690edb7a0 100644 --- a/webapp/packages/core-localization/src/locales/ru.ts +++ b/webapp/packages/core-localization/src/locales/ru.ts @@ -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', 'Ваша сессия скоро закончится'], diff --git a/webapp/packages/core-sdk/src/queries/navigation-tree/navDeleteNodes.gql b/webapp/packages/core-sdk/src/queries/navigation-tree/navDeleteNodes.gql new file mode 100644 index 0000000000..a1f696f4ba --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/navigation-tree/navDeleteNodes.gql @@ -0,0 +1,3 @@ +mutation navDeleteNodes($nodePaths: [ID!]!) { + navDeleteNodes(nodePaths: $nodePaths) +} diff --git a/webapp/packages/core-sdk/src/queries/navigation-tree/navRenameNode.gql b/webapp/packages/core-sdk/src/queries/navigation-tree/navRenameNode.gql new file mode 100644 index 0000000000..3437c67790 --- /dev/null +++ b/webapp/packages/core-sdk/src/queries/navigation-tree/navRenameNode.gql @@ -0,0 +1,3 @@ +mutation navRenameNode($nodePath: ID!, $newName: String!) { + navRenameNode(nodePath: $nodePath, newName: $newName) +} diff --git a/webapp/packages/core-sdk/src/sdk.ts b/webapp/packages/core-sdk/src/sdk.ts index 9ecb2c3b22..c3e7182260 100644 --- a/webapp/packages/core-sdk/src/sdk.ts +++ b/webapp/packages/core-sdk/src/sdk.ts @@ -297,6 +297,8 @@ export interface Mutation { createConnectionFromTemplate: ConnectionInfo; deleteConnection: Scalars['Boolean']; initConnection: ConnectionInfo; + navDeleteNodes?: Maybe; + navRenameNode?: Maybe; /** @deprecated Field no longer supported */ openConnection: ConnectionInfo; openSession: SessionInfo; @@ -391,6 +393,15 @@ export interface MutationInitConnectionArgs { saveCredentials?: Maybe; } +export interface MutationNavDeleteNodesArgs { + nodePaths: Array; +} + +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']; +}>; + +export type NavDeleteNodesMutation = Pick; + export type NavNodeChildrenQueryVariables = Exact<{ parentPath: Scalars['ID']; withDetails: Scalars['Boolean']; @@ -1803,6 +1820,13 @@ export type NavRefreshNodeQueryVariables = Exact<{ export type NavRefreshNodeQuery = Pick; +export type NavRenameNodeMutationVariables = Exact<{ + nodePath: Scalars['ID']; + newName: Scalars['String']; +}>; + +export type NavRenameNodeMutation = Pick; + 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 { return withWrapper(() => client.request(GetDbObjectInfoDocument, variables)); }, + navDeleteNodes(variables: NavDeleteNodesMutationVariables): Promise { + return withWrapper(() => client.request(NavDeleteNodesDocument, variables)); + }, navNodeChildren(variables: NavNodeChildrenQueryVariables): Promise { return withWrapper(() => client.request(NavNodeChildrenDocument, variables)); }, @@ -3276,6 +3313,9 @@ export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = navRefreshNode(variables: NavRefreshNodeQueryVariables): Promise { return withWrapper(() => client.request(NavRefreshNodeDocument, variables)); }, + navRenameNode(variables: NavRenameNodeMutationVariables): Promise { + return withWrapper(() => client.request(NavRenameNodeDocument, variables)); + }, querySqlCompletionProposals(variables: QuerySqlCompletionProposalsQueryVariables): Promise { return withWrapper(() => client.request(QuerySqlCompletionProposalsDocument, variables)); }, diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts index de2cf60f2e..7945de6808 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts @@ -10,7 +10,8 @@ import { NavNodeContextMenuService, EObjectFeature, NodeManagerUtils, - NavNode + NavNode, + INodeMenuData } from '@cloudbeaver/core-app'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { ContextMenuService } from '@cloudbeaver/core-dialogs'; @@ -27,16 +28,16 @@ export class PublicConnectionFormBootstrap extends Bootstrap { } register(): void | Promise { - this.contextMenuService.addMenuItem(this.contextMenuService.getRootMenuToken(), { + this.contextMenuService.addMenuItem(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 }); }, diff --git a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts index 8093ec7b36..06cbda9f2f 100644 --- a/webapp/packages/plugin-data-export/src/DataExportMenuService.ts +++ b/webapp/packages/plugin-data-export/src/DataExportMenuService.ts @@ -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( + this.contextMenuService.addMenuItem( 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, diff --git a/webapp/packages/plugin-object-viewer/src/INodeActions.ts b/webapp/packages/plugin-object-viewer/src/INodeActions.ts new file mode 100644 index 0000000000..2e805b1eab --- /dev/null +++ b/webapp/packages/plugin-object-viewer/src/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/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx index 6cc3ec65e8..9fd67705ba 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx @@ -10,12 +10,13 @@ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; import styled, { css, use } from 'reshadow'; -import { NavNode, NavNodeContextMenuService, NavNodeManagerService, useDatabaseObjectInfo, useNode } from '@cloudbeaver/core-app'; +import { ENodeFeature, NavNode, NavNodeContextMenuService, NavNodeManagerService, useDatabaseObjectInfo, useNode } from '@cloudbeaver/core-app'; import { StaticImage, TableItem, TableColumnValue, TableItemSelect, useMouse, getComputed, Icon, useStateDelay } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuTrigger } from '@cloudbeaver/core-dialogs'; +import { useTranslate } from '@cloudbeaver/core-localization'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -76,14 +77,21 @@ interface Props { export const Item = observer(function Item({ objectId, columns, }) { + const translate = useTranslate(); + const styles = useStyles(itemStyles); + const { node } = useNode(objectId); const { dbObject } = useDatabaseObjectInfo(objectId); - const styles = useStyles(itemStyles); + + const deletable = node?.features?.includes(ENodeFeature.canDelete); + const tooltip = deletable ? undefined : translate('ui_cant_delete_item'); if (!node) { return styled(styles)( - + + + @@ -97,7 +105,9 @@ export const Item = observer(function Item({ if (!dbObject?.properties || dbObject.properties.length === 0) { return styled(styles)( - + + + @@ -110,7 +120,9 @@ export const Item = observer(function Item({ return styled(styles)( - + + + diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx index 8152439d9b..9ce43ff1c5 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectChildrenPropertyTable.tsx @@ -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(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))( - - -
- - - {nodeIds.map(id => ( - - ))} - -
+ + + + +
+ + + {nodeIds.map(id => ( + + ))} + +
+
+ +
); }); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx index 6140c3306c..b8f0aade55 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTable.tsx @@ -20,6 +20,7 @@ import { ObjectChildrenPropertyTable } from './ObjectChildrenPropertyTable'; const styles = css` div { flex: auto; + overflow: hidden; } `; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx new file mode 100644 index 0000000000..0720a85025 --- /dev/null +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooter.tsx @@ -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(function ObjectPropertyTableFooter({ nodeIds, tableState, className }) { + const service = useService(ObjectPropertyTableFooterService); + + return ( + + {service.constructMenuWithContext(nodeIds, tableState).map((topItem, i) => ( + + ))} + + ); +}); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx new file mode 100644 index 0000000000..8836cef5da --- /dev/null +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterItem.tsx @@ -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 & { + 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(function ObjectPropertyTableFooterItem({ + menuItem, + ...props +}) { + const translate = useTranslate(); + const styles = useStyles(style); + + if (!menuItem.panel) { + return styled(styles)( + menuItem.onClick?.()} + > + {translate(menuItem.title)} + + ); + } + + return styled(styles)( + + {menuItem.icon && ( + + + + )} + {menuItem.title && {translate(menuItem.title)}} + + ); +}); diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts new file mode 100644 index 0000000000..56fb4bbdba --- /dev/null +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -0,0 +1,121 @@ +/* + * 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 { DBObjectService, 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 { getValue } from '../helpers'; + +const NAME_PROPERTY_ID = 'name'; + +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, + private readonly dbObjectService: DBObjectService, + ) { + 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) { + return context.data.tableState.selectedList.length === 0; + }, + onClick: async context => { + const nodeNames = this.getNodeNames(context.data.tableState.selectedList); + + 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_sm.svg', + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + return; + } + + const deleted: string[] = []; + + try { + for (const path of context.data.tableState.selectedList) { + await this.navTreeResource.deleteNode(path); + deleted.push(path); + } + } catch (exception) { + this.notificationService.logException(exception, 'Failed to delete item'); + } + + if (deleted.length) { + context.data.tableState.unselect(deleted); + const title = deleted.length > 1 ? 'Items were deleted' : 'Item was deleted'; + this.notificationService.logSuccess({ title }); + } + }, + }); + } + + registerMenuItem(options: IContextMenuItem): void { + this.contextMenuService.addMenuItem( + this.objectPropertyTableFooterToken, options + ); + } + + constructMenuWithContext(nodeIds: string[], tableState: ITableState): IMenuItem[] { + const context: IMenuContext = { + menuId: this.objectPropertyTableFooterToken, + contextType: ObjectPropertyTableFooterService.objectPropertyContextType, + data: { + nodeIds, + tableState, + }, + }; + return this.contextMenuService.createContextMenu(context, this.objectPropertyTableFooterToken).menuItems; + } + + private getNodeNames(selected: string[]) { + return selected.reduce((acc: string[], path) => { + const node = this.navNodeInfoResource.get(path); + const dbObject = this.dbObjectService.get(path); + + const nameProperty = dbObject?.properties?.find(property => property.id === NAME_PROPERTY_ID); + + if (node) { + const name = nameProperty ? getValue(nameProperty.value) : node.name; + acc.push(`${node.nodeType || 'Object'}${name ? ' (' + name + ')' : ''}`); + } + + return acc; + }, []); + } +} diff --git a/webapp/packages/plugin-object-viewer/src/index.ts b/webapp/packages/plugin-object-viewer/src/index.ts index 4543e9a376..71dcbb2517 100644 --- a/webapp/packages/plugin-object-viewer/src/index.ts +++ b/webapp/packages/plugin-object-viewer/src/index.ts @@ -7,3 +7,4 @@ export * from './ObjectPage/DBObjectPageService'; export * from './ObjectPage/ObjectPage'; export * from './ObjectViewerTabService'; export * from './IObjectViewerTabState'; +export * from './INodeActions'; diff --git a/webapp/packages/plugin-object-viewer/src/manifest.ts b/webapp/packages/plugin-object-viewer/src/manifest.ts index 6cb56a799d..766be5500f 100644 --- a/webapp/packages/plugin-object-viewer/src/manifest.ts +++ b/webapp/packages/plugin-object-viewer/src/manifest.ts @@ -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, ], }; diff --git a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts index f716e87739..173ad438ce 100644 --- a/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts +++ b/webapp/packages/plugin-sql-editor/src/SqlEditorBootstrap.ts @@ -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(this.contextMenuService.getRootMenuToken(), { + this.contextMenuService.addMenuItem(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) => { - 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; From 4b6dde0c162d825a13f689b88b30456d54b1c66b Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 9 Sep 2021 16:41:58 +0300 Subject: [PATCH 2/4] refactor(plugin-object-viewer): CB-1216, CB-1217 move changeName to NavTreeResource --- .../NavigationNode/NavigationNodeEditor.tsx | 8 ++--- .../NodesManager/NavNodeContextMenuService.ts | 6 ++-- .../NodesManager/NavNodeInfoResource.ts | 13 +++---- .../NodesManager/NavNodeManagerService.ts | 5 --- .../shared/NodesManager/NavTreeResource.ts | 26 ++++++++++---- .../PublicConnectionFormBootstrap.ts | 1 - .../ObjectPropertyTable/Item.tsx | 13 +++---- .../ObjectPropertyTableFooterService.ts | 36 +++++++------------ 8 files changed, 48 insertions(+), 60 deletions(-) diff --git a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx index cb965158e1..87a626bca8 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeEditor.tsx @@ -14,7 +14,7 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { InlineEditor } from '../../../shared/InlineEditor/InlineEditor'; import type { NavNode } from '../../../shared/NodesManager/EntityTypes'; -import { NavNodeManagerService } from '../../../shared/NodesManager/NavNodeManagerService'; +import { NavTreeResource } from '../../../shared/NodesManager/NavTreeResource'; interface Props { node: NavNode; @@ -22,7 +22,7 @@ interface Props { } export const NavigationNodeEditor = observer(function NavigationNodeEditor({ node, onClose }) { - const navNodeManagerService = useService(NavNodeManagerService); + const navTreeResource = useService(NavTreeResource); const notificationService = useService(NotificationService); const [loading, setLoading] = useState(false); @@ -36,7 +36,7 @@ export const NavigationNodeEditor = observer(function NavigationNodeEdito try { if (node.name !== name) { setLoading(true); - await navNodeManagerService.changeName(name, node); + await navTreeResource.changeName(node, name); } } catch (exception) { notificationService.logException(exception, 'app_navigationTree_node_change_name_error'); @@ -44,7 +44,7 @@ export const NavigationNodeEditor = observer(function NavigationNodeEdito setLoading(false); onClose(); } - }, [name, onClose, node, loading, navNodeManagerService, notificationService]); + }, [name, onClose, node, loading, navTreeResource, notificationService]); return ( ProcessSnackbar, {}, { title: 'ui_rename_processing' }); try { - await this.navNodeManagerService.changeName(result, node); + await this.navTreeResource.changeName(node, result); const message = `prev: ${name}\nnew: ${result}`; notification.controller.resolve(`${node.nodeType} was renamed`, message); @@ -252,7 +253,7 @@ export class NavNodeContextMenuService extends Bootstrap { || context.data.node.objectFeatures.includes(EObjectFeature.dataSource), onClick: async context => { const node = context.data.node; - const nodeName = `${node.nodeType || 'Object'}${node.name ? ' (' + node.name + ')' : ''}`; + const nodeName = getNodeName(node); const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'ui_data_delete_confirmation', @@ -268,7 +269,6 @@ export class NavNodeContextMenuService extends Bootstrap { try { await this.navTreeResource.deleteNode(node.id); - this.notificationService.logSuccess({ title: 'Object was deleted', message: nodeName }); } catch (exception) { this.notificationService.logException(exception, `Failed to delete "${nodeName}"`); } diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts index 0f6ebf1686..61d192e856 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeInfoResource.ts @@ -82,15 +82,6 @@ export class NavNodeInfoResource extends CachedMapResource { }; } - async changeName(name: string, node: NavNodeInfo): Promise { - await this.performUpdate(node.id, [], async () => { - await this.graphQLService.sdk.navRenameNode({ - nodePath: node.id, - newName: name, - }); - }); - } - private async loadNodeInfo(nodePath: string): Promise { const metadata = this.metadata.get(nodePath); const { navNodeInfo } = await this.graphQLService.sdk.navNodeInfo({ @@ -101,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 447134ff83..fd1bdc990b 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeManagerService.ts @@ -312,11 +312,6 @@ export class NavNodeManagerService extends Bootstrap { }; }; - async changeName(name: string, node: NavNode): Promise { - await this.navNodeInfoResource.changeName(name, node); - await this.refreshTree(node.parentId); - } - private async navigateHandler( data: INodeNavigationData, contexts: IExecutionContextProvider diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 7faaf525b9..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(); @@ -117,13 +118,26 @@ export class NavTreeResource extends CachedMapResource { await this.graphQLService.sdk.navDeleteNodes({ nodePaths }); }); - for (const path of nodePaths) { - const node = this.navNodeInfoResource.get(path); + runInAction(() => { + for (const path of nodePaths) { + const node = this.navNodeInfoResource.get(path); - if (node) { - this.deleteInNode(node.parentId, [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; diff --git a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts index 7945de6808..78183e2a80 100644 --- a/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts +++ b/webapp/packages/plugin-connections/src/PublicConnectionForm/PublicConnectionFormBootstrap.ts @@ -10,7 +10,6 @@ import { NavNodeContextMenuService, EObjectFeature, NodeManagerUtils, - NavNode, INodeMenuData } from '@cloudbeaver/core-app'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx index 9fd67705ba..62b881b5c0 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/Item.tsx @@ -10,13 +10,12 @@ import { observer } from 'mobx-react-lite'; import { useState } from 'react'; import styled, { css, use } from 'reshadow'; -import { ENodeFeature, NavNode, NavNodeContextMenuService, NavNodeManagerService, useDatabaseObjectInfo, useNode } from '@cloudbeaver/core-app'; +import { NavNode, NavNodeContextMenuService, NavNodeManagerService, useDatabaseObjectInfo, useNode } from '@cloudbeaver/core-app'; import { StaticImage, TableItem, TableColumnValue, TableItemSelect, useMouse, getComputed, Icon, useStateDelay } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuTrigger } from '@cloudbeaver/core-dialogs'; -import { useTranslate } from '@cloudbeaver/core-localization'; import type { ObjectPropertyInfo } from '@cloudbeaver/core-sdk'; import { useStyles } from '@cloudbeaver/core-theming'; @@ -77,20 +76,16 @@ interface Props { export const Item = observer(function Item({ objectId, columns, }) { - const translate = useTranslate(); const styles = useStyles(itemStyles); const { node } = useNode(objectId); const { dbObject } = useDatabaseObjectInfo(objectId); - const deletable = node?.features?.includes(ENodeFeature.canDelete); - const tooltip = deletable ? undefined : translate('ui_cant_delete_item'); - if (!node) { return styled(styles)( - + @@ -106,7 +101,7 @@ export const Item = observer(function Item({ return styled(styles)( - + @@ -121,7 +116,7 @@ export const Item = observer(function Item({ return styled(styles)( - + diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index 56fb4bbdba..6e275deb09 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -6,7 +6,7 @@ * you may not use this file except in compliance with the License. */ -import { DBObjectService, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app'; +import { ENodeFeature, getNodeName, NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app'; import type { ITableState } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; import { @@ -14,10 +14,7 @@ import { CommonDialogService, ConfirmationDialog, DialogueStateResult } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; - -import { getValue } from '../helpers'; - -const NAME_PROPERTY_ID = 'name'; +import { resourceKeyList } from '@cloudbeaver/core-sdk'; interface IObjectPropertyTableFooterContext { nodeIds: string[]; @@ -35,7 +32,6 @@ export class ObjectPropertyTableFooterService { private readonly navNodeInfoResource: NavNodeInfoResource, private readonly notificationService: NotificationService, private readonly commonDialogService: CommonDialogService, - private readonly dbObjectService: DBObjectService, ) { this.contextMenuService.addPanel(this.objectPropertyTableFooterToken); @@ -48,11 +44,17 @@ export class ObjectPropertyTableFooterService { isPresent(context) { return context.contextType === ObjectPropertyTableFooterService.objectPropertyContextType; }, - isDisabled(context) { - return context.data.tableState.selectedList.length === 0; + 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 nodeNames = this.getNodeNames(context.data.tableState.selectedList); + const nodes = this.getSelectedNodes(context.data.tableState.selectedList); + const nodeNames = nodes.map(getNodeName); const result = await this.commonDialogService.open(ConfirmationDialog, { title: 'ui_data_delete_confirmation', @@ -103,19 +105,7 @@ export class ObjectPropertyTableFooterService { return this.contextMenuService.createContextMenu(context, this.objectPropertyTableFooterToken).menuItems; } - private getNodeNames(selected: string[]) { - return selected.reduce((acc: string[], path) => { - const node = this.navNodeInfoResource.get(path); - const dbObject = this.dbObjectService.get(path); - - const nameProperty = dbObject?.properties?.find(property => property.id === NAME_PROPERTY_ID); - - if (node) { - const name = nameProperty ? getValue(nameProperty.value) : node.name; - acc.push(`${node.nodeType || 'Object'}${name ? ' (' + name + ')' : ''}`); - } - - return acc; - }, []); + private getSelectedNodes(list: string[]) { + return this.navNodeInfoResource.get(resourceKeyList(list)).filter(Boolean) as NavNode[]; } } From f08b223925fa5d18989759fa11a5a1ae23bde383 Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 9 Sep 2021 17:18:43 +0300 Subject: [PATCH 3/4] refactor(plugin-object-viewer): CB-1216, CB-1217 filter nodes --- .../NodesManager/ConnectionDialogsService.ts | 10 ++++---- .../ObjectPropertyTableFooterService.ts | 23 ++++++++----------- 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts index ea6c26dbc0..b897c43bbe 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/ConnectionDialogsService.ts @@ -44,8 +44,8 @@ export class ConnectionDialogsService extends Bootstrap { this.contextMenuService.getRootMenuToken(), { id: 'closeConnection', - isPresent: - context => context.contextType === NavNodeContextMenuService.nodeContextType, + 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); @@ -53,7 +53,6 @@ export class ConnectionDialogsService extends Bootstrap { return !context.data.node.objectFeatures.includes(EObjectFeature.dataSource) || !connection?.connected; }, - title: 'app_navigationTree_context_disconnect', onClick: context => { const node = context.data.node; this.connectionsManagerService.closeConnectionAsync( @@ -67,8 +66,8 @@ export class ConnectionDialogsService extends Bootstrap { this.contextMenuService.getRootMenuToken(), { id: 'deleteConnection', - isPresent: - context => context.contextType === NavNodeContextMenuService.nodeContextType, + 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); @@ -76,7 +75,6 @@ export class ConnectionDialogsService extends Bootstrap { return !context.data.node.objectFeatures.includes(EObjectFeature.dataSource) || !connection?.features.includes(EConnectionFeature.manageable); }, - title: 'ui_delete', onClick: async context => { const node = context.data.node; try { diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index 6e275deb09..3bf7afdf66 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -6,6 +6,8 @@ * you may not use this file except in compliance with the License. */ +import { runInAction } from 'mobx'; + import { ENodeFeature, getNodeName, NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app'; import type { ITableState } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; @@ -53,7 +55,8 @@ export class ObjectPropertyTableFooterService { return !selectedNodes.some(node => node.features?.includes(ENodeFeature.canDelete)); }, onClick: async context => { - const nodes = this.getSelectedNodes(context.data.tableState.selectedList); + 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, { @@ -67,22 +70,16 @@ export class ObjectPropertyTableFooterService { return; } - const deleted: string[] = []; - try { - for (const path of context.data.tableState.selectedList) { - await this.navTreeResource.deleteNode(path); - deleted.push(path); - } + await runInAction(async () => { + for (const node of nodes) { + await this.navTreeResource.deleteNode(node.id); + context.data.tableState.unselect(node.id); + } + }); } catch (exception) { this.notificationService.logException(exception, 'Failed to delete item'); } - - if (deleted.length) { - context.data.tableState.unselect(deleted); - const title = deleted.length > 1 ? 'Items were deleted' : 'Item was deleted'; - this.notificationService.logSuccess({ title }); - } }, }); } From 56d7758e6b6dabf40ae3e640d2d7aa617592d66b Mon Sep 17 00:00:00 2001 From: Naumov Alexey Date: Thu, 9 Sep 2021 19:00:41 +0300 Subject: [PATCH 4/4] refactor(plugin-object-viewer): CB-1216, CB-1217 change error_icon --- .../NavigationNode/NavigationNodeControl.tsx | 2 +- .../TreeNodeMenu/TreeNodeMenu.tsx | 2 +- webapp/packages/core-app/src/index.ts | 1 + .../src/shared/NodesManager}/INodeActions.ts | 0 .../NodesManager/NavNodeContextMenuService.ts | 2 +- .../core-blocks/public/icons/error_icon.svg | 6 +++--- .../ObjectPropertyTableFooterService.ts | 21 +++++++++++-------- .../plugin-object-viewer/src/index.ts | 1 - 8 files changed, 19 insertions(+), 16 deletions(-) rename webapp/packages/{plugin-object-viewer/src => core-app/src/shared/NodesManager}/INodeActions.ts (100%) 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 cccdca51e5..33d4823fd5 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/NavigationNode/NavigationNodeControl.tsx @@ -13,10 +13,10 @@ import styled, { css, use } from 'reshadow'; 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 { INodeActions } from '@cloudbeaver/plugin-object-viewer'; 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'; 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 c2ce2a556b..035636e51e 100644 --- a/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx +++ b/webapp/packages/core-app/src/NavigationTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenu.tsx @@ -14,9 +14,9 @@ import styled, { use } from 'reshadow'; import { Icon } from '@cloudbeaver/core-blocks'; import { useService } from '@cloudbeaver/core-di'; import { MenuTrigger } from '@cloudbeaver/core-dialogs'; -import type { INodeActions } from '@cloudbeaver/plugin-object-viewer'; import type { NavNode } from '../../../shared/NodesManager/EntityTypes'; +import type { INodeActions } from '../../../shared/NodesManager/INodeActions'; import { NavNodeContextMenuService } from '../../../shared/NodesManager/NavNodeContextMenuService'; import { treeNodeMenuStyles } from './treeNodeMenuStyles'; 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/plugin-object-viewer/src/INodeActions.ts b/webapp/packages/core-app/src/shared/NodesManager/INodeActions.ts similarity index 100% rename from webapp/packages/plugin-object-viewer/src/INodeActions.ts rename to webapp/packages/core-app/src/shared/NodesManager/INodeActions.ts diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts index 1f60759180..3964828b37 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts @@ -12,11 +12,11 @@ import { Bootstrap, injectable } from '@cloudbeaver/core-di'; 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 type { INodeActions } from '@cloudbeaver/plugin-object-viewer'; 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'; 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 @@ - - - + + + diff --git a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts index 3bf7afdf66..ca3837d7d7 100644 --- a/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts +++ b/webapp/packages/plugin-object-viewer/src/ObjectPropertiesPage/ObjectPropertyTable/ObjectPropertyTableFooterService.ts @@ -6,8 +6,6 @@ * you may not use this file except in compliance with the License. */ -import { runInAction } from 'mobx'; - import { ENodeFeature, getNodeName, NavNode, NavNodeInfoResource, NavTreeResource } from '@cloudbeaver/core-app'; import type { ITableState } from '@cloudbeaver/core-blocks'; import { injectable } from '@cloudbeaver/core-di'; @@ -62,7 +60,8 @@ export class ObjectPropertyTableFooterService { 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_sm.svg', + icon: '/icons/error_icon.svg', + bigIcon: true, confirmActionText: 'ui_delete', }); @@ -70,16 +69,20 @@ export class ObjectPropertyTableFooterService { return; } + const deleted: string[] = []; + try { - await runInAction(async () => { - for (const node of nodes) { - await this.navTreeResource.deleteNode(node.id); - context.data.tableState.unselect(node.id); - } - }); + 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); + } }, }); } diff --git a/webapp/packages/plugin-object-viewer/src/index.ts b/webapp/packages/plugin-object-viewer/src/index.ts index 71dcbb2517..4543e9a376 100644 --- a/webapp/packages/plugin-object-viewer/src/index.ts +++ b/webapp/packages/plugin-object-viewer/src/index.ts @@ -7,4 +7,3 @@ export * from './ObjectPage/DBObjectPageService'; export * from './ObjectPage/ObjectPage'; export * from './ObjectViewerTabService'; export * from './IObjectViewerTabState'; -export * from './INodeActions';