From ff45bdd0725bfb690f17cfd10fbbf7a4e6949148 Mon Sep 17 00:00:00 2001 From: Charles Gagnon Date: Mon, 26 Oct 2020 08:43:09 -0700 Subject: [PATCH] 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 --- src/sql/azdata.proposed.d.ts | 30 +++++++++- src/sql/base/browser/ui/table/formatters.ts | 56 ++++++++++++++++--- .../resourceViewer/resourceViewerInput.ts | 42 +++++++------- .../browser/resourceViewerTable.ts | 40 ++++++++++++- 4 files changed, 137 insertions(+), 31 deletions(-) diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 881a735417..f2f285c698 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -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; } /** diff --git a/src/sql/base/browser/ui/table/formatters.ts b/src/sql/base/browser/ui/table/formatters.ts index 1c7f813430..dae06c58c7 100644 --- a/src/sql/base/browser/ui/table/formatters.ts +++ b/src/sql/base/browser/ui/table/formatters.ts @@ -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 !!(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 `${escape(value.displayText)}`; } return `${valueToDisplay}`; } @@ -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 { + displayText: fieldValue.displayText, + linkOrCommand: fieldValue.linkOrCommand + }; + } else { + return { + 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]; diff --git a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts index 609765d307..0f2b2a1208 100644 --- a/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts +++ b/src/sql/workbench/browser/editor/resourceViewer/resourceViewerInput.ts @@ -13,6 +13,7 @@ import { getDataGridFormatter } from 'sql/workbench/services/dataGridProvider/br export interface ColumnDefinition extends Slick.Column { 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 { + 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 { + const items = await this._dataGridProvider.getDataGridItems(this._providerId); + this._data = items; + this._onDataChanged.fire(); } } diff --git a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts index fa108c4b20..bcb5635594 100644 --- a/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts +++ b/src/sql/workbench/contrib/resourceViewer/browser/resourceViewerTable.ts @@ -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; private _dataView: TableDataView; - 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): Array => { return data.filter(item => this.filter(item)); @@ -38,6 +48,8 @@ export class ResourceViewerTable extends Disposable { let filterPlugin = new HeaderFilter(); 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 { + 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)); + } + } + } + } + } }