Adding context menu to table component (#18914)

* Adding context menu to table component

* Removing extra whitespace

* Some logic fixes

* Fixing focus logic
This commit is contained in:
Aasim Khan
2022-04-04 23:47:39 -07:00
committed by GitHub
parent b96fb5aa90
commit 4db0814b94
6 changed files with 167 additions and 12 deletions

View File

@@ -577,9 +577,25 @@ declare module 'azdata' {
url?: string; 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 { export enum ColumnType {
icon = 3, icon = 3,
hyperlink = 4 hyperlink = 4,
contextMenu = 5
} }
export interface TableColumn { export interface TableColumn {
@@ -615,6 +631,9 @@ declare module 'azdata' {
action: ActionOnCellCheckboxCheck; action: ActionOnCellCheckboxCheck;
} }
export interface ContextMenuColumn extends TableColumn {
}
export interface QueryExecuteResultSetNotificationParams { export interface QueryExecuteResultSetNotificationParams {
/** /**
* Contains execution plans returned by the database in ResultSets. * Contains execution plans returned by the database in ResultSets.

View File

@@ -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<T extends Slick.SlickData> extends BaseClickableColumn<T> {
constructor(private options: ContextMenuColumnOptions) {
super(options);
}
public get definition(): Slick.Column<T> {
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<T>, dataContext: T):
string => {
const escapedTitle = escape(this.options.title ?? localize('table.showActions', "Show Actions"));
return `
<button tabIndex=0 title="${escapedTitle}" aria-label="${escapedTitle}" class="codicon toggle-more context-menu-button">
</button>
`;
},
name: this.options.name,
resizable: this.options.resizable,
selectable: false,
focusable: true
};
}
}

View File

@@ -903,7 +903,8 @@ export enum ColumnType {
checkBox = 1, checkBox = 1,
button = 2, button = 2,
icon = 3, icon = 3,
hyperlink = 4 hyperlink = 4,
contextMenu = 5
} }
export enum ActionOnCellCheckboxCheck { export enum ActionOnCellCheckboxCheck {

View File

@@ -21,3 +21,11 @@
.display-none { .display-none {
display: none; display: none;
} }
modelview-table .context-menu-button {
border-width: 0px;
height: 16px;
width: 26px;
vertical-align: middle;
cursor: pointer;
}

View File

@@ -34,7 +34,11 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { ILogService } from 'vs/platform/log/common/log'; import { ILogService } from 'vs/platform/log/common/log';
import { TableCellClickEventArgs } from 'sql/base/browser/ui/table/plugins/tableColumn'; import { TableCellClickEventArgs } from 'sql/base/browser/ui/table/plugins/tableColumn';
import { HyperlinkCellValue, HyperlinkColumn } from 'sql/base/browser/ui/table/plugins/hyperlinkColumn.plugin'; 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 { export enum ColumnSizingMode {
ForceFit = 0, // all columns will be sized to fit in viewable space, no horiz scroll bar ForceFit = 0, // all columns will be sized to fit in viewable space, no horiz scroll bar
@@ -47,11 +51,12 @@ enum ColumnType {
checkBox = 1, checkBox = 1,
button = 2, button = 2,
icon = 3, icon = 3,
hyperlink = 4 hyperlink = 4,
contextMenu = 5
} }
type TableCellInputDataType = string | azdata.IconColumnCellValue | azdata.ButtonColumnCellValue | azdata.HyperlinkColumnCellValue | undefined; type TableCellInputDataType = string | azdata.IconColumnCellValue | azdata.ButtonColumnCellValue | azdata.HyperlinkColumnCellValue | azdata.ContextMenuColumnCellValue | undefined;
type TableCellDataType = string | CssIconCellValue | ButtonCellValue | HyperlinkCellValue | undefined; type TableCellDataType = string | CssIconCellValue | ButtonCellValue | HyperlinkCellValue | ContextMenuCellValue | undefined;
@Component({ @Component({
selector: 'modelview-table', selector: 'modelview-table',
@@ -68,6 +73,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
private _checkboxColumns: CheckboxSelectColumn<{}>[] = []; private _checkboxColumns: CheckboxSelectColumn<{}>[] = [];
private _buttonColumns: ButtonColumn<{}>[] = []; private _buttonColumns: ButtonColumn<{}>[] = [];
private _hyperlinkColumns: HyperlinkColumn<{}>[] = []; private _hyperlinkColumns: HyperlinkColumn<{}>[] = [];
private _contextMenuColumns: ContextMenuColumn<{}>[] = [];
private _pluginsRegisterStatus: boolean[] = []; private _pluginsRegisterStatus: boolean[] = [];
private _filterPlugin: HeaderFilter<Slick.SlickData>; private _filterPlugin: HeaderFilter<Slick.SlickData>;
private _onCheckBoxChanged = new Emitter<ICheckboxCellActionEventArgs>(); private _onCheckBoxChanged = new Emitter<ICheckboxCellActionEventArgs>();
@@ -82,7 +88,10 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
@Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService,
@Inject(forwardRef(() => ElementRef)) el: ElementRef, @Inject(forwardRef(() => ElementRef)) el: ElementRef,
@Inject(ILogService) logService: ILogService, @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); super(changeRef, el, logService);
} }
@@ -101,6 +110,8 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
mycolumns.push(TableComponent.createIconColumn(col)); mycolumns.push(TableComponent.createIconColumn(col));
} else if (col.type === ColumnType.hyperlink) { } else if (col.type === ColumnType.hyperlink) {
this.createHyperlinkPlugin(col); this.createHyperlinkPlugin(col);
} else if (col.type === ColumnType.contextMenu) {
this.createContextMenuButtonPlugin(col);
} }
else if (col.value) { else if (col.value) {
mycolumns.push(TableComponent.createTextColumn(col as azdata.TableColumn)); mycolumns.push(TableComponent.createTextColumn(col as azdata.TableColumn));
@@ -197,6 +208,16 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
break; break;
} }
break; break;
case ColumnType.contextMenu:
if (val) {
const contextMenuValue = <azdata.ContextMenuColumnCellValue>val;
cellValue = <ContextMenuCellValue>{
title: contextMenuValue.title,
commands: contextMenuValue.commands,
context: contextMenuValue.context
};
}
break;
default: default:
cellValue = val; cellValue = val;
} }
@@ -355,6 +376,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
Object.keys(this._checkboxColumns).forEach(col => this.registerPlugins(col, this._checkboxColumns[col])); Object.keys(this._checkboxColumns).forEach(col => this.registerPlugins(col, this._checkboxColumns[col]));
Object.keys(this._buttonColumns).forEach(col => this.registerPlugins(col, this._buttonColumns[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._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) { if (this.headerFilter === true) {
this.registerFilterPlugin(); this.registerFilterPlugin();
@@ -483,7 +505,62 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
} }
} }
private registerPlugins(col: string, plugin: CheckboxSelectColumn<{}> | 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)); const index = 'index' in plugin ? plugin.index : this.columns?.findIndex(x => x === col || ('value' in x && x['value'] === col));
if (index >= 0) { if (index >= 0) {

View File

@@ -19,7 +19,7 @@ suite('TableComponent Tests', () => {
['4', '5', '6'] ['4', '5', '6']
]; ];
let columns = ['c1', 'c2', 'c3']; 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 actual = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [ let expected: { [key: string]: string }[] = [
@@ -39,7 +39,7 @@ suite('TableComponent Tests', () => {
test('Table transformData should return empty array given undefined rows', () => { test('Table transformData should return empty array given undefined rows', () => {
let data = undefined; 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 columns = ['c1', 'c2', 'c3'];
let actual = tableComponent.transformData(data, columns); let actual = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = []; let expected: { [key: string]: string }[] = [];
@@ -52,7 +52,7 @@ suite('TableComponent Tests', () => {
['4', '5', '6'] ['4', '5', '6']
]; ];
let columns; 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 actual = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = []; let expected: { [key: string]: string }[] = [];
assert.deepStrictEqual(actual, expected); assert.deepStrictEqual(actual, expected);
@@ -63,7 +63,7 @@ suite('TableComponent Tests', () => {
['1', '2'], ['1', '2'],
['4', '5'] ['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 columns = ['c1', 'c2', 'c3'];
let actual = tableComponent.transformData(data, columns); let actual = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [ let expected: { [key: string]: string }[] = [