Change tables to make them work for our scenario (#12193)

* Change tables to make them work for our scenario

* Comments & deprecate API

* Disable selections by default
This commit is contained in:
Amir Omidi
2020-09-11 13:44:19 -07:00
committed by GitHub
parent 58d3b969a2
commit 61ceb72cea
12 changed files with 253 additions and 49 deletions

View File

@@ -104,6 +104,7 @@ export function createViewContext(): ViewTestContext {
}); });
let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, { let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, {
onDataChanged: undefined!, onDataChanged: undefined!,
onRowSelected: undefined!,
data: [], data: [],
columns: [] columns: []
}); });

View File

@@ -346,6 +346,7 @@ class TestDeclarativeTableComponent extends TestComponentBase implements azdata.
super(); super();
} }
onDataChanged: vscode.Event<any> = this.onClick.event; onDataChanged: vscode.Event<any> = this.onClick.event;
onRowSelected: vscode.Event<any> = this.onClick.event;
data: any[][]; data: any[][];
columns: azdata.DeclarativeTableColumn[]; columns: azdata.DeclarativeTableColumn[];
} }

View File

@@ -180,6 +180,7 @@ describe('Manage Package Dialog', () => {
}); });
let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, { let declarativeTable: () => azdata.DeclarativeTableComponent = () => Object.assign({}, componentBase, {
onDataChanged: undefined!, onDataChanged: undefined!,
onRowSelected: undefined!,
data: [], data: [],
columns: [] columns: []
}); });

View File

