diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 3a4375eb9f..b7187be748 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -577,9 +577,25 @@ declare module 'azdata' { url?: string; } + export interface ContextMenuColumnCellValue { + /** + * The title of the hyperlink. By default, the title is 'Show Actions' + */ + title?: string; + /** + * commands for the menu. Use an array for a group and menu separators will be added. + */ + commands: (string | string[])[]; + /** + * context that will be passed to the commands. + */ + context?: { [key: string]: string | boolean | number } | string | boolean | number | undefined + } + export enum ColumnType { icon = 3, - hyperlink = 4 + hyperlink = 4, + contextMenu = 5 } export interface TableColumn { @@ -615,6 +631,9 @@ declare module 'azdata' { action: ActionOnCellCheckboxCheck; } + export interface ContextMenuColumn extends TableColumn { + } + export interface QueryExecuteResultSetNotificationParams { /** * Contains execution plans returned by the database in ResultSets. diff --git a/src/sql/base/browser/ui/table/plugins/contextMenuColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/contextMenuColumn.plugin.ts new file mode 100644 index 0000000000..9b73c2f026 --- /dev/null +++ b/src/sql/base/browser/ui/table/plugins/contextMenuColumn.plugin.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { BaseClickableColumn, ClickableColumnOptions, IconColumnOptions } from 'sql/base/browser/ui/table/plugins/tableColumn'; +import { localize } from 'vs/nls'; + +export interface ContextMenuCellValue { + /** + * The title of the hyperlink. By default, the title is 'Show Actions' + */ + title?: string; + /** + * commands for the menu. Use an array for a group and menu separators will be added. + */ + commands: (string | string[])[]; + /** + * context that will be passed to the commands. + */ + context: { [key: string]: string | boolean | number } | string | boolean | number | undefined +} + +export interface ContextMenuColumnOptions extends IconColumnOptions, ClickableColumnOptions { +} + +export class ContextMenuColumn extends BaseClickableColumn { + constructor(private options: ContextMenuColumnOptions) { + super(options); + } + + public get definition(): Slick.Column { + return { + id: this.options.id || this.options.title || this.options.field, + width: this.options.width ?? 26, + formatter: (row: number, cell: number, value: any, columnDef: Slick.Column, dataContext: T): + string => { + const escapedTitle = escape(this.options.title ?? localize('table.showActions', "Show Actions")); + return ` + + `; + }, + name: this.options.name, + resizable: this.options.resizable, + selectable: false, + focusable: true + }; + } +} diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 3ad5ad8d76..003d03c10e 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -903,7 +903,8 @@ export enum ColumnType { checkBox = 1, button = 2, icon = 3, - hyperlink = 4 + hyperlink = 4, + contextMenu = 5 } export enum ActionOnCellCheckboxCheck { diff --git a/src/sql/workbench/browser/modelComponents/media/table.css b/src/sql/workbench/browser/modelComponents/media/table.css index c04a083d45..375d9b8f9c 100644 --- a/src/sql/workbench/browser/modelComponents/media/table.css +++ b/src/sql/workbench/browser/modelComponents/media/table.css @@ -21,3 +21,11 @@ .display-none { display: none; } + +modelview-table .context-menu-button { + border-width: 0px; + height: 16px; + width: 26px; + vertical-align: middle; + cursor: pointer; +} diff --git a/src/sql/workbench/browser/modelComponents/table.component.ts b/src/sql/workbench/browser/modelComponents/table.component.ts index 4514f225f5..a42795cbe1 100644 --- a/src/sql/workbench/browser/modelComponents/table.component.ts +++ b/src/sql/workbench/browser/modelComponents/table.component.ts @@ -34,7 +34,11 @@ import { onUnexpectedError } from 'vs/base/common/errors'; import { ILogService } from 'vs/platform/log/common/log'; import { TableCellClickEventArgs } from 'sql/base/browser/ui/table/plugins/tableColumn'; import { HyperlinkCellValue, HyperlinkColumn } from 'sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin'; -import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { IContextMenuService, IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ContextMenuColumn, ContextMenuCellValue } from 'sql/base/browser/ui/table/plugins/contextMenuColumn.plugin'; +import { IAction, Separator } from 'vs/base/common/actions'; +import { MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; export enum ColumnSizingMode { ForceFit = 0, // all columns will be sized to fit in viewable space, no horiz scroll bar @@ -47,11 +51,12 @@ enum ColumnType { checkBox = 1, button = 2, icon = 3, - hyperlink = 4 + hyperlink = 4, + contextMenu = 5 } -type TableCellInputDataType = string | azdata.IconColumnCellValue | azdata.ButtonColumnCellValue | azdata.HyperlinkColumnCellValue | undefined; -type TableCellDataType = string | CssIconCellValue | ButtonCellValue | HyperlinkCellValue | undefined; +type TableCellInputDataType = string | azdata.IconColumnCellValue | azdata.ButtonColumnCellValue | azdata.HyperlinkColumnCellValue | azdata.ContextMenuColumnCellValue | undefined; +type TableCellDataType = string | CssIconCellValue | ButtonCellValue | HyperlinkCellValue | ContextMenuCellValue | undefined; @Component({ selector: 'modelview-table', @@ -68,6 +73,7 @@ export default class TableComponent extends ComponentBase[] = []; private _buttonColumns: ButtonColumn<{}>[] = []; private _hyperlinkColumns: HyperlinkColumn<{}>[] = []; + private _contextMenuColumns: ContextMenuColumn<{}>[] = []; private _pluginsRegisterStatus: boolean[] = []; private _filterPlugin: HeaderFilter; private _onCheckBoxChanged = new Emitter(); @@ -82,7 +88,10 @@ export default class TableComponent extends ComponentBase ElementRef)) el: ElementRef, @Inject(ILogService) logService: ILogService, - @Inject(IContextViewService) private contextViewService: IContextViewService) { + @Inject(IContextViewService) private contextViewService: IContextViewService, + @Inject(IContextMenuService) private contextMenuService: IContextMenuService, + @Inject(IInstantiationService) private instantiationService: IInstantiationService + ) { super(changeRef, el, logService); } @@ -101,6 +110,8 @@ export default class TableComponent extends ComponentBaseval; + cellValue = { + title: contextMenuValue.title, + commands: contextMenuValue.commands, + context: contextMenuValue.context + }; + } + break; default: cellValue = val; } @@ -355,6 +376,7 @@ export default class TableComponent extends ComponentBase this.registerPlugins(col, this._checkboxColumns[col])); Object.keys(this._buttonColumns).forEach(col => this.registerPlugins(col, this._buttonColumns[col])); Object.keys(this._hyperlinkColumns).forEach(col => this.registerPlugins(col, this._hyperlinkColumns[col])); + Object.keys(this._contextMenuColumns).forEach(col => this.registerPlugins(col, this._contextMenuColumns[col])); if (this.headerFilter === true) { this.registerFilterPlugin(); @@ -483,7 +505,62 @@ export default class TableComponent extends ComponentBase | ButtonColumn<{}> | HyperlinkColumn<{}>): void { + + private createContextMenuButtonPlugin(col: azdata.ContextMenuColumn) { + if (!this._contextMenuColumns[col.value]) { + this._contextMenuColumns[col.value] = new ContextMenuColumn({ + title: col.value, + width: col.width, + field: col.value, + name: col.name, + resizable: col.resizable + }); + } + + this._register( + this._contextMenuColumns[col.value].onClick((state) => { + const cellValue = state.item[col.value]; + const actions: IAction[] = []; + cellValue.commands.forEach((c, i) => { + if (typeof c === 'string') { + actions.push(this.createMenuItem(c)); + } else { + if (actions.length !== 0) { + actions.push(new Separator()); + } + actions.push(...c.map(cmd => { + return this.createMenuItem(cmd); + })); + if (i !== cellValue.commands.length - 1) { + actions.push(new Separator()); + } + } + }); + + this.contextMenuService.showContextMenu({ + getAnchor: () => { + return { + x: state.position.x, + y: state.position.y + }; + }, + getActions: () => actions, + getActionsContext: () => cellValue.context, + onHide: () => { + this.focus(); + } + }); + }) + ); + } + + private createMenuItem(commandId: string): MenuItemAction { + const command = MenuRegistry.getCommand(commandId); + return this.instantiationService.createInstance(MenuItemAction, command, undefined, { shouldForwardArgs: true }); + } + + + private registerPlugins(col: string, plugin: CheckboxSelectColumn<{}> | ButtonColumn<{}> | HyperlinkColumn<{}> | ContextMenuColumn<{}>): void { const index = 'index' in plugin ? plugin.index : this.columns?.findIndex(x => x === col || ('value' in x && x['value'] === col)); if (index >= 0) { diff --git a/src/sql/workbench/test/electron-browser/modalComponents/table.component.test.ts b/src/sql/workbench/test/electron-browser/modalComponents/table.component.test.ts index ea4ff86f75..3d2fdf9bf7 100644 --- a/src/sql/workbench/test/electron-browser/modalComponents/table.component.test.ts +++ b/src/sql/workbench/test/electron-browser/modalComponents/table.component.test.ts @@ -19,7 +19,7 @@ suite('TableComponent Tests', () => { ['4', '5', '6'] ]; let columns = ['c1', 'c2', 'c3']; - const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined); + const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined, undefined, undefined); let actual = tableComponent.transformData(data, columns); let expected: { [key: string]: string }[] = [ @@ -39,7 +39,7 @@ suite('TableComponent Tests', () => { test('Table transformData should return empty array given undefined rows', () => { let data = undefined; - const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined); + const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined, undefined, undefined); let columns = ['c1', 'c2', 'c3']; let actual = tableComponent.transformData(data, columns); let expected: { [key: string]: string }[] = []; @@ -52,7 +52,7 @@ suite('TableComponent Tests', () => { ['4', '5', '6'] ]; let columns; - const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined); + const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined, undefined, undefined); let actual = tableComponent.transformData(data, columns); let expected: { [key: string]: string }[] = []; assert.deepStrictEqual(actual, expected); @@ -63,7 +63,7 @@ suite('TableComponent Tests', () => { ['1', '2'], ['4', '5'] ]; - const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined); + const tableComponent = new TableComponent(undefined, undefined, undefined, new NullLogService(), undefined, undefined, undefined); let columns = ['c1', 'c2', 'c3']; let actual = tableComponent.transformData(data, columns); let expected: { [key: string]: string }[] = [