From 92c7cef0c63ec2f6ecfd19e50d3a6c1b726d4d26 Mon Sep 17 00:00:00 2001 From: alex <48489896+devnaumov@users.noreply.github.com> Date: Mon, 16 Dec 2024 12:02:43 +0100 Subject: [PATCH 1/2] CB-6039 prevent click through menu (#3134) * CB-6039 prevent click through menu * CB-6039 --- webapp/packages/core-blocks/src/Menu/Menu.tsx | 6 +--- .../core-blocks/src/Menu/MenuPanel.tsx | 9 ++---- .../core-ui/src/ContextMenu/ContextMenu.tsx | 3 +- .../src/ContextMenu/IContextMenuProps.ts | 3 +- .../Formatters/Menu/CellMenu.module.css | 4 +++ .../src/DataGrid/Formatters/Menu/CellMenu.tsx | 29 ++++++++++--------- 6 files changed, 25 insertions(+), 29 deletions(-) diff --git a/webapp/packages/core-blocks/src/Menu/Menu.tsx b/webapp/packages/core-blocks/src/Menu/Menu.tsx index 68f3a0cdad..4bdc61dc9f 100644 --- a/webapp/packages/core-blocks/src/Menu/Menu.tsx +++ b/webapp/packages/core-blocks/src/Menu/Menu.tsx @@ -16,7 +16,7 @@ import { useCombinedRef } from '../useCombinedRef.js'; import { useObjectRef } from '../useObjectRef.js'; import { useS } from '../useS.js'; import style from './Menu.module.css'; -import { type IMenuPanelProps, MenuPanel } from './MenuPanel.js'; +import { MenuPanel } from './MenuPanel.js'; import { type IMenuState, MenuStateContext } from './MenuStateContext.js'; import type { IMouseContextMenu } from './useMouseContextMenu.js'; @@ -33,7 +33,6 @@ interface IMenuProps extends React.ButtonHTMLAttributes { rtl?: boolean; hasBindings?: boolean; panelAvailable?: boolean; - panelProps?: Partial; getHasBindings?: () => boolean; onVisibleSwitch?: (visible: boolean) => void; } @@ -56,7 +55,6 @@ export const Menu = observer( modal, submenu, rtl, - panelProps, className, ...props }, @@ -155,7 +153,6 @@ export const Menu = observer( panelAvailable={panelAvailable} hasBindings={hasBindings} getHasBindings={getHasBindings} - {...panelProps} > {items} @@ -190,7 +187,6 @@ export const Menu = observer( panelAvailable={panelAvailable} hasBindings={hasBindings} getHasBindings={getHasBindings} - {...panelProps} > {items} diff --git a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx index 070bb5f67f..1b854f2fcb 100644 --- a/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx +++ b/webapp/packages/core-blocks/src/Menu/MenuPanel.tsx @@ -17,7 +17,7 @@ import { useS } from '../useS.js'; import { MenuEmptyItem } from './MenuEmptyItem.js'; import style from './MenuPanel.module.css'; -export interface IMenuPanelProps extends Omit, 'children'> { +export interface IMenuPanelProps { label: string; menu: MenuStateReturn; // from reakit useMenuState panelAvailable?: boolean; @@ -26,13 +26,11 @@ export interface IMenuPanelProps extends Omit React.ReactNode); rtl?: boolean; submenu?: boolean; + className?: string; } export const MenuPanel = observer( - forwardRef(function MenuPanel( - { label, menu, submenu, panelAvailable = true, rtl, getHasBindings, hasBindings, children, className, ...rest }, - ref, - ) { + forwardRef(function MenuPanel({ label, menu, submenu, panelAvailable = true, rtl, getHasBindings, hasBindings, children, className }, ref) { const translate = useTranslate(); const styles = useS(style); const visible = menu.visible; @@ -57,7 +55,6 @@ export const MenuPanel = observer( {...menu} aria-label={translate(label)} visible={panelAvailable} - {...rest} >
{Children.count(renderedChildren) === 0 && } diff --git a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx index c83e7a41eb..be50f14c97 100644 --- a/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/ContextMenu.tsx @@ -17,7 +17,7 @@ import { MenuItemRenderer } from './MenuItemRenderer.js'; // TODO the click doesn't work for React components as children export const ContextMenu = observer( forwardRef(function ContextMenu( - { mouseContextMenu, menu: menuData, disclosure, children, placement, visible, onVisibleSwitch, modal, rtl, panelProps, ...props }, + { mouseContextMenu, menu: menuData, disclosure, children, placement, visible, onVisibleSwitch, modal, rtl, ...props }, ref, ) { const translate = useTranslate(); @@ -82,7 +82,6 @@ export const ContextMenu = observer( placement={placement} disabled={disabled} disclosure={disclosure} - panelProps={panelProps} getHasBindings={handlers.hasBindings} onVisibleSwitch={handlers.handleVisibleSwitch} > diff --git a/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts b/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts index 5252c8f587..a6bd3f940d 100644 --- a/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts +++ b/webapp/packages/core-ui/src/ContextMenu/IContextMenuProps.ts @@ -8,7 +8,7 @@ import type { ButtonHTMLAttributes } from 'react'; import type { MenuInitialState } from 'reakit'; -import type { IMenuPanelProps, IMouseContextMenu } from '@cloudbeaver/core-blocks'; +import type { IMouseContextMenu } from '@cloudbeaver/core-blocks'; import type { IMenuData } from '@cloudbeaver/core-view'; export interface IContextMenuBaseProps extends React.PropsWithChildren { @@ -26,7 +26,6 @@ export interface IContextMenuProps extends Omit, 'chil modal?: boolean; visible?: boolean; rtl?: boolean; - panelProps?: Partial; children?: React.ReactNode | ContextMenuRenderingChildren; onVisibleSwitch?: (visible: boolean) => void; } diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css index a64ea14844..dde6e70cd3 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.module.css @@ -6,6 +6,10 @@ * you may not use this file except in compliance with the License. */ +.container { + display: flex; +} + .contextMenu { padding: 0; height: 16px; diff --git a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx index ccd37dce02..733ab9ce43 100644 --- a/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx +++ b/webapp/packages/plugin-data-spreadsheet-new/src/DataGrid/Formatters/Menu/CellMenu.tsx @@ -71,20 +71,21 @@ export const CellMenu = observer(function CellMenu({ model, actions, spre return ( - -
- -
-
+
+ +
+ +
+
+
); }); From eada3fddb5456f5ec7f0f270bcd0f6177391554f Mon Sep 17 00:00:00 2001 From: sergeyteleshev Date: Mon, 16 Dec 2024 17:01:32 +0100 Subject: [PATCH 2/2] CB-5955 Add create connection/folder actions in navigator tree (#3130) * CB-5955 adds create menu (connection, folder) * CB-5955 hides folder creation for non authorized users * CB-5955 fixes label * CB-5955 disable new connection * CB-5955 shows create actions only for folders and connections nodes * CB-5955 cleanup * CB-5955 Add handler to hide create menu when no items are available * CB-5955 Refactor handler to directly bind elementsTreeActionHandler for folder creation * CB-5955 pr fixes * CB-5955 creates connection according to the selected in tree project + tree selection refactor * CB-5955 reverts old behavior with Generate SQL submenu for connections * CB-5955 cleanup * CB-5955 adds connection via menu into any folder * CB-5955 updates TS references * CB-5955 cleanup * CB-5955 pr fixes * CB-5955 pr fixes * CB-5955 pr fixes * CB-5955 cleanup * CB-5955 fixes bug with dissapeared create connection button at top bar * CB-5955 pr fixes --------- Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com> --- .../src/NavTree/getFolderPath.ts | 12 ++ .../src/NavTree/getFolderPathWithProjectId.ts | 16 ++ .../src/NavTree/isConnectionNode.ts | 14 ++ webapp/packages/core-connections/src/index.ts | 3 + .../src/NodesManager/isConnectionFolder.ts | 13 ++ .../src/NodesManager/isProjectNode.ts | 14 ++ .../core-navigation-tree/src/index.ts | 2 + .../src/ContextMenu/SubMenuElement.tsx | 2 +- .../tsconfig.json | 14 +- .../plugin-connection-custom/package.json | 3 + .../src/CustomConnectionPluginBootstrap.ts | 89 ++++++++--- .../DriverSelector/DriverSelectorDialog.tsx | 22 +-- .../DriverSelector/useDriverSelectorDialog.ts | 28 +++- .../plugin-connection-custom/tsconfig.json | 6 + .../Actions/ACTION_TREE_CREATE_CONNECTION.ts | 12 ++ .../src/Actions/ACTION_TREE_CREATE_FOLDER.ts | 12 ++ .../NavNodes/ConnectionFoldersBootstrap.ts | 147 ++++++++---------- .../packages/plugin-connections/src/index.ts | 1 + .../plugin-connections/src/locales/en.ts | 1 + .../plugin-connections/src/locales/fr.ts | 1 + .../plugin-connections/src/locales/it.ts | 1 + .../plugin-connections/src/locales/ru.ts | 1 + .../plugin-connections/src/locales/zh.ts | 1 + .../src/NavNodes/ResourceFoldersBootstrap.ts | 84 ++-------- .../MENU_NAVIGATION_TREE_CREATE.ts | 10 ++ .../ElementsTree/TreeSelectionService.ts | 95 +++++++++++ .../navigationTreeProjectFilter.ts | 13 +- .../navigationTreeProjectSearchCompare.ts | 4 +- ...navigationTreeProjectsRendererRenderer.tsx | 7 +- .../NodesManager/NavNodeContextMenuService.ts | 12 +- .../plugin-navigation-tree/src/index.ts | 2 + .../plugin-navigation-tree/src/manifest.ts | 1 + 32 files changed, 431 insertions(+), 212 deletions(-) create mode 100644 webapp/packages/core-connections/src/NavTree/getFolderPath.ts create mode 100644 webapp/packages/core-connections/src/NavTree/getFolderPathWithProjectId.ts create mode 100644 webapp/packages/core-connections/src/NavTree/isConnectionNode.ts create mode 100644 webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts create mode 100644 webapp/packages/core-navigation-tree/src/NodesManager/isProjectNode.ts create mode 100644 webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts create mode 100644 webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.ts create mode 100644 webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts diff --git a/webapp/packages/core-connections/src/NavTree/getFolderPath.ts b/webapp/packages/core-connections/src/NavTree/getFolderPath.ts new file mode 100644 index 0000000000..0017d2c1fe --- /dev/null +++ b/webapp/packages/core-connections/src/NavTree/getFolderPath.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { getFolderPathWithProjectId } from './getFolderPathWithProjectId.js'; + +export function getFolderPath(folderId: string): string { + return getFolderPathWithProjectId(folderId).split('/').slice(1).join('/'); +} diff --git a/webapp/packages/core-connections/src/NavTree/getFolderPathWithProjectId.ts b/webapp/packages/core-connections/src/NavTree/getFolderPathWithProjectId.ts new file mode 100644 index 0000000000..8836dc85ee --- /dev/null +++ b/webapp/packages/core-connections/src/NavTree/getFolderPathWithProjectId.ts @@ -0,0 +1,16 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { isFolderNodeId } from './isFolderNodeId.js'; + +export function getFolderPathWithProjectId(folderId: string): string { + if (!isFolderNodeId(folderId)) { + throw new Error('Invalid folder id'); + } + + return folderId.replace('folder://', ''); +} diff --git a/webapp/packages/core-connections/src/NavTree/isConnectionNode.ts b/webapp/packages/core-connections/src/NavTree/isConnectionNode.ts new file mode 100644 index 0000000000..adae9c7a9e --- /dev/null +++ b/webapp/packages/core-connections/src/NavTree/isConnectionNode.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 type { NavNode } from '@cloudbeaver/core-navigation-tree'; + +import { NAV_NODE_TYPE_CONNECTION } from './NAV_NODE_TYPE_CONNECTION.js'; + +export function isConnectionNode(node: NavNode | undefined): boolean { + return node?.nodeType === NAV_NODE_TYPE_CONNECTION; +} diff --git a/webapp/packages/core-connections/src/index.ts b/webapp/packages/core-connections/src/index.ts index 83f8e3d3c7..d6ab151553 100644 --- a/webapp/packages/core-connections/src/index.ts +++ b/webapp/packages/core-connections/src/index.ts @@ -22,9 +22,12 @@ export * from './NavTree/ConnectionNavNodeService.js'; export * from './NavTree/NavNodeExtensionsService.js'; export * from './NavTree/getConnectionFolderIdFromNodeId.js'; export * from './NavTree/getConnectionFolderId.js'; +export * from './NavTree/getFolderPathWithProjectId.js'; +export * from './NavTree/getFolderPath.js'; export * from './NavTree/getConnectionParentId.js'; export * from './NavTree/getFolderNodeParents.js'; export * from './NavTree/NAV_NODE_TYPE_CONNECTION.js'; +export * from './NavTree/isConnectionNode.js'; export * from './extensions/IConnectionProvider.js'; export * from './extensions/IConnectionSetter.js'; diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts b/webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts new file mode 100644 index 0000000000..426364a81b --- /dev/null +++ b/webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts @@ -0,0 +1,13 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 type { NavNode } from './EntityTypes.js'; +import { NAV_NODE_TYPE_FOLDER } from './NAV_NODE_TYPE_FOLDER.js'; + +export function isConnectionFolder(node: NavNode | undefined): boolean { + return node?.nodeType === NAV_NODE_TYPE_FOLDER; +} diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/isProjectNode.ts b/webapp/packages/core-navigation-tree/src/NodesManager/isProjectNode.ts new file mode 100644 index 0000000000..bef92e3bc2 --- /dev/null +++ b/webapp/packages/core-navigation-tree/src/NodesManager/isProjectNode.ts @@ -0,0 +1,14 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; + +import type { NavNode } from './EntityTypes.js'; + +export function isProjectNode(node: NavNode | undefined): boolean { + return node?.nodeType === NAV_NODE_TYPE_PROJECT; +} diff --git a/webapp/packages/core-navigation-tree/src/index.ts b/webapp/packages/core-navigation-tree/src/index.ts index 5788ad37c5..6ece90a371 100644 --- a/webapp/packages/core-navigation-tree/src/index.ts +++ b/webapp/packages/core-navigation-tree/src/index.ts @@ -24,6 +24,8 @@ export * from './NodesManager/navNodeMoveContext.js'; export * from './NodesManager/getNodesFromContext.js'; export * from './NodesManager/NAV_NODE_TYPE_FOLDER.js'; export * from './NodesManager/NAV_NODE_TYPE_ROOT.js'; +export * from './NodesManager/isConnectionFolder.js'; +export * from './NodesManager/isProjectNode.js'; export * from './NodesManager/ENodeFeature.js'; export * from './NodesManager/EObjectFeature.js'; diff --git a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx index 836fc997f9..61a8b92720 100644 --- a/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx +++ b/webapp/packages/core-ui/src/ContextMenu/SubMenuElement.tsx @@ -66,7 +66,7 @@ export const SubMenuElement = observer( ['handleItemClose', 'hasBindings', 'handleVisibleSwitch'], ); - if (hidden) { + if (hidden || !subMenuData.items.length) { return null; } diff --git a/webapp/packages/plugin-app-logo-administration/tsconfig.json b/webapp/packages/plugin-app-logo-administration/tsconfig.json index 715a037c69..c5fef33116 100644 --- a/webapp/packages/plugin-app-logo-administration/tsconfig.json +++ b/webapp/packages/plugin-app-logo-administration/tsconfig.json @@ -19,6 +19,16 @@ "path": "../plugin-app-logo/tsconfig.json" } ], - "include": ["__custom_mocks__/**/*", "src/**/*", "src/**/*.json", "src/**/*.css", "src/**/*.scss"], - "exclude": ["**/node_modules", "lib/**/*", "dist/**/*"] + "include": [ + "__custom_mocks__/**/*", + "src/**/*", + "src/**/*.json", + "src/**/*.css", + "src/**/*.scss" + ], + "exclude": [ + "**/node_modules", + "lib/**/*", + "dist/**/*" + ] } diff --git a/webapp/packages/plugin-connection-custom/package.json b/webapp/packages/plugin-connection-custom/package.json index d9162a81d1..ab693f6580 100644 --- a/webapp/packages/plugin-connection-custom/package.json +++ b/webapp/packages/plugin-connection-custom/package.json @@ -20,16 +20,19 @@ "dependencies": { "@cloudbeaver/core-blocks": "^0", "@cloudbeaver/core-connections": "^0", + "@cloudbeaver/core-data-context": "^0", "@cloudbeaver/core-di": "^0", "@cloudbeaver/core-dialogs": "^0", "@cloudbeaver/core-events": "^0", "@cloudbeaver/core-localization": "^0", + "@cloudbeaver/core-navigation-tree": "^0", "@cloudbeaver/core-projects": "^0", "@cloudbeaver/core-resource": "^0", "@cloudbeaver/core-settings": "^0", "@cloudbeaver/core-utils": "^0", "@cloudbeaver/core-view": "^0", "@cloudbeaver/plugin-connections": "^0", + "@cloudbeaver/plugin-navigation-tree": "^0", "mobx": "^6", "mobx-react-lite": "^4", "react": "^18" diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts index dccb54acfc..1522be9aac 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts @@ -6,13 +6,16 @@ * you may not use this file except in compliance with the License. */ import { importLazyComponent } from '@cloudbeaver/core-blocks'; -import { ConnectionsManagerService } from '@cloudbeaver/core-connections'; +import { ConnectionsManagerService, getFolderPath, isConnectionNode } from '@cloudbeaver/core-connections'; +import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { DATA_CONTEXT_NAV_NODE, isConnectionFolder, isProjectNode } from '@cloudbeaver/core-navigation-tree'; +import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, getCachedMapResourceLoaderState } from '@cloudbeaver/core-resource'; -import { ActionService, MenuService } from '@cloudbeaver/core-view'; -import { MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; +import { ActionService, type IAction, MenuService } from '@cloudbeaver/core-view'; +import { ACTION_TREE_CREATE_CONNECTION, MENU_CONNECTIONS } from '@cloudbeaver/plugin-connections'; +import { DATA_CONTEXT_ELEMENTS_TREE, MENU_NAVIGATION_TREE_CREATE, TreeSelectionService } from '@cloudbeaver/plugin-navigation-tree'; import { ACTION_CONNECTION_CUSTOM } from './Actions/ACTION_CONNECTION_CUSTOM.js'; import { CustomConnectionSettingsService } from './CustomConnectionSettingsService.js'; @@ -28,6 +31,7 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { private readonly actionService: ActionService, private readonly connectionsManagerService: ConnectionsManagerService, private readonly customConnectionSettingsService: CustomConnectionSettingsService, + private readonly treeSelectionService: TreeSelectionService, ) { super(); } @@ -38,33 +42,70 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { getItems: (context, items) => [...items, ACTION_CONNECTION_CUSTOM], }); - this.actionService.addHandler({ - id: 'connection-custom', - actions: [ACTION_CONNECTION_CUSTOM], - isHidden: (context, action) => { - if (this.connectionsManagerService.createConnectionProjects.length === 0) { - return true; - } + this.menuService.addCreator({ + menus: [MENU_NAVIGATION_TREE_CREATE], + isApplicable: context => { + const node = context.get(DATA_CONTEXT_NAV_NODE); - if (action === ACTION_CONNECTION_CUSTOM) { - return this.customConnectionSettingsService.disabled; + if (![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true)) { + return false; } - return false; + return true; }, + getItems: (context, items) => [...items, ACTION_TREE_CREATE_CONNECTION], + }); + + this.actionService.addHandler({ + id: 'nav-tree-create-create-connection-handler', + menus: [MENU_NAVIGATION_TREE_CREATE], + actions: [ACTION_TREE_CREATE_CONNECTION], + contexts: [DATA_CONTEXT_ELEMENTS_TREE], getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), - handler: async (context, action) => { - switch (action) { - case ACTION_CONNECTION_CUSTOM: { - await this.openConnectionsDialog(); - break; - } - } - }, + handler: this.createConnectionHandler.bind(this), }); + + this.actionService.addHandler({ + id: 'connection-custom', + actions: [ACTION_CONNECTION_CUSTOM], + isHidden: (context, action) => this.isConnectionFeatureDisabled(action === ACTION_CONNECTION_CUSTOM), + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: this.createConnectionHandler.bind(this), + }); + } + + private async createConnectionHandler(context: IDataContextProvider, action: IAction) { + switch (action) { + case ACTION_TREE_CREATE_CONNECTION: { + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const projectId = this.treeSelectionService.getSelectedProject(tree)?.id; + const selectedNode = this.treeSelectionService.getFirstSelectedNode(tree, getProjectNodeId); + const folderPath = selectedNode?.folderId ? getFolderPath(selectedNode.folderId) : undefined; + await this.openConnectionsDialog(projectId, folderPath); + break; + } + case ACTION_CONNECTION_CUSTOM: + await this.openConnectionsDialog(); + break; + } } - private async openConnectionsDialog() { - await this.commonDialogService.open(DriverSelectorDialog, null); + private isConnectionFeatureDisabled(hasSettings: boolean) { + if (this.connectionsManagerService.createConnectionProjects.length === 0) { + return true; + } + + if (hasSettings) { + return this.customConnectionSettingsService.disabled; + } + + return false; + } + + private async openConnectionsDialog(projectId?: string, folderPath?: string) { + await this.commonDialogService.open(DriverSelectorDialog, { + projectId, + folderPath, + }); } } diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx index 668ceeea1c..90cffabfd2 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx @@ -8,7 +8,6 @@ import { observer } from 'mobx-react-lite'; import { CommonDialogBody, CommonDialogHeader, CommonDialogWrapper, s, useResource, useS, useTranslate } from '@cloudbeaver/core-blocks'; -import { DBDriverResource } from '@cloudbeaver/core-connections'; import type { DialogComponent } from '@cloudbeaver/core-dialogs'; import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey } from '@cloudbeaver/core-resource'; @@ -17,23 +16,26 @@ import { DriverSelector } from './DriverSelector.js'; import styles from './DriverSelectorDialog.module.css'; import { useDriverSelectorDialog } from './useDriverSelectorDialog.js'; -export const DriverSelectorDialog: DialogComponent = observer(function DriverSelectorDialog({ rejectDialog }) { +type Payload = { + projectId?: string; + folderPath?: string; +}; + +export const DriverSelectorDialog: DialogComponent = observer(function DriverSelectorDialog({ rejectDialog, payload }) { const translate = useTranslate(); const style = useS(styles); useResource(DriverSelectorDialog, ProjectInfoResource, CachedMapAllKey, { forceSuspense: true }); - const dbDriverResource = useResource(DriverSelectorDialog, DBDriverResource, CachedMapAllKey); - - const enabledDrivers = dbDriverResource.resource.enabledDrivers; - const dialog = useDriverSelectorDialog( - enabledDrivers.map(driver => driver.id), - rejectDialog, - ); + const dialog = useDriverSelectorDialog({ + projectId: payload.projectId, + folderPath: payload.folderPath, + onSelect: rejectDialog, + }); return ( - + ); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts index 49b5236146..518cb459f0 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts @@ -5,42 +5,56 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { action } from 'mobx'; +import { action, observable } from 'mobx'; -import { useObservableRef } from '@cloudbeaver/core-blocks'; -import { ConnectionsManagerService } from '@cloudbeaver/core-connections'; +import { useObservableRef, useResource } from '@cloudbeaver/core-blocks'; +import { ConnectionsManagerService, DBDriverResource } from '@cloudbeaver/core-connections'; import { useService } from '@cloudbeaver/core-di'; import { NotificationService } from '@cloudbeaver/core-events'; +import { CachedMapAllKey } from '@cloudbeaver/core-resource'; import { PublicConnectionFormService } from '@cloudbeaver/plugin-connections'; +import type { IDriver } from './Driver.js'; + interface State { select(driverId: string): Promise; + enabledDrivers: IDriver[]; +} + +interface DriverSelectorDialogArgs { + onSelect?: () => void; + projectId: string | undefined; + folderPath: string | undefined; } -export function useDriverSelectorDialog(drivers: string[], onSelect?: () => void) { +export function useDriverSelectorDialog({ onSelect, projectId, folderPath }: DriverSelectorDialogArgs) { const notificationService = useService(NotificationService); const connectionsManagerService = useService(ConnectionsManagerService); const publicConnectionFormService = useService(PublicConnectionFormService); + const dbDriverResource = useResource(useDriverSelectorDialog, DBDriverResource, CachedMapAllKey); + const enabledDrivers = dbDriverResource.resource.enabledDrivers; const state: State = useObservableRef( () => ({ async select(driverId: string) { const projects = this.connectionsManagerService.createConnectionProjects; + const drivers = this.enabledDrivers.map(driver => driver.id); if (projects.length === 0) { this.notificationService.logError({ title: 'core_projects_no_default_project' }); return; } - const state = await this.publicConnectionFormService.open(projects[0]!.id, { driverId }, this.drivers); + const selectedProjectId = projects.find(project => project.id === projectId)?.id || projects[0]!.id; + const state = await this.publicConnectionFormService.open(selectedProjectId, { driverId, folder: folderPath }, drivers); if (state) { onSelect?.(); } }, }), - { select: action.bound }, - { drivers, notificationService, connectionsManagerService, publicConnectionFormService }, + { select: action.bound, enabledDrivers: observable.ref }, + { notificationService, connectionsManagerService, publicConnectionFormService, enabledDrivers }, ); return state; diff --git a/webapp/packages/plugin-connection-custom/tsconfig.json b/webapp/packages/plugin-connection-custom/tsconfig.json index 0ae63b1262..17198e218a 100644 --- a/webapp/packages/plugin-connection-custom/tsconfig.json +++ b/webapp/packages/plugin-connection-custom/tsconfig.json @@ -24,6 +24,9 @@ { "path": "../core-localization/tsconfig.json" }, + { + "path": "../core-navigation-tree/tsconfig.json" + }, { "path": "../core-projects/tsconfig.json" }, @@ -41,6 +44,9 @@ }, { "path": "../plugin-connections/tsconfig.json" + }, + { + "path": "../plugin-navigation-tree/tsconfig.json" } ], "include": [ diff --git a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts new file mode 100644 index 0000000000..8aaa7e1829 --- /dev/null +++ b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_CONNECTION.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_TREE_CREATE_CONNECTION = createAction('create-tree-connection', { + label: 'plugin_connections_connection_create_menu_title', +}); diff --git a/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts new file mode 100644 index 0000000000..bd57b1ef06 --- /dev/null +++ b/webapp/packages/plugin-connections/src/Actions/ACTION_TREE_CREATE_FOLDER.ts @@ -0,0 +1,12 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { createAction } from '@cloudbeaver/core-view'; + +export const ACTION_TREE_CREATE_FOLDER = createAction('create-tree-folder', { + label: 'ui_folder_new', +}); diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index ce69635c20..8d8290c537 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -5,8 +5,6 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { untracked } from 'mobx'; - import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { ConfirmationDialogDelete } from '@cloudbeaver/core-blocks'; import { @@ -14,13 +12,13 @@ import { ConnectionFolderProjectKey, ConnectionFolderResource, ConnectionInfoResource, - ConnectionsManagerService, createConnectionFolderParam, createConnectionParam, getConnectionFolderId, getConnectionFolderIdFromNodeId, type IConnectionFolderParam, type IConnectionInfoParams, + isConnectionNode, } from '@cloudbeaver/core-connections'; import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; @@ -29,10 +27,12 @@ import { NotificationService } from '@cloudbeaver/core-events'; import { ExecutorInterrupter, type IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { + DATA_CONTEXT_NAV_NODE, ENodeMoveType, getNodesFromContext, type INodeMoveData, - NAV_NODE_TYPE_FOLDER, + isConnectionFolder, + isProjectNode, type NavNode, NavNodeInfoResource, NavNodeManagerService, @@ -42,22 +42,25 @@ import { ProjectsNavNodeService, ROOT_NODE_PATH, } from '@cloudbeaver/core-navigation-tree'; -import { getProjectNodeId, NAV_NODE_TYPE_PROJECT, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; +import { + CachedMapAllKey, + getCachedMapResourceLoaderState, + resourceKeyList, + type ResourceKeySimple, + ResourceKeyUtils, +} from '@cloudbeaver/core-resource'; import { createPath } from '@cloudbeaver/core-utils'; import { ACTION_NEW_FOLDER, ActionService, type IAction, MenuService } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_ELEMENTS_TREE, type IElementsTree, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; +import { + DATA_CONTEXT_ELEMENTS_TREE, + MENU_ELEMENTS_TREE_TOOLS, + MENU_NAVIGATION_TREE_CREATE, + TreeSelectionService, +} from '@cloudbeaver/plugin-navigation-tree'; import { FolderDialog } from '@cloudbeaver/plugin-projects'; -import { NAV_NODE_TYPE_CONNECTION } from './NAV_NODE_TYPE_CONNECTION.js'; - -interface ITargetNode { - projectId: string; - folderId?: string; - - projectNodeId: string; - selectProject: boolean; -} +import { ACTION_TREE_CREATE_FOLDER } from '../Actions/ACTION_TREE_CREATE_FOLDER.js'; @injectable() export class ConnectionFoldersBootstrap extends Bootstrap { @@ -70,12 +73,12 @@ export class ConnectionFoldersBootstrap extends Bootstrap { private readonly connectionInfoResource: ConnectionInfoResource, private readonly navNodeManagerService: NavNodeManagerService, private readonly connectionFolderResource: ConnectionFolderResource, - private readonly connectionsManagerService: ConnectionsManagerService, private readonly commonDialogService: CommonDialogService, private readonly notificationService: NotificationService, private readonly navNodeInfoResource: NavNodeInfoResource, private readonly projectInfoResource: ProjectInfoResource, private readonly projectsNavNodeService: ProjectsNavNodeService, + private readonly treeSelectionService: TreeSelectionService, ) { super(); } @@ -130,25 +133,11 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return false; } - const targetNode = this.getTargetNode(tree); + const targetNode = this.treeSelectionService.getFirstSelectedNode(tree, getProjectNodeId); return targetNode !== undefined; }, - // isDisabled: (context, action) => { - // const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); - - // if (!tree) { - // return true; - // } - - // if (action === ACTION_NEW_FOLDER) { - // const targetNode = this.getTargetNode(tree); - - // return targetNode === undefined; - // } - - // return false; - // }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.elementsTreeActionHandler.bind(this), }); @@ -162,10 +151,42 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return items; }, }); + + this.menuService.addCreator({ + menus: [MENU_NAVIGATION_TREE_CREATE], + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + getItems: (context, items) => [...items, ACTION_TREE_CREATE_FOLDER], + }); + + this.actionService.addHandler({ + id: 'nav-tree-create-create-folders-handler', + menus: [MENU_NAVIGATION_TREE_CREATE], + contexts: [DATA_CONTEXT_NAV_NODE, DATA_CONTEXT_ELEMENTS_TREE], + actions: [ACTION_TREE_CREATE_FOLDER], + isActionApplicable: (context, action) => { + const node = context.get(DATA_CONTEXT_NAV_NODE)!; + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + const targetNode = this.treeSelectionService.getFirstSelectedNode(tree, getProjectNodeId); + + if ( + action !== ACTION_TREE_CREATE_FOLDER || + ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || + !this.userInfoResource.isAuthenticated() || + tree.baseRoot !== ROOT_NODE_PATH || + targetNode === undefined + ) { + return false; + } + + return true; + }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), + handler: this.elementsTreeActionHandler.bind(this), + }); } private async moveConnectionToFolder({ type, targetNode, moveContexts }: INodeMoveData, contexts: IExecutionContextProvider) { - if (![NAV_NODE_TYPE_PROJECT, NAV_NODE_TYPE_FOLDER].includes(targetNode.nodeType!)) { + if (![isProjectNode, isConnectionFolder].some(check => check(targetNode))) { return; } @@ -179,7 +200,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const supported = nodes.every(node => { if ( - ![NAV_NODE_TYPE_CONNECTION, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_PROJECT].includes(node.nodeType!) || + ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || targetProject !== this.projectsNavNodeService.getProject(node.id) || children.includes(node.id) || targetNode.id === node.id @@ -202,9 +223,9 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children)); const folderDuplicates = nodes.filter( node => - node.nodeType === NAV_NODE_TYPE_FOLDER && - (childrenNode.some(child => child?.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name) || - nodes.some(child => child.nodeType === NAV_NODE_TYPE_FOLDER && child.name === node.name && child.id !== node.id)), + isConnectionFolder(node) && + (childrenNode.some(child => child && isConnectionFolder(child) && child.name === node.name) || + nodes.some(child => isConnectionFolder(child) && child.name === node.name && child.id !== node.id)), ); if (folderDuplicates.length > 0) { @@ -246,8 +267,9 @@ export class ConnectionFoldersBootstrap extends Bootstrap { } switch (action) { + case ACTION_TREE_CREATE_FOLDER: case ACTION_NEW_FOLDER: { - const targetNode = this.getTargetNode(tree); + const targetNode = this.treeSelectionService.getFirstSelectedNode(tree, getProjectNodeId); if (!targetNode) { this.notificationService.logError({ title: "Can't create folder", message: 'core_projects_no_default_project' }); @@ -312,51 +334,4 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.connectionFolderResource.markOutdated(); } } - - private getTargetNode(tree: IElementsTree): ITargetNode | undefined { - untracked(() => this.projectInfoResource.load(CachedMapAllKey)); - const selected = tree.getSelected(); - - if (selected.length === 0) { - const editableProjects = this.connectionsManagerService.createConnectionProjects; - - if (editableProjects.length > 0) { - const project = editableProjects[0]!; - - return { - projectId: project.id, - projectNodeId: getProjectNodeId(project.id), - selectProject: editableProjects.length > 1, - }; - } - return; - } - - const targetFolder = selected[0]!; - const parentIds = [...this.navNodeInfoResource.getParents(targetFolder), targetFolder]; - const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); - const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_PROJECT); - - if (!projectNode) { - return; - } - - const project = this.projectsNavNodeService.getByNodeId(projectNode.id); - - if (!project?.canEditDataSources) { - return; - } - - const targetFolderNode = parents - .slice() - .reverse() - .find(parent => parent?.nodeType === NAV_NODE_TYPE_FOLDER); - - return { - projectId: project.id, - folderId: targetFolderNode?.id, - projectNodeId: projectNode.id, - selectProject: false, - }; - } } diff --git a/webapp/packages/plugin-connections/src/index.ts b/webapp/packages/plugin-connections/src/index.ts index c5f442faa6..d10ffc9f40 100644 --- a/webapp/packages/plugin-connections/src/index.ts +++ b/webapp/packages/plugin-connections/src/index.ts @@ -30,5 +30,6 @@ export * from './PublicConnectionForm/PublicConnectionFormService.js'; export * from './ConnectionAuthService.js'; export * from './PluginConnectionsSettingsService.js'; export * from './ConnectionShieldLazy.js'; +export * from './Actions/ACTION_TREE_CREATE_CONNECTION.js'; export default connectionPlugin; diff --git a/webapp/packages/plugin-connections/src/locales/en.ts b/webapp/packages/plugin-connections/src/locales/en.ts index 8c55100259..61d5f9f09f 100644 --- a/webapp/packages/plugin-connections/src/locales/en.ts +++ b/webapp/packages/plugin-connections/src/locales/en.ts @@ -44,4 +44,5 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], + ['plugin_connections_connection_create_menu_title', 'Connection'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/fr.ts b/webapp/packages/plugin-connections/src/locales/fr.ts index 89b98319ce..5fb33cc349 100644 --- a/webapp/packages/plugin-connections/src/locales/fr.ts +++ b/webapp/packages/plugin-connections/src/locales/fr.ts @@ -53,4 +53,5 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info', "Vous pouvez gérer les identifiants dans l'onglet "], ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', 'Onglet Identifiants'], ['plugin_connections_connection_auth_secret_description', 'Veuillez sélectionner les identifiants fournis par une de vos équipes'], + ['plugin_connections_connection_create_menu_title', 'Connection'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/it.ts b/webapp/packages/plugin-connections/src/locales/it.ts index 986ec92b9b..1789dfe180 100644 --- a/webapp/packages/plugin-connections/src/locales/it.ts +++ b/webapp/packages/plugin-connections/src/locales/it.ts @@ -46,4 +46,5 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'There are multiple credentials available for authentication.\nPlease choose credentials you want to use.', ], + ['plugin_connections_connection_create_menu_title', 'Connection'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/ru.ts b/webapp/packages/plugin-connections/src/locales/ru.ts index 9b810049bf..e85d1f46cc 100644 --- a/webapp/packages/plugin-connections/src/locales/ru.ts +++ b/webapp/packages/plugin-connections/src/locales/ru.ts @@ -44,4 +44,5 @@ export default [ 'plugin_connections_connection_auth_secret_description', 'У вас есть несколько учетных записей для авторизации.\nВыберите учетную запись из списка.', ], + ['plugin_connections_connection_create_menu_title', 'Подключение'], ]; diff --git a/webapp/packages/plugin-connections/src/locales/zh.ts b/webapp/packages/plugin-connections/src/locales/zh.ts index ae9b7f3715..c22e3872d0 100644 --- a/webapp/packages/plugin-connections/src/locales/zh.ts +++ b/webapp/packages/plugin-connections/src/locales/zh.ts @@ -40,4 +40,5 @@ export default [ ['plugin_connections_connection_form_shared_credentials_manage_info', '您可在此管理凭证 '], ['plugin_connections_connection_form_shared_credentials_manage_info_tab_link', '凭证页签'], ['plugin_connections_connection_auth_secret_description', '有多个凭证可用于身份验证.\n请选择您要使用的凭证。'], + ['plugin_connections_connection_create_menu_title', 'Connection'], ]; diff --git a/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts b/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts index eb96149973..ac81dabbcb 100644 --- a/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts +++ b/webapp/packages/plugin-navigation-tree-rm/src/NavNodes/ResourceFoldersBootstrap.ts @@ -17,13 +17,12 @@ import { ENodeMoveType, getNodesFromContext, type INodeMoveData, - NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, ProjectsNavNodeService, } from '@cloudbeaver/core-navigation-tree'; -import { ProjectInfoResource, ProjectsService } from '@cloudbeaver/core-projects'; +import { ProjectInfoResource } from '@cloudbeaver/core-projects'; import { CachedMapAllKey, CachedTreeChildrenKey, @@ -41,7 +40,7 @@ import { } from '@cloudbeaver/core-resource-manager'; import { createPath, getPathParent } from '@cloudbeaver/core-utils'; import { ACTION_NEW_FOLDER, ActionService, type IAction, MenuService } from '@cloudbeaver/core-view'; -import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS } from '@cloudbeaver/plugin-navigation-tree'; +import { DATA_CONTEXT_ELEMENTS_TREE, MENU_ELEMENTS_TREE_TOOLS, TreeSelectionService } from '@cloudbeaver/plugin-navigation-tree'; import { FolderDialog } from '@cloudbeaver/plugin-projects'; import { ResourceManagerService } from '@cloudbeaver/plugin-resource-manager'; @@ -51,13 +50,6 @@ import { getResourceKeyFromNodeId } from './getResourceKeyFromNodeId.js'; import { getResourceNodeId } from './getResourceNodeId.js'; import { getRmProjectNodeId } from './getRmProjectNodeId.js'; -interface ITargetNode { - projectId: string; - folderId?: string; - projectNodeId: string; - selectProject: boolean; -} - @injectable() export class ResourceFoldersBootstrap extends Bootstrap { constructor( @@ -68,13 +60,12 @@ export class ResourceFoldersBootstrap extends Bootstrap { private readonly navNodeManagerService: NavNodeManagerService, private readonly resourceManagerResource: ResourceManagerResource, private readonly resourceManagerService: ResourceManagerService, - private readonly projectsService: ProjectsService, private readonly projectInfoResource: ProjectInfoResource, private readonly commonDialogService: CommonDialogService, private readonly actionService: ActionService, private readonly menuService: MenuService, private readonly navResourceNodeService: NavResourceNodeService, - private readonly navNodeInfoResource: NavNodeInfoResource, + private readonly treeSelectionService: TreeSelectionService, private readonly projectsNavNodeService: ProjectsNavNodeService, ) { super(); @@ -86,17 +77,22 @@ export class ResourceFoldersBootstrap extends Bootstrap { this.actionService.addHandler({ id: 'tree-tools-menu-resource-folders-handler', actions: [ACTION_NEW_FOLDER], + contexts: [DATA_CONTEXT_ELEMENTS_TREE, DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID], isActionApplicable: context => { - const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE); + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; - if (!tree?.baseRoot.startsWith(RESOURCES_NODE_PATH) || !this.userInfoResource.isAuthenticated()) { + if (!tree.baseRoot.startsWith(RESOURCES_NODE_PATH) || !this.userInfoResource.isAuthenticated()) { return false; } return true; }, getLoader: () => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), - isDisabled: context => this.getTargetNode(context) === undefined, + isDisabled: context => { + const tree = context.get(DATA_CONTEXT_ELEMENTS_TREE)!; + + return this.treeSelectionService.getFirstSelectedNode(tree, getRmProjectNodeId) === undefined; + }, handler: this.elementsTreeActionHandler.bind(this), }); @@ -164,10 +160,12 @@ export class ResourceFoldersBootstrap extends Bootstrap { } private async elementsTreeActionHandler(contexts: IDataContextProvider, action: IAction) { - const resourceTypeId = contexts.get(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID); + const resourceTypeId = contexts.get(DATA_CONTEXT_RESOURCE_MANAGER_TREE_RESOURCE_TYPE_ID)!; + const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE)!; + switch (action) { case ACTION_NEW_FOLDER: { - const targetNode = this.getTargetNode(contexts); + const targetNode = this.treeSelectionService.getFirstSelectedNode(tree, getRmProjectNodeId); if (!targetNode) { return; @@ -234,58 +232,6 @@ export class ResourceFoldersBootstrap extends Bootstrap { } } - private getTargetNode(contexts: IDataContextProvider): ITargetNode | undefined { - const tree = contexts.get(DATA_CONTEXT_ELEMENTS_TREE); - - if (!tree) { - return undefined; - } - - const selected = tree.getSelected(); - - if (selected.length === 0) { - const editableProjects = this.projectsService.activeProjects.filter(project => project.canEditResources); - - if (editableProjects.length > 0) { - const project = editableProjects[0]!; - - return { - projectId: project.id, - projectNodeId: getRmProjectNodeId(project.id), - selectProject: editableProjects.length > 1, - }; - } - return; - } - - const targetFolder = selected[0]!; - const parentIds = [...this.navNodeInfoResource.getParents(targetFolder), targetFolder]; - const parents = this.navNodeInfoResource.get(resourceKeyList(parentIds)); - const projectNode = parents.find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_PROJECT); - - if (!projectNode) { - return; - } - - const project = this.projectsNavNodeService.getByNodeId(projectNode.id); - - if (!project?.canEditResources) { - return; - } - - const targetFolderNode = parents - .slice() - .reverse() - .find(parent => parent?.nodeType === NAV_NODE_TYPE_RM_RESOURCE && parent.folder); - - return { - projectId: project.id, - folderId: targetFolderNode?.id, - projectNodeId: projectNode.id, - selectProject: false, - }; - } - private getResourceTypeFolder(projectId: string, resourceTypeId: string | undefined): string | undefined { if (!resourceTypeId) { return undefined; diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.ts new file mode 100644 index 0000000000..1017287609 --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.ts @@ -0,0 +1,10 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 DBeaver Corp and others + * + * Licensed under the Apache License, Version 2.0. + * you may not use this file except in compliance with the License. + */ +import { createMenu } from '@cloudbeaver/core-view'; + +export const MENU_NAVIGATION_TREE_CREATE = createMenu('navigation-tree-create-menu', 'ui_create'); diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts new file mode 100644 index 0000000000..44220c95aa --- /dev/null +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts @@ -0,0 +1,95 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2024 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 { ConnectionsManagerService } from '@cloudbeaver/core-connections'; +import { injectable } from '@cloudbeaver/core-di'; +import { isConnectionFolder, isProjectNode, type NavNode, NavNodeInfoResource, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree'; +import { type ProjectInfo } from '@cloudbeaver/core-projects'; +import { resourceKeyList } from '@cloudbeaver/core-resource'; +import { isNotNullDefined } from '@cloudbeaver/core-utils'; + +import type { IElementsTree } from './useElementsTree.js'; + +interface ISelectedNode { + projectId: string; + folderId?: string; + projectNodeId: string; + selectProject: boolean; +} + +type NodeIdGetter = (projectId: string) => string; + +@injectable() +export class TreeSelectionService { + constructor( + private readonly connectionsManagerService: ConnectionsManagerService, + private readonly navNodeInfoResource: NavNodeInfoResource, + private readonly projectsNavNodeService: ProjectsNavNodeService, + ) { + this.getSelectedProject = this.getSelectedProject.bind(this); + this.getFirstSelectedNode = this.getFirstSelectedNode.bind(this); + } + + // Should preload ProjectInfoResource. Cause the resource used indirectly (TODO make it directly used) + getFirstSelectedNode(tree: IElementsTree, nodeIdGetter: NodeIdGetter): ISelectedNode | undefined { + const selected = tree.getSelected(); + + if (selected.length === 0) { + const editableProjects = this.connectionsManagerService.createConnectionProjects; + + if (editableProjects.length > 0) { + const project = editableProjects[0]!; + + return { + projectId: project.id, + projectNodeId: nodeIdGetter(project.id), + selectProject: editableProjects.length > 1, + }; + } + + return; + } + + const project = this.getSelectedProject(tree); + + if (!project?.canEditDataSources) { + return; + } + + const selectedFolderNode = this.getParents(tree).slice().reverse().find(isConnectionFolder); + + return { + projectId: project.id, + folderId: selectedFolderNode?.id, + projectNodeId: nodeIdGetter(project.id), + selectProject: false, + }; + } + + // Should preload ProjectInfoResource. Cause the resource used indirectly (TODO make it directly used) + getSelectedProject(tree: IElementsTree): ProjectInfo | undefined { + const projectNode = this.getParents(tree).find(isProjectNode); + + if (!projectNode) { + return; + } + + return this.projectsNavNodeService.getByNodeId(projectNode.id); + } + + private getParents(tree: IElementsTree): NavNode[] { + const selected = tree.getSelected(); + const selectedFolder = selected[0]; + + if (!selectedFolder) { + return []; + } + + const parentIds = [...this.navNodeInfoResource.getParents(selectedFolder), selectedFolder]; + return this.navNodeInfoResource.get(resourceKeyList(parentIds)).filter(isNotNullDefined); + } +} diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts index 8b2a3008e6..c785b829a9 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectFilter.ts @@ -5,8 +5,15 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { type NavNode, NavNodeInfoResource, NavTreeResource, ProjectsNavNodeService, ROOT_NODE_PATH } from '@cloudbeaver/core-navigation-tree'; -import { NAV_NODE_TYPE_PROJECT, ProjectsService } from '@cloudbeaver/core-projects'; +import { + isProjectNode, + type NavNode, + NavNodeInfoResource, + NavTreeResource, + ProjectsNavNodeService, + ROOT_NODE_PATH, +} from '@cloudbeaver/core-navigation-tree'; +import { ProjectsService } from '@cloudbeaver/core-projects'; import { resourceKeyList } from '@cloudbeaver/core-resource'; import type { IElementsTreeFilter } from '../ElementsTree/useElementsTree.js'; @@ -26,7 +33,7 @@ export function navigationTreeProjectFilter( .get(resourceKeyList(children)) .filter((node => node !== undefined) as (node: NavNode | undefined) => node is NavNode) .filter(node => { - if (node.nodeType === NAV_NODE_TYPE_PROJECT) { + if (isProjectNode(node)) { const project = projectsNavNodeService.getProject(node.id); if (!project || !projectsService.activeProjects.includes(project)) { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectSearchCompare.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectSearchCompare.ts index 984032268c..83e32addf5 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectSearchCompare.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectSearchCompare.ts @@ -5,13 +5,13 @@ * Licensed under the Apache License, Version 2.0. * you may not use this file except in compliance with the License. */ -import { NAV_NODE_TYPE_PROJECT } from '@cloudbeaver/core-projects'; +import { isProjectNode } from '@cloudbeaver/core-navigation-tree'; import { elementsTreeNameFilterNode } from '../ElementsTree/elementsTreeNameFilter.js'; import { EEquality, type NavNodeFilterCompareFn } from '../ElementsTree/NavNodeFilterCompareFn.js'; export const navigationTreeProjectSearchCompare: NavNodeFilterCompareFn = function navigationTreeProjectSearchCompare(tree, node, filter) { - if (node.nodeType === NAV_NODE_TYPE_PROJECT) { + if (isProjectNode(node)) { return EEquality.none; } diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx index 96177d03a0..82deb5cca6 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ProjectsRenderer/navigationTreeProjectsRendererRenderer.tsx @@ -9,9 +9,7 @@ import { observer } from 'mobx-react-lite'; import { useContext } from 'react'; import { getComputed, s, SContext, type StyleRegistry, Translate, TreeNodeNestedMessage, useS } from '@cloudbeaver/core-blocks'; -import { useService } from '@cloudbeaver/core-di'; -import type { NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; -import { NAV_NODE_TYPE_PROJECT, ProjectsService } from '@cloudbeaver/core-projects'; +import { isProjectNode, type NavNodeInfoResource } from '@cloudbeaver/core-navigation-tree'; import { NavigationNodeControlRendererStyles, NavigationNodeNestedStyles } from '../../index.js'; import { useNode } from '../../NodesManager/useNode.js'; @@ -44,7 +42,7 @@ export function navigationTreeProjectsRendererRenderer(navNodeInfoResource: NavN return nodeId => { const node = navNodeInfoResource.get(nodeId); - if (node?.nodeType === NAV_NODE_TYPE_PROJECT) { + if (isProjectNode(node)) { return ProjectRenderer; } @@ -61,7 +59,6 @@ const ProjectRenderer: NavigationNodeRendererComponent = observer(function Manag expanded, }) { const styles = useS(style); - const projectsService = useService(ProjectsService); const elementsTreeContext = useContext(ElementsTreeContext); const { node } = useNode(nodeId); diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts index 18fd02b79e..f69e598c36 100644 --- a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts @@ -16,7 +16,7 @@ import { ENodeFeature, getNodePlainName, type INodeActions, - NAV_NODE_TYPE_FOLDER, + isConnectionFolder, type NavNode, NavNodeInfoResource, NavNodeManagerService, @@ -38,6 +38,7 @@ import { } from '@cloudbeaver/core-view'; import { DATA_CONTEXT_NAV_NODE_ACTIONS } from '../NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/DATA_CONTEXT_NAV_NODE_ACTIONS.js'; +import { MENU_NAVIGATION_TREE_CREATE } from '../NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.js'; export interface INodeMenuData { node: NavNode; @@ -100,7 +101,7 @@ export class NavNodeContextMenuService extends Bootstrap { isActionApplicable: (context, action): boolean => { const node = context.get(DATA_CONTEXT_NAV_NODE)!; - if (NodeManagerUtils.isDatabaseObject(node.id) || node.nodeType === NAV_NODE_TYPE_FOLDER) { + if (NodeManagerUtils.isDatabaseObject(node.id) || isConnectionFolder(node)) { if (action === ACTION_RENAME) { return node.features?.includes(ENodeFeature.canRename) ?? false; } @@ -180,11 +181,16 @@ export class NavNodeContextMenuService extends Bootstrap { }, }); + this.menuService.setHandler({ + id: 'menu-navigation-tree-create', + menus: [MENU_NAVIGATION_TREE_CREATE], + }); + this.menuService.addCreator({ root: true, contexts: [DATA_CONTEXT_NAV_NODE], getItems: (context, items) => { - items = [ACTION_OPEN, ACTION_REFRESH, ...items]; + items = [MENU_NAVIGATION_TREE_CREATE, ACTION_OPEN, ACTION_REFRESH, ...items]; if (this.navTreeSettingsService.editing) { items.push(ACTION_RENAME); diff --git a/webapp/packages/plugin-navigation-tree/src/index.ts b/webapp/packages/plugin-navigation-tree/src/index.ts index 3f84713274..8bb20c34ef 100644 --- a/webapp/packages/plugin-navigation-tree/src/index.ts +++ b/webapp/packages/plugin-navigation-tree/src/index.ts @@ -15,6 +15,7 @@ export * from './NavigationTree/ElementsTree/ElementsTreeTools/NavigationTreeSet export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNode/NavigationNodeLoaders.js'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/DATA_CONTEXT_NAV_NODE_ACTIONS.js'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/TreeNodeMenuLoader.js'; +export * from './NavigationTree/ElementsTree/NavigationTreeNode/TreeNodeMenu/MENU_NAVIGATION_TREE_CREATE.js'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/NavigationNodeRendererLoader.js'; export * from './NavigationTree/ElementsTree/NavigationTreeNode/isDraggingInsideProject.js'; @@ -34,6 +35,7 @@ export * from './NavigationTree/ElementsTree/ElementsTreeTools/MENU_ELEMENTS_TRE export * from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeToolsMenuService.js'; export * from './NavigationTree/ElementsTree/elementsTreeNameFilter.js'; export * from './NavigationTree/ElementsTree/ElementsTreeService.js'; +export * from './NavigationTree/ElementsTree/TreeSelectionService.js'; export * from './NavigationTree/NavigationTreeBootstrap.js'; export * from './NavigationTree/NavigationTreeService.js'; export { default as ElementsTreeToolsStyles } from './NavigationTree/ElementsTree/ElementsTreeTools/ElementsTreeTools.module.css'; diff --git a/webapp/packages/plugin-navigation-tree/src/manifest.ts b/webapp/packages/plugin-navigation-tree/src/manifest.ts index 6b9d66bb38..707eee0998 100644 --- a/webapp/packages/plugin-navigation-tree/src/manifest.ts +++ b/webapp/packages/plugin-navigation-tree/src/manifest.ts @@ -22,5 +22,6 @@ export const navigationTreePlugin: PluginManifest = { ), () => import('./NavigationTreeSettingsService.js').then(m => m.NavigationTreeSettingsService), () => import('./NavigationTree/ElementsTree/ElementsTreeService.js').then(m => m.ElementsTreeService), + () => import('./NavigationTree/ElementsTree/TreeSelectionService.js').then(m => m.TreeSelectionService), ], };