diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 1194cac88b..569c4c95ff 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -493,7 +493,8 @@ { "command": "mssql.designTable", "when": "connectionProvider == MSSQL && nodeType == Table && nodeSubType != LedgerDropped", - "group": "0_query@3" + "group": "0_query@3", + "isDefault": true }, { "command": "mssql.newTable", @@ -513,7 +514,8 @@ { "command": "mssql.objectProperties", "when": "connectionProvider == MSSQL && nodeType =~ /^(ServerLevelLogin|User)$/ && config.workbench.enablePreviewFeatures", - "group": "0_query@1" + "group": "0_query@1", + "isDefault": true }, { "command": "mssql.deleteObject", diff --git a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts index 8242075aa5..883c25f398 100644 --- a/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts +++ b/src/sql/workbench/contrib/objectExplorer/browser/serverTreeView.ts @@ -43,6 +43,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { CONNECTIONS_SORT_BY_CONFIG_KEY } from 'sql/platform/connection/common/connectionConfig'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; import { debounce } from 'vs/base/common/decorators'; +import { ActionRunner } from 'vs/base/common/actions'; export const CONTEXT_SERVER_TREE_VIEW = new RawContextKey('serverTreeView.view', ServerTreeViewView.all); export const CONTEXT_SERVER_TREE_HAS_CONNECTIONS = new RawContextKey('serverTreeView.hasConnections', false); @@ -60,6 +61,7 @@ export class ServerTreeView extends Disposable implements IServerTreeView { private _actionProvider: ServerTreeActionProvider; private _viewKey: IContextKey; private _hasConnectionsKey: IContextKey; + private _actionRunner: ActionRunner; constructor( @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, @@ -79,6 +81,8 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._treeSelectionHandler = this._instantiationService.createInstance(TreeSelectionHandler); this._onSelectionOrFocusChange = new Emitter(); this._actionProvider = this._instantiationService.createInstance(ServerTreeActionProvider); + this._actionRunner = new ActionRunner(); + this._register(this._actionRunner); this._capabilitiesService.onCapabilitiesRegistered(async () => { await this.handleOnCapabilitiesRegistered(); }); @@ -166,21 +170,8 @@ export class ServerTreeView extends Disposable implements IServerTreeView { this._register(this._tree.onDidBlur(() => this._onSelectionOrFocusChange.fire())); this._register(this._tree.onDidChangeFocus(() => this._onSelectionOrFocusChange.fire())); if (this._tree instanceof AsyncServerTree) { - this._register(this._tree.onContextMenu(e => this.onContextMenu(e))); - this._register(this._tree.onMouseDblClick(e => { - // Open dashboard on double click for server and database nodes - let connectionProfile: ConnectionProfile | undefined; - if (e.element instanceof ConnectionProfile) { - connectionProfile = e.element; - } else if (e.element instanceof TreeNode) { - if (TreeUpdateUtils.isAvailableDatabaseNode(e.element)) { - connectionProfile = TreeUpdateUtils.getConnectionProfile(e.element); - } - } - if (connectionProfile) { - this._connectionManagementService.showDashboard(connectionProfile); - } - })); + this._register(this._tree.onContextMenu(e => this.onTreeNodeContextMenu(e))); + this._register(this._tree.onMouseDblClick(async e => { await this.onTreeNodeDoubleClick(e.element); })); this._register(this._connectionManagementService.onConnectionChanged(() => { this.refreshTree().catch(err => errors.onUnexpectedError); })); @@ -539,7 +530,12 @@ export class ServerTreeView extends Disposable implements IServerTreeView { } private onSelected(event: any): void { - this._treeSelectionHandler.onTreeSelect(event, this._tree!, this._connectionManagementService, this._objectExplorerService, this._capabilitiesService, () => this._onSelectionOrFocusChange.fire()); + this._treeSelectionHandler.onTreeSelect(event, this._tree!, + this._connectionManagementService, + this._objectExplorerService, + this._capabilitiesService, + () => this._onSelectionOrFocusChange.fire(), + (node) => { this.onTreeNodeDoubleClick(node).catch(errors.onUnexpectedError); }); this._onSelectionOrFocusChange.fire(); } @@ -592,7 +588,6 @@ export class ServerTreeView extends Disposable implements IServerTreeView { } else { this._tree!.clearSelection(); } - } if (selected) { if (this._tree instanceof AsyncServerTree) { @@ -626,35 +621,15 @@ export class ServerTreeView extends Disposable implements IServerTreeView { /** * Return actions in the context menu */ - private onContextMenu(e: ITreeContextMenuEvent): boolean { + private onTreeNodeContextMenu(e: ITreeContextMenuEvent): void { if (e.element) { e.browserEvent.preventDefault(); e.browserEvent.stopPropagation(); this._tree!.setSelection([e.element]); - - let actionContext: any; - if (e.element instanceof TreeNode) { - let context = new ObjectExplorerActionsContext(); - context.nodeInfo = e.element.toNodeInfo(); - // Note: getting DB name before, but intentionally not using treeUpdateUtils.getConnectionProfile as it replaces - // the connection ID with a new one. This breaks a number of internal tasks - context.connectionProfile = e.element.getConnectionProfile()!.toIConnectionProfile(); - context.connectionProfile.databaseName = e.element.getDatabaseName(); - actionContext = context; - } else if (e.element instanceof ConnectionProfile) { - let context = new ObjectExplorerActionsContext(); - context.connectionProfile = e.element.toIConnectionProfile(); - context.isConnectionNode = true; - actionContext = context; - } else { - // TODO: because the connection group is used as a context object and isn't serializable, - // the Group-level context menu is not currently extensible - actionContext = e.element; - } - + const actionContext = this.getActionContext(e.element); this._contextMenuService.showContextMenu({ getAnchor: () => e.anchor, - getActions: () => this._actionProvider.getActions(this._tree!, e.element!), + getActions: () => this._actionProvider.getActions(this._tree!, e.element), getKeyBinding: (action) => this._keybindingService.lookupKeybinding(action.id), onHide: (wasCancelled?: boolean) => { if (wasCancelled) { @@ -663,9 +638,56 @@ export class ServerTreeView extends Disposable implements IServerTreeView { }, getActionsContext: () => (actionContext) }); - - return true; } - return false; + } + + private async onTreeNodeDoubleClick(node: ServerTreeElement): Promise { + const action = this._actionProvider.getDefaultAction(this.tree, node); + + if (action) { + this._actionRunner.run(action, this.getActionContext(node)).catch(errors.onUnexpectedError); + } else { + // If no default action is defined, fallback to the default behavior of opening the dashboard. + // Open dashboard on double click for server and database nodes + let connectionProfile: ConnectionProfile | undefined; + if (node instanceof ConnectionProfile) { + connectionProfile = node; + await TreeUpdateUtils.connectAndCreateOeSession(connectionProfile, { + params: undefined, + saveTheConnection: true, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true, + showDashboard: true + }, this._connectionManagementService, this._objectExplorerService, this.tree); + } else if (node instanceof TreeNode) { + if (TreeUpdateUtils.isAvailableDatabaseNode(node)) { + connectionProfile = TreeUpdateUtils.getConnectionProfile(node); + this._connectionManagementService.showDashboard(connectionProfile); + } + } + } + } + + private getActionContext(element: ServerTreeElement): any { + let actionContext: any; + if (element instanceof TreeNode) { + let context = new ObjectExplorerActionsContext(); + context.nodeInfo = element.toNodeInfo(); + // Note: getting DB name before, but intentionally not using treeUpdateUtils.getConnectionProfile as it replaces + // the connection ID with a new one. This breaks a number of internal tasks + context.connectionProfile = element.getConnectionProfile()!.toIConnectionProfile(); + context.connectionProfile.databaseName = element.getDatabaseName(); + actionContext = context; + } else if (element instanceof ConnectionProfile) { + let context = new ObjectExplorerActionsContext(); + context.connectionProfile = element.toIConnectionProfile(); + context.isConnectionNode = true; + actionContext = context; + } else { + // TODO: because the connection group is used as a context object and isn't serializable, + // the Group-level context menu is not currently extensible + actionContext = element; + } + return actionContext; } } diff --git a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts index 5bf6895748..5f974894da 100644 --- a/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts +++ b/src/sql/workbench/services/objectExplorer/browser/serverTreeActionProvider.ts @@ -18,7 +18,7 @@ import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectio import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; -import { MenuId, IMenuService } from 'vs/platform/actions/common/actions'; +import { MenuId, IMenuService, MenuItemAction } from 'vs/platform/actions/common/actions'; import { ConnectionContextKey } from 'sql/workbench/services/connection/common/connectionContextKey'; import { TreeNodeContextKey } from 'sql/workbench/services/objectExplorer/common/treeNodeContextKey'; import { IQueryManagementService } from 'sql/workbench/services/query/common/queryManagement'; @@ -26,6 +26,7 @@ import { ServerInfoContextKey } from 'sql/workbench/services/connection/common/s import { fillInActions } from 'vs/platform/actions/browser/menuEntryActionViewItem'; import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; +import { ILogService } from 'vs/platform/log/common/log'; /** * Provides actions for the server tree elements @@ -38,7 +39,8 @@ export class ServerTreeActionProvider { @IQueryManagementService private _queryManagementService: IQueryManagementService, @IMenuService private menuService: IMenuService, @IContextKeyService private _contextKeyService: IContextKeyService, - @ICapabilitiesService private _capabilitiesService: ICapabilitiesService + @ICapabilitiesService private _capabilitiesService: ICapabilitiesService, + @ILogService private _logService: ILogService ) { } @@ -65,6 +67,29 @@ export class ServerTreeActionProvider { return []; } + /** + * Get the default action for the given element. + */ + public getDefaultAction(tree: AsyncServerTree | ITree, element: ServerTreeElement): IAction | undefined { + const actions = this.getActions(tree, element).filter(a => { + return a instanceof MenuItemAction && a.isDefault; + }); + if (actions.length === 1) { + return actions[0]; + } else if (actions.length > 1) { + let nodeName: string; + if (element instanceof ConnectionProfile) { + nodeName = element.serverName; + } else if (element instanceof ConnectionProfileGroup) { + nodeName = element.name; + } else { + nodeName = element.label; + } + this._logService.error(`Multiple default actions defined for node: ${nodeName}, actions: ${actions.map(a => a.id).join(', ')}`); + } + return undefined; + } + /** * Return actions for connection elements */ diff --git a/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts b/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts index c3869f8758..ab674ded3d 100644 --- a/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts +++ b/src/sql/workbench/services/objectExplorer/browser/treeSelectionHandler.ts @@ -11,9 +11,10 @@ import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/br // import { IProgressRunner, IProgressService } from 'vs/platform/progress/common/progress'; import { TreeNode } from 'sql/workbench/services/objectExplorer/common/treeNode'; import { TreeUpdateUtils } from 'sql/workbench/services/objectExplorer/browser/treeUpdateUtils'; -import { AsyncServerTree } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; +import { AsyncServerTree, ServerTreeElement } from 'sql/workbench/services/objectExplorer/browser/asyncServerTree'; import { ICapabilitiesService } from 'sql/platform/capabilities/common/capabilitiesService'; import { onUnexpectedError } from 'vs/base/common/errors'; +import { ConnectionProfileGroup } from 'sql/platform/connection/common/connectionProfileGroup'; export interface ObjectExplorerRequestStatus { inProgress: boolean; @@ -54,14 +55,14 @@ export class TreeSelectionHandler { /** * Handle selection of tree element */ - public onTreeSelect(event: any, tree: AsyncServerTree | ITree, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, capabilitiesService: ICapabilitiesService, connectionCompleteCallback: () => void) { + public onTreeSelect(event: any, tree: AsyncServerTree | ITree, connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, capabilitiesService: ICapabilitiesService, connectionCompleteCallback: () => void, doubleClickHandler: (node: ServerTreeElement) => void) { let sendSelectionEvent = ((event: any, selection: any, isDoubleClick: boolean, userInteraction: boolean, requestStatus: ObjectExplorerRequestStatus | undefined = undefined) => { // userInteraction: defensive - don't touch this something else is handling it. if (userInteraction === true && this._lastClicked && this._lastClicked[0] === selection[0]) { this._lastClicked = undefined; } if (!TreeUpdateUtils.isInDragAndDrop) { - this.handleTreeItemSelected(connectionManagementService, objectExplorerService, capabilitiesService, isDoubleClick, this.isKeyboardEvent(event), selection, tree, connectionCompleteCallback, requestStatus); + this.handleTreeItemSelected(connectionManagementService, objectExplorerService, capabilitiesService, isDoubleClick, this.isKeyboardEvent(event), selection, tree, connectionCompleteCallback, requestStatus, doubleClickHandler).catch(onUnexpectedError); } }); @@ -110,63 +111,65 @@ export class TreeSelectionHandler { * @param tree * @param connectionCompleteCallback A function that gets called after a connection is established due to the selection, if needed * @param requestStatus Used to identify if a new session should be created or not to avoid creating back to back sessions + * @param doubleClickHandler */ - private handleTreeItemSelected(connectionManagementService: IConnectionManagementService, objectExplorerService: IObjectExplorerService, capabilitiesService: ICapabilitiesService, isDoubleClick: boolean, isKeyboard: boolean, selection: any[], tree: AsyncServerTree | ITree, connectionCompleteCallback: () => void, requestStatus: ObjectExplorerRequestStatus | undefined): void { + private async handleTreeItemSelected(connectionManagementService: IConnectionManagementService, + objectExplorerService: IObjectExplorerService, + capabilitiesService: ICapabilitiesService, + isDoubleClick: boolean, + isKeyboard: boolean, + selection: any[], + tree: AsyncServerTree | ITree, + connectionCompleteCallback: () => void, + requestStatus: ObjectExplorerRequestStatus | undefined, + doubleClickHandler: (node: ServerTreeElement) => void): Promise { + if (!selection || selection.length === 0) { + return; + } + const selectedNode = selection[0]; + if (selectedNode instanceof ConnectionProfile && !capabilitiesService.getCapabilities(selectedNode.providerName)) { + connectionManagementService.handleUnsupportedProvider(selectedNode.providerName).catch(onUnexpectedError); + return; + } + if (tree instanceof AsyncServerTree) { - if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) { - if (!capabilitiesService.getCapabilities(selection[0].providerName)) { - connectionManagementService.handleUnsupportedProvider(selection[0].providerName).catch(onUnexpectedError); - return; - } + if (selectedNode instanceof ConnectionProfile) { this.onTreeActionStateChange(true); } } else { - let connectionProfile: ConnectionProfile | undefined = undefined; - let options: IConnectionCompletionOptions = { - params: undefined, - saveTheConnection: true, - showConnectionDialogOnError: true, - showFirewallRuleOnError: true, - showDashboard: isDoubleClick // only show the dashboard if the action is double click - }; - if (selection && selection.length > 0 && (selection[0] instanceof ConnectionProfile)) { - connectionProfile = selection[0]; - if (!capabilitiesService.getCapabilities(connectionProfile.providerName)) { - connectionManagementService.handleUnsupportedProvider(connectionProfile.providerName).catch(onUnexpectedError); - return; - } - - if (connectionProfile) { + if (selectedNode instanceof TreeNode || selectedNode instanceof ConnectionProfile || selectedNode instanceof ConnectionProfileGroup) { + if (isDoubleClick) { + doubleClickHandler(selectedNode); + } else if (selectedNode instanceof ConnectionProfile) { + let options: IConnectionCompletionOptions = { + params: undefined, + saveTheConnection: true, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true, + showDashboard: false + }; this.onTreeActionStateChange(true); - TreeUpdateUtils.connectAndCreateOeSession(connectionProfile, options, connectionManagementService, objectExplorerService, tree, requestStatus).then(sessionCreated => { + try { + const sessionCreated = await TreeUpdateUtils.connectAndCreateOeSession(selectedNode, options, connectionManagementService, objectExplorerService, tree, requestStatus); // Clears request status object that was created when the first timeout callback is executed. if (this._requestStatus) { this._requestStatus = undefined; } - if (!sessionCreated) { this.onTreeActionStateChange(false); } if (connectionCompleteCallback) { connectionCompleteCallback(); } - }, error => { + } catch (error) { this.onTreeActionStateChange(false); - }); - } - } else if (isDoubleClick && selection && selection.length > 0 && (selection[0] instanceof TreeNode)) { - let treeNode = selection[0]; - if (TreeUpdateUtils.isAvailableDatabaseNode(treeNode)) { - connectionProfile = TreeUpdateUtils.getConnectionProfile(treeNode); - if (connectionProfile) { - connectionManagementService.showDashboard(connectionProfile); } } } if (isKeyboard) { - tree.toggleExpansion(selection[0]); + tree.toggleExpansion(selectedNode); } } } diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 3a10d7b9bb..58f9d33fb0 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -23,6 +23,7 @@ export interface IMenuItem { when?: ContextKeyExpression; group?: 'navigation' | string; order?: number; + isDefault?: boolean; // {{SQL CARBON EDIT}} - Used by object explorer, indicating whether this is the action to be executed when a node is double clicked. } export interface ISubmenuItem { @@ -366,6 +367,8 @@ export class MenuItemAction implements IAction { readonly id: string; + isDefault: boolean = false; // {{SQL CARBON EDIT}}} - Add isDefault property to MenuItemAction. + // {{SQL CARBON EDIT}} -- remove readonly since notebook component sets these label: string; tooltip: string; diff --git a/src/vs/platform/actions/common/menuService.ts b/src/vs/platform/actions/common/menuService.ts index c6708992b9..517c88d60c 100644 --- a/src/vs/platform/actions/common/menuService.ts +++ b/src/vs/platform/actions/common/menuService.ts @@ -154,6 +154,11 @@ class Menu implements IMenu { ? new MenuItemAction(item.command, item.alt, options, this._contextKeyService, this._commandService) : new SubmenuItemAction(item, this._menuService, this._contextKeyService, options); + // {{SQL CARBON EDIT}} - Set isDefault property + if (isIMenuItem(item)) { + (action).isDefault = item.isDefault; + } + // {{SQL CARBON EDIT}} - End activeActions.push(action); } } diff --git a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts index e5562e2ac6..f03a659698 100644 --- a/src/vs/workbench/services/actions/common/menusExtensionPoint.ts +++ b/src/vs/workbench/services/actions/common/menusExtensionPoint.ts @@ -313,6 +313,7 @@ namespace schema { alt?: string; when?: string; group?: string; + isDefault?: boolean; // {{SQL CARBON EDIT}} - Used by object explorer, indicating whether this is the action to be executed when a node is double clicked. } export interface IUserFriendlySubmenuItem { @@ -824,6 +825,8 @@ menusExtensionPoint.setHandler(extensions => { } item = { command, alt, group: undefined, order: undefined, when: undefined }; + + (item).isDefault = menuItem.isDefault; // {{SQL CARBON EDIT}}} Set isDefault property. } else { if (menu.supportsSubmenus === false) { collector.error(localize('unsupported.submenureference', "Menu item references a submenu for a menu which doesn't have submenu support."));