mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 17:22:45 -05:00
487 lines
16 KiB
TypeScript
487 lines
16 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import 'vs/css!./media/declarativeTable';
|
|
import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, Inject, Input, OnDestroy } from '@angular/core';
|
|
import * as azdata from 'azdata';
|
|
import { convertSize } from 'sql/base/browser/dom';
|
|
import { ComponentEventType, IComponent, IComponentDescriptor, IModelStore, ModelViewAction } from 'sql/platform/dashboard/browser/interfaces';
|
|
import { ContainerBase } from 'sql/workbench/browser/modelComponents/componentBase';
|
|
import { EventHelper } from 'vs/base/browser/dom';
|
|
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
|
import { ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
|
|
import { equals as arrayEquals } from 'vs/base/common/arrays';
|
|
import { KeyCode } from 'vs/base/common/keyCodes';
|
|
import { localize } from 'vs/nls';
|
|
import { ILogService } from 'vs/platform/log/common/log';
|
|
import * as colorRegistry from 'vs/platform/theme/common/colorRegistry';
|
|
import { IColorTheme, IThemeService } from 'vs/platform/theme/common/themeService';
|
|
import { equals } from 'vs/base/common/objects';
|
|
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
|
|
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
|
import { MenuItemAction, MenuRegistry } from 'vs/platform/actions/common/actions';
|
|
import { IAction, Separator } from 'vs/base/common/actions';
|
|
|
|
export enum DeclarativeDataType {
|
|
string = 'string',
|
|
category = 'category',
|
|
boolean = 'boolean',
|
|
editableCategory = 'editableCategory',
|
|
component = 'component',
|
|
menu = 'menu'
|
|
}
|
|
|
|
@Component({
|
|
selector: 'modelview-declarativeTable',
|
|
templateUrl: decodeURI(require.toUrl('./declarativeTable.component.html'))
|
|
})
|
|
export default class DeclarativeTableComponent extends ContainerBase<any, azdata.DeclarativeTableProperties> implements IComponent, OnDestroy, AfterViewInit {
|
|
@Input() descriptor: IComponentDescriptor;
|
|
@Input() modelStore: IModelStore;
|
|
|
|
private _data: azdata.DeclarativeTableCellValue[][] = [];
|
|
private _filteredRowIndexes: number[] | undefined = undefined;
|
|
private columns: azdata.DeclarativeTableColumn[] = [];
|
|
private _colorTheme: IColorTheme;
|
|
private _hasFocus: boolean;
|
|
|
|
/**
|
|
* The flag is set to true when the table gains focus. When a row is selected and the flag is true the row selected event will
|
|
* fire regardless whether the row is already selected.
|
|
*
|
|
*/
|
|
private _rowSelectionFocusFlag: boolean = false;
|
|
|
|
constructor(
|
|
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
|
|
@Inject(forwardRef(() => ElementRef)) el: ElementRef,
|
|
@Inject(ILogService) logService: ILogService,
|
|
@Inject(IThemeService) themeService: IThemeService,
|
|
@Inject(IContextMenuService) private contextMenuService: IContextMenuService,
|
|
@Inject(IInstantiationService) private instantiationService: IInstantiationService
|
|
) {
|
|
super(changeRef, el, logService);
|
|
this._colorTheme = themeService.getColorTheme();
|
|
this._register(themeService.onDidColorThemeChange((colorTheme) => {
|
|
this._colorTheme = colorTheme;
|
|
}));
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
this.baseInit();
|
|
}
|
|
|
|
override ngOnDestroy(): void {
|
|
this.baseDestroy();
|
|
}
|
|
|
|
public headerCheckboxVisible(colIdx: number): boolean {
|
|
return this.columns[colIdx].showCheckAll && this.isCheckBox(colIdx);
|
|
}
|
|
|
|
public isHeaderChecked(colIdx: number): boolean {
|
|
for (const row of this.data) {
|
|
const cellData = row[colIdx];
|
|
if (cellData.value === false && cellData.enabled !== false) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
public isCheckBox(colIdx: number): boolean {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.valueType === DeclarativeDataType.boolean;
|
|
}
|
|
|
|
public isContextMenuColumn(colIdx: number): boolean {
|
|
return this.columns[colIdx].valueType === DeclarativeDataType.menu;
|
|
}
|
|
|
|
public isControlEnabled(rowIdx: number, colIdx: number): boolean {
|
|
const cellData = this.data[rowIdx][colIdx];
|
|
const column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return !column.isReadOnly && cellData.enabled !== false;
|
|
}
|
|
|
|
private isLabel(colIdx: number): boolean {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.isReadOnly && column.valueType === DeclarativeDataType.string;
|
|
}
|
|
|
|
public isChecked(rowIdx: number, colIdx: number): boolean {
|
|
let cellData = this.data[rowIdx][colIdx];
|
|
if (cellData?.value === false) {
|
|
return false;
|
|
}
|
|
// Disabling it to check for null and undefined.
|
|
// eslint-disable-next-line eqeqeq
|
|
return cellData != undefined;
|
|
}
|
|
|
|
public onInputBoxChanged(e: string, rowIdx: number, colIdx: number): void {
|
|
this.onCellDataChanged(e, rowIdx, colIdx);
|
|
}
|
|
|
|
public onCheckBoxChanged(e: boolean, rowIdx: number, colIdx: number): void {
|
|
this.onCellDataChanged(e, rowIdx, colIdx);
|
|
// If all of the rows in that column are now checked, let's update the header.
|
|
if (this.columns[colIdx].showCheckAll) {
|
|
this._changeRef.detectChanges();
|
|
}
|
|
}
|
|
|
|
public onHeaderCheckBoxChanged(e: boolean, colIdx: number): void {
|
|
this.data.forEach((row, rowIdx) => {
|
|
const cellData = row[colIdx];
|
|
if (cellData.value !== e && cellData.enabled !== false) {
|
|
this.onCellDataChanged(e, rowIdx, colIdx);
|
|
}
|
|
});
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
public trackByFnCols(index: number, _item: any): number {
|
|
return index;
|
|
}
|
|
|
|
public onSelectBoxChanged(e: ISelectData | string, rowIdx: number, colIdx: number): void {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
|
|
if (column.categoryValues) {
|
|
if (typeof e === 'string') {
|
|
let category = column.categoryValues.find(c => c.displayName === e);
|
|
if (category) {
|
|
this.onCellDataChanged(category.name, rowIdx, colIdx);
|
|
} else {
|
|
this.onCellDataChanged(e, rowIdx, colIdx);
|
|
}
|
|
} else {
|
|
this.onCellDataChanged(column.categoryValues[e.index].name, rowIdx, colIdx);
|
|
}
|
|
}
|
|
}
|
|
|
|
private onCellDataChanged(newValue: string | number | boolean | any, rowIdx: number, colIdx: number): void {
|
|
this.data[rowIdx][colIdx].value = newValue;
|
|
|
|
if (this.properties.data) {
|
|
this.setPropertyFromUI<any[][]>((props, value) => props.data = value, this.data);
|
|
} else {
|
|
this.setPropertyFromUI<any[][]>((props, value) => props.dataValues = value, this.data);
|
|
}
|
|
|
|
let newCellData: azdata.TableCell = {
|
|
row: rowIdx,
|
|
column: colIdx,
|
|
value: newValue
|
|
};
|
|
this.fireEvent({
|
|
eventType: ComponentEventType.onDidChange,
|
|
args: newCellData
|
|
});
|
|
}
|
|
|
|
public isSelectBox(colIdx: number): boolean {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.valueType === DeclarativeDataType.category;
|
|
}
|
|
|
|
private isEditableSelectBox(colIdx: number): boolean {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.valueType === DeclarativeDataType.editableCategory;
|
|
}
|
|
|
|
public isInputBox(colIdx: number): boolean {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.valueType === DeclarativeDataType.string && !column.isReadOnly;
|
|
}
|
|
|
|
public isComponent(colIdx: number): boolean {
|
|
return this.columns[colIdx].valueType === DeclarativeDataType.component;
|
|
}
|
|
|
|
public getColumnWidth(col: number | azdata.DeclarativeTableColumn): string {
|
|
let column = typeof col === 'number' ? this.columns[col] : col;
|
|
return convertSize(column.width, '30px');
|
|
}
|
|
|
|
public getOptions(colIdx: number): string[] {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
return column.categoryValues ? column.categoryValues.map(x => x.displayName) : [];
|
|
}
|
|
|
|
public getSelectedOptionDisplayName(rowIdx: number, colIdx: number): string {
|
|
let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
|
|
let cellData = this.data[rowIdx][colIdx];
|
|
if (cellData && column.categoryValues) {
|
|
let category = column.categoryValues.find(v => v.name === cellData.value);
|
|
if (category) {
|
|
return category.displayName;
|
|
} else if (this.isEditableSelectBox(colIdx)) {
|
|
return String(cellData.value);
|
|
} else {
|
|
return undefined;
|
|
}
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
public getAriaLabel(rowIdx: number, colIdx: number): string {
|
|
const cellData = this.data[rowIdx][colIdx];
|
|
if (this.isLabel(colIdx)) {
|
|
if (cellData) {
|
|
if (cellData.ariaLabel) {
|
|
return cellData.ariaLabel;
|
|
} else if (cellData.value) {
|
|
return String(cellData.value);
|
|
}
|
|
} else {
|
|
return localize('blankValue', "blank");
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
public getCheckAllColumnAriaLabel(colIdx: number): string {
|
|
return localize('checkAllColumnLabel', "check all checkboxes in column: {0}", this.columns[colIdx].displayName);
|
|
}
|
|
|
|
public getHeaderAriaLabel(colIdx: number): string {
|
|
const column = this.columns[colIdx];
|
|
return (column.ariaLabel) ? column.ariaLabel : column.displayName;
|
|
}
|
|
|
|
public getItemDescriptor(componentId: string): IComponentDescriptor {
|
|
return this.modelStore.getComponentDescriptor(componentId);
|
|
}
|
|
|
|
/// IComponent implementation
|
|
|
|
public setLayout(layout: any): void {
|
|
// TODO allow configuring the look and feel
|
|
this.layout();
|
|
}
|
|
|
|
private static ACCEPTABLE_VALUES = new Set<string>(['number', 'string', 'boolean']);
|
|
public override setProperties(properties: azdata.DeclarativeTableProperties): void {
|
|
const basicData: any[][] = properties.data ?? [];
|
|
const complexData: azdata.DeclarativeTableCellValue[][] = properties.dataValues ?? [];
|
|
let finalData: azdata.DeclarativeTableCellValue[][];
|
|
|
|
finalData = basicData.map(row => {
|
|
return row.map((value): azdata.DeclarativeTableCellValue => {
|
|
if (DeclarativeTableComponent.ACCEPTABLE_VALUES.has(typeof (value))) {
|
|
return {
|
|
value: value
|
|
};
|
|
} else {
|
|
return {
|
|
value: JSON.stringify(value)
|
|
};
|
|
}
|
|
});
|
|
});
|
|
|
|
if (finalData.length <= 0) {
|
|
finalData = complexData;
|
|
}
|
|
|
|
this.columns = properties.columns ?? [];
|
|
|
|
// check whether the data property is changed before actually setting the properties.
|
|
const isDataPropertyChanged = !arrayEquals(this.data, finalData ?? [], (a, b) => {
|
|
return arrayEquals(a, b, (cell1, cell2) => {
|
|
return equals(cell1, cell2);
|
|
});
|
|
});
|
|
|
|
// the angular is using reference compare to determine whether the data is changed or not
|
|
// so we are only updating it when the actual data has changed by doing the deep comparison.
|
|
// if the data property is changed, we need add child components to the container,
|
|
// so that the events can be passed upwards through the control hierarchy.
|
|
if (isDataPropertyChanged) {
|
|
this.clearContainer();
|
|
this._data = finalData;
|
|
}
|
|
|
|
const previousSelectedRow = this.selectedRow;
|
|
|
|
super.setProperties(properties);
|
|
|
|
if (this.selectedRow !== previousSelectedRow && this.enableRowSelection) {
|
|
this.fireEvent({
|
|
eventType: ComponentEventType.onSelectedRowChanged,
|
|
args: {
|
|
row: this.selectedRow
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public override clearContainer(): void {
|
|
super.clearContainer();
|
|
this.selectedRow = -1;
|
|
}
|
|
|
|
public get data(): azdata.DeclarativeTableCellValue[][] {
|
|
return this._data;
|
|
}
|
|
|
|
public isRowSelected(row: number): boolean {
|
|
if (!this.enableRowSelection) {
|
|
return false;
|
|
}
|
|
return this.selectedRow === row;
|
|
}
|
|
|
|
public onRowSelected(row: number) {
|
|
if (!this.enableRowSelection) {
|
|
return;
|
|
}
|
|
if (this._rowSelectionFocusFlag || !this.isRowSelected(row)) {
|
|
this.selectedRow = row;
|
|
this._rowSelectionFocusFlag = false;
|
|
this._changeRef.detectChanges();
|
|
|
|
this.fireEvent({
|
|
eventType: ComponentEventType.onSelectedRowChanged,
|
|
args: {
|
|
row
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
public get contextMenuButtonTitle(): string {
|
|
return localize('declarativeTable.showActions', "Show Actions");
|
|
}
|
|
|
|
public onContextMenuButtonKeyDown(event: KeyboardEvent, row: number, column: number): void {
|
|
const keyboardEvent = new StandardKeyboardEvent(event);
|
|
if (keyboardEvent.keyCode === KeyCode.Space ||
|
|
keyboardEvent.keyCode === KeyCode.Enter) {
|
|
this.showContextMenu(event, row, column);
|
|
}
|
|
}
|
|
|
|
public onContextMenuButtonClick(event: MouseEvent, row: number, column: number): void {
|
|
this.showContextMenu(event, row, column);
|
|
}
|
|
|
|
private showContextMenu(event: MouseEvent | KeyboardEvent, row: number, column: number): void {
|
|
EventHelper.stop(event, true);
|
|
const cellValue = this.data[row][column].value as azdata.DeclarativeTableMenuCellValue;
|
|
const actions: IAction[] = [];
|
|
let addSeparator = false;
|
|
for (const [index, command] of cellValue.commands.entries()) {
|
|
const isCommand = typeof command === 'string';
|
|
if (addSeparator || (!isCommand && index !== 0)) {
|
|
actions.push(new Separator());
|
|
}
|
|
if (typeof command === 'string') {
|
|
addSeparator = false;
|
|
actions.push(this.createMenuItem(command));
|
|
} else {
|
|
addSeparator = true;
|
|
actions.push(...command.map(cmd => {
|
|
return this.createMenuItem(cmd);
|
|
}));
|
|
}
|
|
}
|
|
this.contextMenuService.showContextMenu({
|
|
getAnchor: () => event.currentTarget as HTMLElement,
|
|
getActions: () => actions,
|
|
getActionsContext: () => cellValue.context
|
|
});
|
|
}
|
|
|
|
private createMenuItem(commandId: string): MenuItemAction {
|
|
const command = MenuRegistry.getCommand(commandId);
|
|
return this.instantiationService.createInstance(MenuItemAction, command, undefined, { shouldForwardArgs: true });
|
|
}
|
|
|
|
public onKey(e: KeyboardEvent, row: number) {
|
|
// Ignore the bubble up events
|
|
if (e.target !== e.currentTarget) {
|
|
return;
|
|
}
|
|
const event = new StandardKeyboardEvent(e);
|
|
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
|
|
this.onRowSelected(row);
|
|
EventHelper.stop(e, true);
|
|
}
|
|
}
|
|
|
|
public override doAction(action: string, ...args: any[]): void {
|
|
if (action === ModelViewAction.Filter) {
|
|
this._filteredRowIndexes = args[0];
|
|
}
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
/**
|
|
* Checks whether a given row is filtered (not visible)
|
|
* @param rowIndex The row to check
|
|
*/
|
|
public isFiltered(rowIndex: number): boolean {
|
|
if (this._filteredRowIndexes === undefined) {
|
|
return false;
|
|
}
|
|
return this._filteredRowIndexes.includes(rowIndex) ? false : true;
|
|
}
|
|
|
|
public override get CSSStyles(): azdata.CssStyles {
|
|
return this.mergeCss(super.CSSStyles, {
|
|
'width': this.getWidth(),
|
|
'height': this.getHeight()
|
|
});
|
|
}
|
|
|
|
public getRowStyle(rowIndex: number): azdata.CssStyles {
|
|
if (this.isRowSelected(rowIndex)) {
|
|
const bgColor = this._hasFocus ? colorRegistry.listActiveSelectionBackground : colorRegistry.listInactiveSelectionBackground;
|
|
const color = this._hasFocus ? colorRegistry.listActiveSelectionForeground : colorRegistry.listInactiveSelectionForeground;
|
|
return {
|
|
'background-color': this._colorTheme.getColor(bgColor)?.toString(),
|
|
'color': this._colorTheme.getColor(color)?.toString()
|
|
};
|
|
} else {
|
|
return {};
|
|
}
|
|
}
|
|
|
|
onFocusIn() {
|
|
this._hasFocus = true;
|
|
this._rowSelectionFocusFlag = true;
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
onFocusOut() {
|
|
this._hasFocus = false;
|
|
this._changeRef.detectChanges();
|
|
}
|
|
|
|
public get enableRowSelection(): boolean {
|
|
return this.getPropertyOrDefault<boolean>((props) => props.enableRowSelection, false);
|
|
}
|
|
|
|
public get selectedRow(): number {
|
|
return this.getPropertyOrDefault<number>((props) => props.selectedRow, -1);
|
|
}
|
|
|
|
public set selectedRow(row: number) {
|
|
if (row !== this.selectedRow) {
|
|
this.setPropertyFromUI<number>((properties, value) => { properties.selectedRow = value; }, row);
|
|
}
|
|
}
|
|
|
|
public showColumn(column: azdata.DeclarativeTableColumn): boolean {
|
|
return column.hidden === undefined || !column.hidden;
|
|
}
|
|
}
|