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 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 * 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 * 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; 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 namespace DBCellValue {
export function isDBCellValue(object: any): boolean { export function isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined); 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 * 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 { } else {
cellClasses += ' missing-value'; cellClasses += ' missing-value';
} }
} else if (isHyperlinkCellValue(value)) {
return `<a class="${cellClasses}" href="#" >${escape(value.displayText)}</a>`;
} }
return `<span title="${valueToDisplay}" class="${cellClasses}">${valueToDisplay}</span>`; 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. * 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. * 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; } { export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any): TextCellValue | HyperlinkCellValue {
let displayValue = value[columnDef.field]; let fieldValue = value[columnDef.field];
return { if (columnDef.type === 'hyperlink') {
text: displayValue, return <HyperlinkCellValue>{
ariaLabel: displayValue ? escape(displayValue) : displayValue 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 * 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 * 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; } { export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: any): { text: string; ariaLabel: string; } {
let displayValue = value[columnDef.field]; 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> { export interface ColumnDefinition extends Slick.Column<Slick.SlickData> {
name: string; name: string;
type: string;
filterable?: boolean; filterable?: boolean;
} }
@@ -69,8 +70,8 @@ export class ResourceViewerInput extends EditorInput {
]); ]);
} }
private fetchColumns(): void { private async fetchColumns(): Promise<void> {
this._dataGridProvider.getDataGridColumns(this._providerId).then(columns => { const columns = await this._dataGridProvider.getDataGridColumns(this._providerId);
this.columns = columns.map(col => { this.columns = columns.map(col => {
return { return {
name: col.name, name: col.name,
@@ -81,16 +82,15 @@ export class ResourceViewerInput extends EditorInput {
filterable: col.filterable ?? true, filterable: col.filterable ?? true,
resizable: col.resizable ?? true, resizable: col.resizable ?? true,
tooltip: col.tooltip, tooltip: col.tooltip,
width: col.width width: col.width,
type: col.type
}; };
}); });
}).catch(err => onUnexpectedError(err));
} }
private fetchItems(): void { private async fetchItems(): Promise<void> {
this._dataGridProvider.getDataGridItems(this._providerId).then(items => { const items = await this._dataGridProvider.getDataGridItems(this._providerId);
this._data = items; this._data = items;
this._onDataChanged.fire(); this._onDataChanged.fire();
}).catch(err => onUnexpectedError(err));
} }
} }

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 { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; 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 { HeaderFilter, CommandEventArgs, IExtendedColumn } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
import { Disposable } from 'vs/base/common/lifecycle'; import { Disposable } from 'vs/base/common/lifecycle';
import { TableDataView } from 'sql/base/browser/ui/table/tableDataView'; 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 { export class ResourceViewerTable extends Disposable {
private _resourceViewerTable!: Table<Slick.SlickData>; private _resourceViewerTable!: Table<Slick.SlickData>;
private _dataView: TableDataView<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(); super();
let filterFn = (data: Array<Slick.SlickData>): Array<Slick.SlickData> => { let filterFn = (data: Array<Slick.SlickData>): Array<Slick.SlickData> => {
return data.filter(item => this.filter(item)); return data.filter(item => this.filter(item));
@@ -38,6 +48,8 @@ export class ResourceViewerTable extends Disposable {
let filterPlugin = new HeaderFilter<Slick.SlickData>(); let filterPlugin = new HeaderFilter<Slick.SlickData>();
this._register(attachButtonStyler(filterPlugin, this._themeService)); this._register(attachButtonStyler(filterPlugin, this._themeService));
this._register(attachTableStyler(this._resourceViewerTable, this._themeService)); this._register(attachTableStyler(this._resourceViewerTable, this._themeService));
this._register(this._resourceViewerTable.onClick(this.onTableClick, this));
filterPlugin.onFilterApplied.subscribe(() => { filterPlugin.onFilterApplied.subscribe(() => {
this._dataView.filter(); this._dataView.filter();
this._resourceViewerTable.grid.invalidate(); this._resourceViewerTable.grid.invalidate();
@@ -93,4 +105,28 @@ export class ResourceViewerTable extends Disposable {
} }
return value; 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));
}
}
}
}
}
} }