mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-16 17:22:29 -05:00
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:
30
src/sql/azdata.proposed.d.ts
vendored
30
src/sql/azdata.proposed.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user