@@ -416,7 +416,7 @@ export class PublishDatabaseDialog {
table.onDataChanged(() => { table.onDataChanged(() => {
this.sqlCmdVars = {}; this.sqlCmdVars = {};
table.data.forEach((row) => { table.data?.forEach((row) => {
(<Record<string, string>>this.sqlCmdVars)[row[0]] = row[1]; (<Record<string, string>>this.sqlCmdVars)[row[0]] = row[1];
}); });

View File

@@ -16,32 +16,77 @@ export class SqlDatabaseTree extends AssessmentDialogComponent {
private createTableComponent(view: azdata.ModelView): azdata.DeclarativeTableComponent { private createTableComponent(view: azdata.ModelView): azdata.DeclarativeTableComponent {
const table = view.modelBuilder.declarativeTable().withProperties<azdata.DeclarativeTableProperties>( const style: azdata.CssStyles = {
'border': 'none',
'text-align': 'left'
};
const table = view.modelBuilder.declarativeTable().withProps(
{ {
selectEffect: true,
columns: [ columns: [
{
displayName: '',
valueType: azdata.DeclarativeDataType.boolean,
width: 5,
isReadOnly: false,
showCheckAll: true,
headerCssStyles: style,
ariaLabel: 'Database Migration Check' // TODO localize
},
{ {
displayName: 'Database', // TODO localize displayName: 'Database', // TODO localize
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
width: 50, width: 50,
isReadOnly: true, isReadOnly: true,
showCheckAll: true headerCssStyles: style
}, },
{ {
displayName: '', // Incidents displayName: '', // Incidents
valueType: azdata.DeclarativeDataType.string, valueType: azdata.DeclarativeDataType.string,
width: 5, width: 5,
isReadOnly: true, isReadOnly: true,
showCheckAll: false headerCssStyles: style,
ariaLabel: 'Issue Count' // TODO localize
} }
], ],
data: [ dataValues: [
['DB1', '1'], [
['DB2', '0'] {
value: false,
style
},
{
value: 'DB1',
style
},
{
value: 1,
style
}
],
[
{
value: true,
style
},
{
value: 'DB2',
style
},
{
value: 2,
style
}
]
], ],
width: '200px'
} }
); );
table.component().onRowSelected(({ row }) => {
console.log(row);
});
return table.component(); return table.component();
} }
} }

5
src/sql/azdata.d.ts vendored
View File

@@ -3306,7 +3306,10 @@ declare module 'azdata' {
} }
export interface DeclarativeTableProperties { export interface DeclarativeTableProperties {
data: any[][]; /**
* @deprecated Use dataValues instead.
*/
data?: any[][];
columns: DeclarativeTableColumn[]; columns: DeclarativeTableColumn[];
} }

View File

@@ -203,17 +203,26 @@ declare module 'azdata' {
} }
export interface DeclarativeTableColumn { export interface DeclarativeTableColumn {
headerCssStyles?: { [key: string]: string }; headerCssStyles?: CssStyles;
rowCssStyles?: { [key: string]: string }; rowCssStyles?: CssStyles;
ariaLabel?: string; ariaLabel?: string;
showCheckAll?: boolean; showCheckAll?: boolean;
isChecked?: boolean; isChecked?: boolean;
} }
export enum DeclarativeDataType { export enum DeclarativeDataType {
component = 'component' component = 'component'
} }
export type DeclarativeTableRowSelectedEvent = {
row: number
};
export interface DeclarativeTableComponent extends Component, DeclarativeTableProperties {
onRowSelected: vscode.Event<DeclarativeTableRowSelectedEvent>;
}
/* /*
* Add optional azureAccount for connectionWidget. * Add optional azureAccount for connectionWidget.
*/ */
@@ -295,6 +304,21 @@ declare module 'azdata' {
} }
export interface DeclarativeTableProperties extends ComponentProperties { export interface DeclarativeTableProperties extends ComponentProperties {
/**
* dataValues will only be used if data is an empty array
*/
dataValues?: DeclarativeTableCellValue[][];
/**
* Should the table react to user selections
*/
selectEffect?: boolean; // Defaults to false
}
export interface DeclarativeTableCellValue {
value: string | number | boolean;
ariaLabel?: string;
style?: CssStyles
} }
export interface ComponentProperties { export interface ComponentProperties {

View File

@@ -1462,15 +1462,26 @@ class DeclarativeTableWrapper extends ComponentWrapper implements azdata.Declara
super(proxy, handle, ModelComponentTypes.DeclarativeTable, id); super(proxy, handle, ModelComponentTypes.DeclarativeTable, id);
this.properties = {}; this.properties = {};
this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>()); this._emitterMap.set(ComponentEventType.onDidChange, new Emitter<any>());
this._emitterMap.set(ComponentEventType.onDidClick, new Emitter<any>());
} }
public get data(): any[][] { public get data(): any[][] {
return this.properties['data']; return this.properties['data'];
} }
public set data(v: any[][]) { public set data(v: any[][]) {
this.setProperty('data', v); this.setProperty('data', v);
} }
public get dataValues(): azdata.DeclarativeTableCellValue[][] {
return this.properties['dataValues'];
}
public set dataValues(v: azdata.DeclarativeTableCellValue[][]) {
this.setProperty('dataValues', v);
}
public get columns(): azdata.DeclarativeTableColumn[] { public get columns(): azdata.DeclarativeTableColumn[] {
return this.properties['columns']; return this.properties['columns'];
} }
@@ -1484,10 +1495,23 @@ class DeclarativeTableWrapper extends ComponentWrapper implements azdata.Declara
return emitter && emitter.event; return emitter && emitter.event;
} }
public get onRowSelected(): vscode.Event<any> {
let emitter = this._emitterMap.get(ComponentEventType.onDidClick);
return emitter && emitter.event;
}
protected notifyPropertyChanged(): Thenable<void> { protected notifyPropertyChanged(): Thenable<void> {
return this._proxy.$setProperties(this._handle, this._id, this.getPropertiesForMainThread()); return this._proxy.$setProperties(this._handle, this._id, this.getPropertiesForMainThread());
} }
public get selectEffect(): boolean | undefined {
return this.properties['selectEffect'];
}
public set selectEffect(v: boolean | undefined) {
this.setProperty('selectEffect', v);
}
public toComponentShape(): IComponentShape { public toComponentShape(): IComponentShape {
// Overridden to ensure we send the correct properties mapping. // Overridden to ensure we send the correct properties mapping.
return <IComponentShape>{ return <IComponentShape>{

View File

@@ -372,6 +372,17 @@ export abstract class ContainerBase<T, TPropertyBag extends azdata.ComponentProp
return; return;
} }
public mergeCss(...styles: azdata.CssStyles[]): azdata.CssStyles {
const x = styles.reduce((previous, current) => {
if (current) {
return Object.assign(previous, current);
}
return previous;
}, {});
return x;
}
protected onItemsUpdated(): void { protected onItemsUpdated(): void {
} }

View File

@@ -1,26 +1,45 @@
<table role=grid #container *ngIf="columns" class="declarative-table" [style.height]="getHeight()" [style.width]="getWidth()" [attr.aria-label]="ariaLabel"> <table role=grid #container *ngIf="columns" class="declarative-table" [style.height]="getHeight()"
[style.width]="getWidth()" [attr.aria-label]="ariaLabel">
<thead> <thead>
<ng-container *ngFor="let column of columns; let c = index;"> <ng-container *ngFor="let column of columns; let c = index;">
<th class="declarative-table-header" aria-sort="none" [style.width]="getColumnWidth(column)" [attr.aria-label]="column.ariaLabel" [ngStyle]="column.headerCssStyles" role="columnheader"> <th class="declarative-table-header" aria-sort="none" [style.width]="getColumnWidth(column)"
{{column.displayName}} [attr.aria-label]="column.ariaLabel" [ngStyle]="column.headerCssStyles" role="columnheader">
<checkbox *ngIf="isCheckBox(c)" [checked]="isHeaderChecked(c)" (onChange)="onHeaderCheckBoxChanged($event,c)" label="" ></checkbox> {{column.displayName}}
</th> <checkbox *ngIf="isCheckBox(c)" [checked]="isHeaderChecked(c)"
(onChange)="onHeaderCheckBoxChanged($event,c)" label=""></checkbox>
</th>
</ng-container> </ng-container>
</thead> </thead>
<ng-container *ngIf="data"> <ng-container *ngIf="data.length > 0">
<ng-container *ngFor="let row of data;let r = index;"> <ng-container *ngFor="let row of data;let r = index;">
<tr class="declarative-table-row"> <tr class="declarative-table-row" [class.selected]="isRowSelected(r)">
<ng-container *ngFor="let cellData of row;let c = index;trackBy:trackByFnCols"> <ng-container *ngFor="let cellData of row;let c = index;trackBy:trackByFnCols">
<td class="declarative-table-cell" [style.width]="getColumnWidth(c)" [attr.aria-label]="getAriaLabel(r, c)" [ngStyle]="columns[c].rowCssStyles"> <td class="declarative-table-cell" [style.width]="getColumnWidth(c)"
<checkbox *ngIf="isCheckBox(c)" label="" (onChange)="onCheckBoxChanged($event,r,c)" [enabled]="isControlEnabled(c)" [checked]="isChecked(r,c)"></checkbox> [attr.aria-label]="getAriaLabel(r, c)"
<select-box *ngIf="isSelectBox(c)" [options]="getOptions(c)" (onDidSelect)="onSelectBoxChanged($event,r,c)" [selectedOption]="getSelectedOptionDisplayName(r,c)"></select-box> [ngStyle]="mergeCss(columns[c].rowCssStyles, cellData.style)">
<editable-select-box *ngIf="isEditableSelectBox(c)" [options]="getOptions(c)" (onDidSelect)="onSelectBoxChanged($event,r,c)" [selectedOption]="getSelectedOptionDisplayName(r,c)"></editable-select-box> <checkbox *ngIf="isCheckBox(c)" label="" (onChange)="onCheckBoxChanged($event,r,c)"
<input-box *ngIf="isInputBox(c)" [value]="cellData" (onDidChange)="onInputBoxChanged($event,r,c)"></input-box> [enabled]="isControlEnabled(c)" [checked]="isChecked(r,c)"
<ng-container *ngIf="isLabel(c)" >{{cellData}}</ng-container> [ngStyle]="mergeCss(columns[c].rowCssStyles, cellData.style)">
<model-component-wrapper *ngIf="isComponent(c) && getItemDescriptor(cellData)" [descriptor]="getItemDescriptor(cellData)" [modelStore]="modelStore"></model-component-wrapper> </checkbox>
</td> <select-box *ngIf="isSelectBox(c)" [options]="getOptions(c)"
</ng-container> (onDidSelect)="onSelectBoxChanged($event,r,c)"
</tr> [selectedOption]="getSelectedOptionDisplayName(r,c)">
</ng-container> </select-box>
<editable-select-box *ngIf="isEditableSelectBox(c)" [options]="getOptions(c)"
(onDidSelect)="onSelectBoxChanged($event,r,c)"
[selectedOption]="getSelectedOptionDisplayName(r,c)">
</editable-select-box>
<input-box *ngIf="isInputBox(c)" [value]="cellData.value"
(onDidChange)="onInputBoxChanged($event,r,c)"></input-box>
<span *ngIf="isLabel(c)" (click)="onCellClick(r)">
{{cellData.value}}
</span>
<model-component-wrapper *ngIf="isComponent(c) && getItemDescriptor(cellData)"
[descriptor]="getItemDescriptor(cellData)" [modelStore]="modelStore">
</model-component-wrapper>
</td>
</ng-container>
</tr>
</ng-container> </ng-container>
</table> </ng-container>
</table>

View File

@@ -30,12 +30,13 @@ export enum DeclarativeDataType {
selector: 'modelview-declarativeTable', selector: 'modelview-declarativeTable',
templateUrl: decodeURI(require.toUrl('./declarativeTable.component.html')) templateUrl: decodeURI(require.toUrl('./declarativeTable.component.html'))
}) })
export default class DeclarativeTableComponent extends ContainerBase<any> implements IComponent, OnDestroy, AfterViewInit { export default class DeclarativeTableComponent extends ContainerBase<any, azdata.DeclarativeTableProperties> implements IComponent, OnDestroy, AfterViewInit {
@Input() descriptor: IComponentDescriptor; @Input() descriptor: IComponentDescriptor;
@Input() modelStore: IModelStore; @Input() modelStore: IModelStore;
private data: any[][] = []; private _data: azdata.DeclarativeTableCellValue[][] = [];
private columns: azdata.DeclarativeTableColumn[] = []; private columns: azdata.DeclarativeTableColumn[] = [];
private _selectedRow: number;
constructor( constructor(
@Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef,
@@ -77,7 +78,12 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
public isChecked(rowIdx: number, colIdx: number): boolean { public isChecked(rowIdx: number, colIdx: number): boolean {
let cellData = this.data[rowIdx][colIdx]; let cellData = this.data[rowIdx][colIdx];
return cellData; 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 { public onInputBoxChanged(e: string, rowIdx: number, colIdx: number): void {
@@ -86,10 +92,11 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
public onCheckBoxChanged(e: boolean, rowIdx: number, colIdx: number): void { public onCheckBoxChanged(e: boolean, rowIdx: number, colIdx: number): void {
this.onCellDataChanged(e, rowIdx, colIdx); 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) { if (this.columns[colIdx].showCheckAll) {
if (e) { if (e) {
for (let rowIdx = 0; rowIdx < this.data.length; rowIdx++) { for (let rowIdx = 0; rowIdx < this.data.length; rowIdx++) {
if (!this.data[rowIdx][colIdx]) { if (this.data[rowIdx][colIdx].value === false) {
return; return;
} }
} }
@@ -102,20 +109,20 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
public onHeaderCheckBoxChanged(e: boolean, colIdx: number): void { public onHeaderCheckBoxChanged(e: boolean, colIdx: number): void {
this.columns[colIdx].isChecked = e; this.columns[colIdx].isChecked = e;
this.data.forEach((row, rowIdx) => { this.data.forEach((row, rowIdx) => {
if (row[colIdx] !== e) { if (row[colIdx].value !== e) {
this.onCellDataChanged(e, rowIdx, colIdx); this.onCellDataChanged(e, rowIdx, colIdx);
} }
}); });
this._changeRef.detectChanges(); this._changeRef.detectChanges();
} }
public trackByFnCols(index: number, item: any): any { public trackByFnCols(index: number, _item: any): number {
return index; return index;
} }
public onSelectBoxChanged(e: ISelectData | string, rowIdx: number, colIdx: number): void { public onSelectBoxChanged(e: ISelectData | string, rowIdx: number, colIdx: number): void {
let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
if (column.categoryValues) { if (column.categoryValues) {
if (typeof e === 'string') { if (typeof e === 'string') {
let category = find(column.categoryValues, c => c.displayName === e); let category = find(column.categoryValues, c => c.displayName === e);
@@ -130,8 +137,8 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
} }
} }
private onCellDataChanged(newValue: any, rowIdx: number, colIdx: number): void { private onCellDataChanged(newValue: string | number | boolean | any, rowIdx: number, colIdx: number): void {
this.data[rowIdx][colIdx] = newValue; this.data[rowIdx][colIdx].value = newValue;
this.setPropertyFromUI<any[][]>((props, value) => props.data = value, this.data); this.setPropertyFromUI<any[][]>((props, value) => props.data = value, this.data);
let newCellData: azdata.TableCell = { let newCellData: azdata.TableCell = {
row: rowIdx, row: rowIdx,
@@ -177,11 +184,11 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
let column: azdata.DeclarativeTableColumn = this.columns[colIdx]; let column: azdata.DeclarativeTableColumn = this.columns[colIdx];
let cellData = this.data[rowIdx][colIdx]; let cellData = this.data[rowIdx][colIdx];
if (cellData && column.categoryValues) { if (cellData && column.categoryValues) {
let category = find(column.categoryValues, v => v.name === cellData); let category = find(column.categoryValues, v => v.name === cellData.value);
if (category) { if (category) {
return category.displayName; return category.displayName;
} else if (this.isEditableSelectBox(colIdx)) { } else if (this.isEditableSelectBox(colIdx)) {
return cellData; return String(cellData.value);
} else { } else {
return undefined; return undefined;
} }
@@ -192,7 +199,19 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
public getAriaLabel(rowIdx: number, colIdx: number): string { public getAriaLabel(rowIdx: number, colIdx: number): string {
const cellData = this.data[rowIdx][colIdx]; const cellData = this.data[rowIdx][colIdx];
return this.isLabel(colIdx) ? (cellData && cellData !== '' ? cellData : localize('blankValue', "blank")) : ''; 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 getItemDescriptor(componentId: string): IComponentDescriptor { public getItemDescriptor(componentId: string): IComponentDescriptor {
@@ -206,12 +225,34 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
this.layout(); this.layout();
} }
public setProperties(properties: { [key: string]: any; }): void { private static ACCEPTABLE_VALUES = new Set<string>(['number', 'string', 'boolean']);
const newData = properties.data ?? []; public 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 ?? []; this.columns = properties.columns ?? [];
// check whether the data property is changed before actually setting the properties. // check whether the data property is changed before actually setting the properties.
const isDataPropertyChanged = !arrayEquals(this.data, newData ?? [], (a, b) => { const isDataPropertyChanged = !arrayEquals(this.data, finalData ?? [], (a, b) => {
return arrayEquals(a, b); return arrayEquals(a, b);
}); });
@@ -221,15 +262,45 @@ export default class DeclarativeTableComponent extends ContainerBase<any> implem
// so that the events can be passed upwards through the control hierarchy. // so that the events can be passed upwards through the control hierarchy.
if (isDataPropertyChanged) { if (isDataPropertyChanged) {
this.clearContainer(); this.clearContainer();
this.data = newData; this._data = finalData;
this.data?.forEach(row => { this.data?.forEach(row => {
for (let i = 0; i < row.length; i++) { for (let i = 0; i < row.length; i++) {
if (this.isComponent(i)) { if (this.isComponent(i)) {
this.addToContainer(this.getItemDescriptor(row[i] as string), undefined); this.addToContainer(this.getItemDescriptor(row[i].value as string), undefined);
} }
} }
}); });
} }
super.setProperties(properties); super.setProperties(properties);
} }
public get data(): azdata.DeclarativeTableCellValue[][] {
return this._data;
}
public isRowSelected(row: number): boolean {
// Only react when the user wants you to
if (this.getProperties().selectEffect !== true) {
return false;
}
return this._selectedRow === row;
}
public onCellClick(row: number) {
// Only react when the user wants you to
if (this.getProperties().selectEffect !== true) {
return;
}
if (!this.isRowSelected(row)) {
this._selectedRow = row;
this._changeRef.detectChanges();
this.fireEvent({
eventType: ComponentEventType.onDidClick,
args: {
row
}
});
}
}
} }

View File

@@ -26,3 +26,7 @@
width: 100%; width: 100%;
text-align: center; text-align: center;
} }
.declarative-table-row.selected {
background-color: rgb(0, 120, 215);
}