From 665c98cdcdc5dac0790b6956c58ee8cc4b20fd20 Mon Sep 17 00:00:00 2001 From: Wroud Date: Tue, 12 Jul 2022 00:55:07 +0300 Subject: [PATCH] feat(core-app): CB-2297 customize node delete confirmation --- webapp/packages/core-app/src/index.ts | 1 + .../NodesManager/NavNodeContextMenuService.ts | 46 ++++++++++++++----- .../shared/NodesManager/NavTreeResource.ts | 11 ++++- .../shared/NodesManager/nodeDeleteContext.ts | 27 +++++++++++ .../core-sdk/src/Resource/ResourceKeyList.ts | 4 ++ .../NavNodes/ConnectionFoldersBootstrap.ts | 45 ++++++++++++++++-- 6 files changed, 118 insertions(+), 16 deletions(-) create mode 100644 webapp/packages/core-app/src/shared/NodesManager/nodeDeleteContext.ts diff --git a/webapp/packages/core-app/src/index.ts b/webapp/packages/core-app/src/index.ts index ebd845306e..a37baa691d 100644 --- a/webapp/packages/core-app/src/index.ts +++ b/webapp/packages/core-app/src/index.ts @@ -23,6 +23,7 @@ export * from './shared/NodesManager/NavNodeContextMenuService'; export * from './shared/NodesManager/NavNodeInfoResource'; export * from './shared/NodesManager/NavNodeManagerService'; export * from './shared/NodesManager/NavTreeResource'; +export * from './shared/NodesManager/nodeDeleteContext'; export * from './shared/NodesManager/NavNodeExtensionsService'; export * from './shared/NodesManager/NodeLink'; export * from './shared/NodesManager/NodeManagerUtils'; diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts index 1793f90e6d..bdddb5d10e 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavNodeContextMenuService.ts @@ -11,7 +11,9 @@ import { untracked } from 'mobx'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; +import { ExecutorInterrupter } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; +import { ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { ActionService, ACTION_DELETE, ACTION_OPEN, ACTION_REFRESH, ACTION_RENAME, DATA_CONTEXT_MENU_NESTED, menuExtractActions, MenuSeparatorItem, MenuService } from '@cloudbeaver/core-view'; import { CoreSettingsService } from '../../CoreSettingsService'; @@ -20,9 +22,10 @@ import { DATA_CONTEXT_NAV_NODE } from './DATA_CONTEXT_NAV_NODE'; import { ENodeFeature } from './ENodeFeature'; import type { NavNode } from './EntityTypes'; import type { INodeActions } from './INodeActions'; -import { getNodeDisplayName } from './NavNodeInfoResource'; +import { getNodeDisplayName, NavNodeInfoResource } from './NavNodeInfoResource'; import { NavNodeManagerService } from './NavNodeManagerService'; import { NavTreeResource } from './NavTreeResource'; +import { nodeDeleteContext } from './nodeDeleteContext'; export interface INodeMenuData { node: NavNode; @@ -39,12 +42,41 @@ export class NavNodeContextMenuService extends Bootstrap { private readonly actionService: ActionService, private readonly menuService: MenuService, private readonly coreSettingsService: CoreSettingsService, - private readonly localizationService: LocalizationService + private readonly localizationService: LocalizationService, + private readonly navNodeInfoResource: NavNodeInfoResource ) { super(); } register(): void { + this.navTreeResource.beforeNodeDelete.addPostHandler(async (data, contexts) => { + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + + const deleteContext = contexts.getContext(nodeDeleteContext); + + if (deleteContext.confirmed) { + return; + } + + const nodes = ResourceKeyUtils + .mapArray(data, nodeId => this.navNodeInfoResource.get(nodeId)) + .filter(Boolean as any) + .map(node => node.name) + .join(); + + const result = await this.commonDialogService.open(ConfirmationDialogDelete, { + title: 'ui_data_delete_confirmation', + message: this.localizationService.translate('app_navigationTree_node_delete_confirmation', undefined, { name: nodes }), + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } + }); + this.actionService.addHandler({ id: 'nav-node-base-handler', isActionApplicable: (context, action): boolean => { @@ -130,16 +162,6 @@ export class NavNodeContextMenuService extends Bootstrap { break; } case ACTION_DELETE: { - const result = await this.commonDialogService.open(ConfirmationDialogDelete, { - title: 'ui_data_delete_confirmation', - message: this.localizationService.translate('app_navigationTree_node_delete_confirmation', undefined, { name }), - confirmActionText: 'ui_delete', - }); - - if (result === DialogueStateResult.Rejected) { - return; - } - try { await this.navTreeResource.deleteNode(node.id); } catch (exception: any) { diff --git a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts index 665f962e38..8dbcd2456b 100644 --- a/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts +++ b/webapp/packages/core-app/src/shared/NodesManager/NavTreeResource.ts @@ -11,7 +11,7 @@ import { action, computed, makeObservable, runInAction } from 'mobx'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { Connection, ConnectionInfoResource } from '@cloudbeaver/core-connections'; import { injectable } from '@cloudbeaver/core-di'; -import { Executor, IExecutor } from '@cloudbeaver/core-executor'; +import { Executor, ExecutorInterrupter, IExecutor } from '@cloudbeaver/core-executor'; import { EPermission, SessionPermissionsResource, SessionDataResource } from '@cloudbeaver/core-root'; import { GraphQLService, @@ -45,6 +45,7 @@ interface INodeMetadata extends ICachedMapResourceMetadata { @injectable() export class NavTreeResource extends CachedMapResource { + readonly beforeNodeDelete: IExecutor>; readonly onNodeRefresh: IExecutor; protected metadata: MetadataMap; @@ -63,6 +64,8 @@ export class NavTreeResource extends CachedMapResource { ) { super(); + this.beforeNodeDelete = new Executor(); + makeObservable(this, { childrenLimit: computed, setDetails: action, @@ -164,6 +167,12 @@ export class NavTreeResource extends CachedMapResource { } async deleteNode(key: ResourceKey): Promise { + const contexts = await this.beforeNodeDelete.execute(key); + + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + const nodePaths = isResourceKeyList(key) ? key.list : [key]; await this.performUpdate(key, [], async () => { diff --git a/webapp/packages/core-app/src/shared/NodesManager/nodeDeleteContext.ts b/webapp/packages/core-app/src/shared/NodesManager/nodeDeleteContext.ts new file mode 100644 index 0000000000..d4e7accee2 --- /dev/null +++ b/webapp/packages/core-app/src/shared/NodesManager/nodeDeleteContext.ts @@ -0,0 +1,27 @@ +/* + * CloudBeaver - Cloud Database Manager + * Copyright (C) 2020-2022 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 { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import type { ResourceKey } from '@cloudbeaver/core-sdk'; + +export interface INodeDeleteContext { + confirmed: boolean; + confirm(): void; +} + +export function nodeDeleteContext( + contexts: IExecutionContextProvider>, + data: ResourceKey +): INodeDeleteContext { + return { + confirmed: false, + confirm() { + this.confirmed = true; + }, + }; +} \ No newline at end of file diff --git a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts index 561eb9e56a..f5d3709062 100644 --- a/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts +++ b/webapp/packages/core-sdk/src/Resource/ResourceKeyList.ts @@ -46,6 +46,7 @@ export interface ResourceKeyUtils { every: (key: ResourceKey, predicate: (key: TKey, index: number) => boolean) => boolean; map: MapFnc; toArray: (key: ResourceKey) => TKey[]; + filter: (key: ResourceKey, filter: (key: TKey) => boolean) => TKey[]; mapKey: (key: ResourceKey, selector: (key: TKey, index: number) => TValue) => ResourceKey; mapArray: (key: ResourceKey, selector: (key: TKey, index: number) => TValue) => TValue[]; includes: (first: ResourceKey, second: ResourceKey) => boolean; @@ -135,6 +136,9 @@ export const ResourceKeyUtils: ResourceKeyUtils = { return [key]; } }, + filter(key: ResourceKey, filter: (key: TKey) => boolean): TKey[] { + return this.toArray(key).filter(filter); + }, mapKey(key: ResourceKey, selector: (key: TKey, index: number) => TValue): ResourceKey { if (isResourceKeyList(key)) { diff --git a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts index f6c9e46fb9..3d0db4cf72 100644 --- a/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts +++ b/webapp/packages/plugin-connections/src/NavNodes/ConnectionFoldersBootstrap.ts @@ -7,13 +7,13 @@ */ -import { DATA_CONTEXT_ELEMENTS_TREE, ENodeFeature, ENodeMoveType, getNodesFromContext, IElementsTree, INodeMoveData, MENU_ELEMENTS_TREE_TOOLS, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_ROOT, ROOT_NODE_PATH } from '@cloudbeaver/core-app'; +import { DATA_CONTEXT_ELEMENTS_TREE, ENodeFeature, ENodeMoveType, getNodesFromContext, IElementsTree, INodeMoveData, MENU_ELEMENTS_TREE_TOOLS, NavNode, NavNodeInfoResource, NavNodeManagerService, navNodeMoveContext, NavTreeResource, NAV_NODE_TYPE_FOLDER, NAV_NODE_TYPE_ROOT, nodeDeleteContext, ROOT_NODE_PATH } from '@cloudbeaver/core-app'; import { UserInfoResource } from '@cloudbeaver/core-authentication'; import { ConnectionFolderResource, ConnectionInfoResource, CONNECTION_FOLDER_NAME_VALIDATION } from '@cloudbeaver/core-connections'; import { Bootstrap, injectable } from '@cloudbeaver/core-di'; -import { CommonDialogService, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; +import { CommonDialogService, ConfirmationDialogDelete, DialogueStateResult, RenameDialog } from '@cloudbeaver/core-dialogs'; import { NotificationService } from '@cloudbeaver/core-events'; -import type { IExecutionContextProvider } from '@cloudbeaver/core-executor'; +import { ExecutorInterrupter, IExecutionContextProvider } from '@cloudbeaver/core-executor'; import { LocalizationService } from '@cloudbeaver/core-localization'; import { CachedMapAllKey, ResourceKey, resourceKeyList, ResourceKeyUtils } from '@cloudbeaver/core-sdk'; import { ActionService, ACTION_NEW_FOLDER, DATA_CONTEXT_MENU, IAction, IDataContextProvider, MenuService } from '@cloudbeaver/core-view'; @@ -44,6 +44,45 @@ export class ConnectionFoldersBootstrap extends Bootstrap { this.navNodeInfoResource.onItemDelete.addHandler(this.syncWithNavTree.bind(this)); this.navNodeManagerService.onMove.addHandler(this.moveConnectionToFolder.bind(this)); + this.navTreeResource.beforeNodeDelete.addHandler(async (data, contexts) => { + if (ExecutorInterrupter.isInterrupted(contexts)) { + return; + } + + const deleteContext = contexts.getContext(nodeDeleteContext); + + if (deleteContext.confirmed) { + return; + } + await this.connectionFolderResource.load(CachedMapAllKey); + + const nodes = ResourceKeyUtils + .filter( + data, + nodeId => this.connectionFolderResource.fromNodeId(nodeId) !== undefined + ) + .map(nodeId => this.navNodeInfoResource.get(nodeId)) + .filter(Boolean as any) + .map(node => node.name) + .join(); + + if (!nodes) { + return; + } + + const result = await this.commonDialogService.open(ConfirmationDialogDelete, { + title: 'ui_data_delete_confirmation', + message: this.localizationService.translate('connections_public_connection_folder_delete_confirmation', undefined, { name: nodes }), + confirmActionText: 'ui_delete', + }); + + if (result === DialogueStateResult.Rejected) { + ExecutorInterrupter.interrupt(contexts); + } else { + deleteContext.confirm(); + } + }); + this.actionService.addHandler({ id: 'tree-tools-menu-folders-handler', isActionApplicable: (context, action) => {