table column with iconcss (#13056)

This commit is contained in:
Vladimir Chernov
2020-10-31 02:23:38 +03:00
committed by GitHub
parent 2f571d868b
commit da6f800f11
11 changed files with 168 additions and 41 deletions

View File

@@ -819,10 +819,25 @@ declare module 'azdata' {
}
export interface TableComponent {
/**
* Append data to an exsiting table data.
*/
appendData(data: any[][]);
}
export interface IconColumnCellValue {
icon: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri };
ariaLabel: string;
}
export enum ColumnType {
icon = 3
}
export interface TableColumn {
/**
* The text to display on the column heading. 'value' property will be used, if not specified
**/
name?: string;
}
}

View File

@@ -36,6 +36,12 @@ export interface HyperlinkCellValue {
linkOrCommand: string | ExecuteCommandInfo;
}
export interface CssIconCellValue {
iconCssClass: string,
ariaLabel: string
}
export namespace DBCellValue {
export function isDBCellValue(object: any): boolean {
return (object !== undefined && object.displayValue !== undefined && object.isNull !== undefined);
@@ -50,6 +56,10 @@ export function isHyperlinkCellValue(obj: any | undefined): obj is HyperlinkCell
return !!(<HyperlinkCellValue>obj)?.linkOrCommand;
}
export function isCssIconCellValue(obj: any | undefined): obj is CssIconCellValue {
return !!(<CssIconCellValue>obj)?.iconCssClass;
}
/**
* Format xml field into a hyperlink and performs HTML entity encoding
*/
@@ -102,6 +112,14 @@ export function textFormatter(row: number | undefined, cell: any | undefined, va
return `<span title="${titleValue}" class="${cellClasses}">${valueToDisplay}</span>`;
}
export function iconCssFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string {
if (isCssIconCellValue(value)) {
return `<div role='image' aria-label="${escape(value.ariaLabel)}" class="grid-cell-value-container icon codicon slick-icon-cell-content ${value.iconCssClass}"></div>`;
}
return textFormatter(row, cell, value, columnDef, dataContext);
}
export function imageFormatter(row: number | undefined, cell: any | undefined, value: any, columnDef: any | undefined, dataContext: any | undefined): string {
return `<img src="${value.text}" />`;
}
@@ -129,7 +147,7 @@ export function slickGridDataItemColumnValueExtractor(value: any, columnDef: any
* 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' 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; } | CssIconCellValue {
let displayValue = value[columnDef.field];
if (typeof displayValue === 'number') {
displayValue = displayValue.toString();
@@ -137,6 +155,11 @@ export function slickGridDataItemColumnValueWithNoData(value: any, columnDef: an
if (displayValue instanceof Array) {
displayValue = displayValue.toString();
}
if (isCssIconCellValue(displayValue)) {
return displayValue;
}
return {
text: displayValue,
ariaLabel: displayValue ? escape(displayValue) : ((displayValue !== undefined) ? localize("tableCell.NoDataAvailable", "no data available") : displayValue)

View File

@@ -12,29 +12,32 @@ export interface TextWithIconColumnDefinition<T extends Slick.SlickData> extends
iconCssClassField?: string;
}
export interface TextWithIconColumnOptions {
export interface TextWithIconColumnOptions<T extends Slick.SlickData> {
iconCssClassField?: string;
field?: string;
width?: number;
id?: string;
resizable?: boolean;
name?: string;
headerCssClass?: string;
formatter?: Slick.Formatter<T>
}
export class TextWithIconColumn<T extends Slick.SlickData> {
private _definition: TextWithIconColumnDefinition<T>;
constructor(options: TextWithIconColumnOptions) {
constructor(options: TextWithIconColumnOptions<T>) {
this._definition = {
id: options.id,
field: options.field,
resizable: options.resizable,
formatter: this.formatter,
formatter: options.formatter ?? this.formatter,
width: options.width,
name: options.name,
iconCssClassField: options.iconCssClassField,
cssClass: 'slick-icon-cell'
cssClass: 'slick-icon-cell',
headerCssClass: options.headerCssClass
};
}
private formatter(row: number, cell: number, value: any, columnDef: Slick.Column<T>, dataContext: T): string {

View File

@@ -811,7 +811,8 @@ export enum SchemaObjectType {
export enum ColumnType {
text = 0,
checkBox = 1,
button = 2
button = 2,
icon = 3
}
export enum ActionOnCellCheckboxCheck {

View File

@@ -52,3 +52,7 @@ function getIconUri(iconPath: string | URI): URI {
return URI.revive(iconPath);
}
}
export function getIconKey(iconPath: IUserFriendlyIcon): string {
return getLightIconUri(iconPath).toString(true) + getDarkIconUri(iconPath)?.toString(true);
}

View File

@@ -23,12 +23,12 @@ import { CheckboxSelectColumn, ICheckboxCellActionEventArgs } from 'sql/base/bro
import { Emitter, Event as vsEvent } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
import { slickGridDataItemColumnValueWithNoData, textFormatter } from 'sql/base/browser/ui/table/formatters';
import { slickGridDataItemColumnValueWithNoData, textFormatter, iconCssFormatter, CssIconCellValue } from 'sql/base/browser/ui/table/formatters';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType, ModelViewAction } from 'sql/platform/dashboard/browser/interfaces';
import { convertSizeToNumber } from 'sql/base/browser/dom';
import { ButtonColumn, ButtonClickEventArgs } from 'sql/base/browser/ui/table/plugins/buttonColumn.plugin';
import { createIconCssClass } from 'sql/workbench/browser/modelComponents/iconUtils';
import { IUserFriendlyIcon, createIconCssClass, getIconKey } from 'sql/workbench/browser/modelComponents/iconUtils';
import { HeaderFilter } from 'sql/base/browser/ui/table/plugins/headerFilter.plugin';
export enum ColumnSizingMode {
@@ -37,6 +37,13 @@ export enum ColumnSizingMode {
DataFit = 2 // columns use sizing based on cell data, horiz scroll bar present if more cells than visible in view area
}
enum ColumnType {
text = 0,
checkBox = 1,
button = 2,
icon = 3
}
@Component({
selector: 'modelview-table',
template: `
@@ -57,6 +64,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
private _onButtonClicked = new Emitter<ButtonClickEventArgs<{}>>();
public readonly onCheckBoxChanged: vsEvent<ICheckboxCellActionEventArgs> = this._onCheckBoxChanged.event;
public readonly onButtonClicked: vsEvent<ButtonClickEventArgs<{}>> = this._onButtonClicked.event;
private _iconCssMap: { [iconKey: string]: string } = {};
@ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef;
constructor(
@@ -76,24 +84,17 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
if (tableColumns) {
let mycolumns: Slick.Column<any>[] = [];
let index: number = 0;
(<any[]>columns).map(col => {
if (col.type && col.type === 1) {
if (col.type === ColumnType.checkBox) {
this.createCheckBoxPlugin(col, index);
}
else if (col.type && col.type === 2) {
} else if (col.type === ColumnType.button) {
this.createButtonPlugin(col);
} else if (col.type === ColumnType.icon) {
mycolumns.push(TableComponent.createIconColumn(col));
}
else if (col.value) {
mycolumns.push(<Slick.Column<any>>{
name: col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: textFormatter,
});
mycolumns.push(TableComponent.createTextColumn(col as azdata.TableColumn));
} else {
mycolumns.push(<Slick.Column<any>>{
name: <string>col,
@@ -116,16 +117,57 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
}
}
public static transformData(rows: string[][], columns: any[]): { [key: string]: string }[] {
private static createIconColumn<T extends Slick.SlickData>(col: azdata.TableColumn): Slick.Column<T> {
return <Slick.Column<T>>{
name: col.name ?? col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: iconCssFormatter,
filterable: false
};
}
private static createTextColumn<T extends Slick.SlickData>(col: azdata.TableColumn): Slick.Column<T> {
return {
name: col.name ?? col.value,
id: col.value,
field: col.value,
width: col.width,
cssClass: col.cssClass,
headerCssClass: col.headerCssClass,
toolTip: col.toolTip,
formatter: textFormatter
};
}
public transformData(rows: (string | azdata.IconColumnCellValue)[][], columns: any[]): { [key: string]: string | CssIconCellValue }[] {
if (rows && columns) {
return rows.map(row => {
let object: { [key: string]: string } = {};
if (row.forEach) {
row.forEach((val, index) => {
let columnName: string = (columns[index].value) ? columns[index].value : <string>columns[index];
object[columnName] = val;
});
let object: { [key: string]: string | CssIconCellValue } = {};
if (!Array.isArray(row)) {
return object;
}
row.forEach((val, index) => {
let columnName: string = (columns[index].value) ? columns[index].value : <string>columns[index];
if (isIconColumnCellValue(val)) {
const icon: IUserFriendlyIcon = val.icon;
const iconKey: string = getIconKey(icon);
const iconCssClass = this._iconCssMap[iconKey] ?? createIconCssClass(icon);
if (!this._iconCssMap[iconKey]) {
this._iconCssMap[iconKey] = iconCssClass;
}
object[columnName] = { iconCssClass: iconCssClass, ariaLabel: val.ariaLabel };
} else {
object[columnName] = <string>val;
}
});
return object;
});
} else {
@@ -263,7 +305,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
public setProperties(properties: { [key: string]: any; }): void {
super.setProperties(properties);
this._tableData.clear();
this._tableData.push(TableComponent.transformData(this.data, this.columns));
this._tableData.push(this.transformData(this.data, this.columns));
this._tableColumns = this.transformColumns(this.columns);
this._table.columns = this._tableColumns;
this._table.setData(this._tableData);
@@ -497,8 +539,12 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
}
}
private appendData(data: any[][]) {
this._tableData.push(TableComponent.transformData(data, this.columns));
this._tableData.push(this.transformData(data, this.columns));
this.data = this._tableData.getItems().map(dataObject => Object.values(dataObject));
this.layoutTable();
}
}
function isIconColumnCellValue(obj: any | undefined): obj is azdata.IconColumnCellValue {
return !!(<azdata.IconColumnCellValue>obj)?.icon;
}

View File

@@ -5,10 +5,12 @@
import * as assert from 'assert';
import TableComponent from 'sql/workbench/browser/modelComponents/table.component';
import { CssIconCellValue } from 'sql/base/browser/ui/table/formatters';
suite('TableComponent Tests', () => {
setup(() => {
});
test('Table transformData should convert data and columns successfully given valid inputs', () => {
@@ -17,7 +19,9 @@ suite('TableComponent Tests', () => {
['4', '5', '6']
];
let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns);
const tableComponent = new TableComponent(undefined, undefined, undefined);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [
{
'c1': '1',
@@ -35,8 +39,9 @@ suite('TableComponent Tests', () => {
test('Table transformData should return empty array given undefined rows', () => {
let data = undefined;
const tableComponent = new TableComponent(undefined, undefined, undefined);
let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [];
assert.deepEqual(actual, expected);
});
@@ -47,7 +52,8 @@ suite('TableComponent Tests', () => {
['4', '5', '6']
];
let columns;
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns);
const tableComponent = new TableComponent(undefined, undefined, undefined);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [];
assert.deepEqual(actual, expected);
});
@@ -57,8 +63,9 @@ suite('TableComponent Tests', () => {
['1', '2'],
['4', '5']
];
const tableComponent = new TableComponent(undefined, undefined, undefined);
let columns = ['c1', 'c2', 'c3'];
let actual: { [key: string]: string }[] = TableComponent.transformData(data, columns);
let actual: { [key: string]: string | CssIconCellValue }[] = tableComponent.transformData(data, columns);
let expected: { [key: string]: string }[] = [
{
'c1': '1',