mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -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
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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];
|
||||||
|
|||||||
@@ -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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user