Add hyperlink support to DataGrid columns (#13061)

* Add hyperlink support to DataGrid columns

* pr feedback

* Remove unused aria label

* fix error message display

* fix compile
This commit is contained in:
Charles Gagnon
2020-10-26 08:43:09 -07:00
committed by GitHub
parent 3ad39bd0d3
commit ff45bdd072
4 changed files with 137 additions and 31 deletions

View File

@@ -182,6 +182,34 @@ declare module 'azdata' {
width?: number
}
/**
* Info for a command to execute
*/
export interface ExecuteCommandInfo {
/**
* The ID of the command to execute
*/
id: string;
/**
* The optional args to pass to the command
*/
args?: string[];
}
/**
* Info for displaying a hyperlink value in a Data Grid table
*/
export interface DataGridHyperlinkInfo {
/**
* The text to display for the link
*/
displayText: string;
/**
* The URL to open or command to execute
*/
linkOrCommand: string | ExecuteCommandInfo;
}
/**
* An item for displaying in a data grid
*/
@@ -197,7 +225,7 @@ declare module 'azdata' {
/**
* The other properties that will be displayed in the grid
*/
[key: string]: any;
[key: string]: string | DataGridHyperlinkInfo;
}
/**

View File

@@ -11,12 +11,44 @@ export interface DBCellValue {
isNull: boolean;
}
/**
* Info for executing a command. @see azdata.ExecuteCommandInfo
*/
export interface ExecuteCommandInfo {
id: string;
args?: string[]
}
/**
* The info for a DataGrid Text Cell.
*/
export interface TextCellValue {
text: string;
ariaLabel: string;
}
/**
* The info for a DataGrid Hyperlink Cell.
*/
export interface HyperlinkCellValue {
displayText: string;
linkOrCommand: string | ExecuteCommandInfo;
}
export namespace DBCellValue {
export function isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
}
}
/**
* Checks whether the specified object is a HyperlinkCellValue object or not
* @param obj The object to test
*/
export function isHyperlinkCellValue(obj: any | undefined): obj is HyperlinkCellValue {
return !!(<HyperlinkCellValue>obj)?.linkOrCommand;
}
/**
* Format xml field into a hyperlink and performs HTML entity encoding
@@ -34,6 +66,8 @@ export function hyperLinkFormatter(row: number | undefined, cell: any | undefine
} else {
cellClasses += ' missing-value';
}
} else if (isHyperlinkCellValue(value)) {
return `<a class="${cellClasses}" href="#" >${escape(value.displayText)}</a>`;
}
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`;
}
@@ -76,18 +110,26 @@ export function imageFormatter(row: number | undefined, cell: any | undefined, v
* Provide slick grid cell with encoded ariaLabel and plain text.
* text will be escaped by the textFormatter and ariaLabel will be consumed by slickgrid directly.
*/
export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any): { text: string; ariaLabel: string; } {
let displayValue = value[columnDef.field];
return {
text: displayValue,
ariaLabel: displayValue ? escape(displayValue) : displayValue
};
export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any): TextCellValue | HyperlinkCellValue {
let fieldValue = value[columnDef.field];
if (columnDef.type === 'hyperlink') {
return <HyperlinkCellValue>{
displayText: fieldValue.displayText,
linkOrCommand: fieldValue.linkOrCommand
};
} else {
return <TextCellValue>{
text: fieldValue,
ariaLabel: fieldValue ? escape(fieldValue) : fieldValue
};
}
}
/**
* Alternate function to provide slick grid cell with ariaLabel and plain text
* In this case, for no display value ariaLabel will be set to specific string "no data available" for accessibily support for screen readers
* Set 'no data' lable only if cell is present and has no value (so that checkbox and other custom plugins do not get 'no data' label)
* Set 'no data' label only if cell is present and has no value (so that checkbox and other custom plugins do not get 'no data' label)
*/
export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: any): { text: string; ariaLabel: string; } {
let displayValue = value[columnDef.field];

View File

@@ -13,6 +13,7 @@ import { getDataGridFormatter } from 'sql/workbench/services/dataGridProvider/br
export interface ColumnDefinition extends Slick.Column<Slick.SlickData> {
name: string;
type: string;
filterable?: boolean;
}
@@ -69,28 +70,27 @@ export class ResourceViewerInput extends EditorInput {
]);
}
private fetchColumns(): void {
this._dataGridProvider.getDataGridColumns(this._providerId).then(columns => {
this.columns = columns.map(col => {
return {
name: col.name,
field: col.field,
id: col.id,
formatter: getDataGridFormatter(col.type),
sortable: col.sortable ?? true,
filterable: col.filterable ?? true,
resizable: col.resizable ?? true,
tooltip: col.tooltip,
width: col.width
};
});
}).catch(err => onUnexpectedError(err));
private async fetchColumns(): Promise<void> {
const columns = await this._dataGridProvider.getDataGridColumns(this._providerId);
this.columns = columns.map(col => {
return {
name: col.name,
field: col.field,
id: col.id,
formatter: getDataGridFormatter(col.type),
sortable: col.sortable ?? true,
filterable: col.filterable ?? true,
resizable: col.resizable ?? true,
tooltip: col.tooltip,
width: col.width,
type: col.type
};
});
}
private fetchItems(): void {
this._dataGridProvider.getDataGridItems(this._providerId).then(items => {
this._data = items;
this._onDataChanged.fire();
}).catch(err => onUnexpectedError(err));
private async fetchItems(): Promise<void> {
const items = await this._dataGridProvider.getDataGridItems(this._providerId);
this._data = items;
this._onDataChanged.fire();
}
}

View File

@@ -9,17 +9,27 @@ import { attachTableStyler, attachButtonStyler } from 'sql/platform/theme/common
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService';
import { slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters';
import { isHyperlinkCellValue, slickGridDataItemColumnValueExtractor } from 'sql/base/browser/ui/table/formatters';
import { HeaderFilter, CommandEventArgs, IExtendedColumn } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { Disposable } from 'vs/base/common/lifecycle';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView';
import { ITableMouseEvent } from 'sql/base/browser/ui/table/interfaces';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import { isString } from 'vs/base/common/types';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { localize } from 'vs/nls';
import { INotificationService } from 'vs/platform/notification/common/notification';
export class ResourceViewerTable extends Disposable {
private _resourceViewerTable!: Table<Slick.SlickData>;
private _dataView: TableDataView<Slick.SlickData>;
constructor(parent: HTMLElement, @IWorkbenchThemeService private _themeService: IWorkbenchThemeService) {
constructor(parent: HTMLElement,
@IWorkbenchThemeService private _themeService: IWorkbenchThemeService,
@IOpenerService private _openerService: IOpenerService,
@ICommandService private _commandService: ICommandService,
@INotificationService private _notificationService: INotificationService) {
super();
let filterFn = (data: Array<Slick.SlickData>): Array<Slick.SlickData> => {
return data.filter(item => this.filter(item));
@@ -38,6 +48,8 @@ export class ResourceViewerTable extends Disposable {
let filterPlugin = new HeaderFilter<Slick.SlickData>();
this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableStyler(this._resourceViewerTable, this._themeService));
this._register(this._resourceViewerTable.onClick(this.onTableClick, this));
filterPlugin.onFilterApplied.subscribe(() => {
this._dataView.filter();
this._resourceViewerTable.grid.invalidate();
@@ -93,4 +105,28 @@ export class ResourceViewerTable extends Disposable {
}
return value;
}
private async onTableClick(event: ITableMouseEvent): Promise<void> {
const column = this._resourceViewerTable.columns[event.cell.cell];
if (column) {
const row = this._dataView.getItem(event.cell.row);
const value = row[column.field];
if (isHyperlinkCellValue(value)) {
if (isString(value.linkOrCommand)) {
try {
await this._openerService.open(value.linkOrCommand);
} catch (err) {
this._notificationService.error(localize('resourceViewerTable.openError', "Error opening link : {0}", err.message ?? err));
}
} else {
try {
await this._commandService.executeCommand(value.linkOrCommand.id, ...(value.linkOrCommand.args ?? []));
} catch (err) {
this._notificationService.error(localize('resourceViewerTable.commandError', "Error executing command '{0}' : {1}", value.linkOrCommand.id, err.message ?? err));
}
}
}
}
}
}