mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
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:
21
src/sql/azdata.proposed.d.ts
vendored
21
src/sql/azdata.proposed.d.ts
vendored
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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 }[] = [
|
||||||
|
|||||||
Reference in New Issue
Block a user