diff --git a/webapp/packages/core-navigation-tree/src/NodesManager/isFolderNode.ts b/webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts similarity index 84% rename from webapp/packages/core-navigation-tree/src/NodesManager/isFolderNode.ts rename to webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts index 6c78bef6d8..426364a81b 100644 --- a/webapp/packages/core-navigation-tree/src/NodesManager/isFolderNode.ts +++ b/webapp/packages/core-navigation-tree/src/NodesManager/isConnectionFolder.ts @@ -8,6 +8,6 @@ import type { NavNode } from './EntityTypes.js'; import { NAV_NODE_TYPE_FOLDER } from './NAV_NODE_TYPE_FOLDER.js'; -export function isFolderNode(node: NavNode | undefined): boolean { +export function isConnectionFolder(node: NavNode | undefined): boolean { return node?.nodeType === NAV_NODE_TYPE_FOLDER; } diff --git a/webapp/packages/core-navigation-tree/src/index.ts b/webapp/packages/core-navigation-tree/src/index.ts index 2993ff891d..6ece90a371 100644 --- a/webapp/packages/core-navigation-tree/src/index.ts +++ b/webapp/packages/core-navigation-tree/src/index.ts @@ -24,7 +24,7 @@ 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/isFolderNode.js'; +export * from './NodesManager/isConnectionFolder.js'; export * from './NodesManager/isProjectNode.js'; export * from './NodesManager/ENodeFeature.js'; diff --git a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts index 0834e63f24..1522be9aac 100644 --- a/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts +++ b/webapp/packages/plugin-connection-custom/src/CustomConnectionPluginBootstrap.ts @@ -10,7 +10,7 @@ import { ConnectionsManagerService, getFolderPath, isConnectionNode } from '@clo import type { IDataContextProvider } from '@cloudbeaver/core-data-context'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService } from '@cloudbeaver/core-dialogs'; -import { DATA_CONTEXT_NAV_NODE, isFolderNode, isProjectNode } from '@cloudbeaver/core-navigation-tree'; +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, type IAction, MenuService } from '@cloudbeaver/core-view'; @@ -47,7 +47,7 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { isApplicable: context => { const node = context.get(DATA_CONTEXT_NAV_NODE); - if (![isConnectionNode, isFolderNode, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true)) { + if (![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || this.isConnectionFeatureDisabled(true)) { return false; } @@ -61,6 +61,7 @@ export class CustomConnectionPluginBootstrap extends Bootstrap { menus: [MENU_NAVIGATION_TREE_CREATE], actions: [ACTION_TREE_CREATE_CONNECTION], contexts: [DATA_CONTEXT_ELEMENTS_TREE], + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.createConnectionHandler.bind(this), }); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx b/webapp/packages/plugin-connection-custom/src/DriverSelector/DriverSelectorDialog.tsx index 1c53de307e..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'; @@ -26,11 +25,7 @@ export const DriverSelectorDialog: DialogComponent = observer(function 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({ - drivers: enabledDrivers.map(driver => driver.id), projectId: payload.projectId, folderPath: payload.folderPath, onSelect: rejectDialog, @@ -40,7 +35,7 @@ export const DriverSelectorDialog: DialogComponent = observer(function - + ); diff --git a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts index 6e4d55ab03..518cb459f0 100644 --- a/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts +++ b/webapp/packages/plugin-connection-custom/src/DriverSelector/useDriverSelectorDialog.ts @@ -5,34 +5,40 @@ * 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[]; } -type Args = { - drivers: string[]; +interface DriverSelectorDialogArgs { onSelect?: () => void; projectId: string | undefined; folderPath: string | undefined; -}; +} -export function useDriverSelectorDialog({ drivers, onSelect, projectId, folderPath }: Args) { +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' }); @@ -40,15 +46,15 @@ export function useDriverSelectorDialog({ drivers, onSelect, projectId, folderPa } const selectedProjectId = projects.find(project => project.id === projectId)?.id || projects[0]!.id; - const state = await this.publicConnectionFormService.open(selectedProjectId, { driverId, folder: folderPath }, this.drivers); + 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-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index 0d501eac6d..8d8290c537 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -31,7 +31,7 @@ import { ENodeMoveType, getNodesFromContext, type INodeMoveData, - isFolderNode, + isConnectionFolder, isProjectNode, type NavNode, NavNodeInfoResource, @@ -43,7 +43,13 @@ import { ROOT_NODE_PATH, } from '@cloudbeaver/core-navigation-tree'; import { getProjectNodeId, ProjectInfoResource } from '@cloudbeaver/core-projects'; -import { CachedMapAllKey, resourceKeyList, type ResourceKeySimple, ResourceKeyUtils } from '@cloudbeaver/core-resource'; +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 { @@ -131,6 +137,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return targetNode !== undefined; }, + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.elementsTreeActionHandler.bind(this), }); @@ -147,13 +154,23 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.menuService.addCreator({ menus: [MENU_NAVIGATION_TREE_CREATE], - isApplicable: context => { - const node = context.get(DATA_CONTEXT_NAV_NODE); + 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 ( - ![isConnectionNode, isFolderNode, isProjectNode].some(check => check(node)) || + action !== ACTION_TREE_CREATE_FOLDER || + ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || !this.userInfoResource.isAuthenticated() || tree.baseRoot !== ROOT_NODE_PATH || targetNode === undefined @@ -163,19 +180,13 @@ export class ConnectionFoldersBootstrap extends Bootstrap { return true; }, - getItems: (context, items) => [...items, ACTION_TREE_CREATE_FOLDER], - }); - - this.actionService.addHandler({ - id: 'nav-tree-create-create-folders-handler', - menus: [MENU_NAVIGATION_TREE_CREATE], - actions: [ACTION_TREE_CREATE_FOLDER], + getLoader: (context, action) => getCachedMapResourceLoaderState(this.projectInfoResource, () => CachedMapAllKey), handler: this.elementsTreeActionHandler.bind(this), }); } private async moveConnectionToFolder({ type, targetNode, moveContexts }: INodeMoveData, contexts: IExecutionContextProvider) { - if (![isProjectNode, isFolderNode].some(check => check(targetNode))) { + if (![isProjectNode, isConnectionFolder].some(check => check(targetNode))) { return; } @@ -189,7 +200,7 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const supported = nodes.every(node => { if ( - ![isConnectionNode, isFolderNode, isProjectNode].some(check => check(node)) || + ![isConnectionNode, isConnectionFolder, isProjectNode].some(check => check(node)) || targetProject !== this.projectsNavNodeService.getProject(node.id) || children.includes(node.id) || targetNode.id === node.id @@ -212,9 +223,9 @@ export class ConnectionFoldersBootstrap extends Bootstrap { const childrenNode = this.navNodeInfoResource.get(resourceKeyList(children)); const folderDuplicates = nodes.filter( node => - isFolderNode(node) && - (childrenNode.some(child => child && isFolderNode(child) && child.name === node.name) || - nodes.some(child => isFolderNode(child) && 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) { diff --git a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts index c999b4e3bf..44220c95aa 100644 --- a/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts +++ b/webapp/packages/plugin-navigation-tree/src/NavigationTree/ElementsTree/TreeSelectionService.ts @@ -7,7 +7,7 @@ */ import { ConnectionsManagerService } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { isFolderNode, isProjectNode, type NavNode, NavNodeInfoResource, ProjectsNavNodeService } from '@cloudbeaver/core-navigation-tree'; +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'; @@ -34,6 +34,7 @@ export class TreeSelectionService { 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(); @@ -53,28 +54,23 @@ export class TreeSelectionService { return; } - const projectNode = this.getParents(tree).find(isProjectNode); - - if (!projectNode) { - return; - } - const project = this.getSelectedProject(tree); if (!project?.canEditDataSources) { return; } - const selectedFolderNode = this.getParents(tree).slice().reverse().find(isFolderNode); + const selectedFolderNode = this.getParents(tree).slice().reverse().find(isConnectionFolder); return { projectId: project.id, folderId: selectedFolderNode?.id, - projectNodeId: projectNode.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); diff --git a/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/plugin-navigation-tree/src/NodesManager/NavNodeContextMenuService.ts index a6ceff919d..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, - isFolderNode, + isConnectionFolder, type NavNode, NavNodeInfoResource, NavNodeManagerService, @@ -101,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) || isFolderNode(node)) { + if (NodeManagerUtils.isDatabaseObject(node.id) || isConnectionFolder(node)) { if (action === ACTION_RENAME) { return node.features?.includes(ENodeFeature.canRename) ?? false; }