From 2d7eb0dcb50f207c5e1a68b5c703fb35dfb7efcb Mon Sep 17 00:00:00 2001 From: udeeshagautam <46980425+udeeshagautam@users.noreply.github.com> Date: Wed, 22 May 2019 13:53:50 -0700 Subject: [PATCH] Feature/schemacompare include exclude checkboxes (#5548) * Adding include exclude boxes * Adding as table component generic column and rememebering state. * converged custome action and select checkboxes, moved sc checkbox to middle, can have multiple checkboxes and can remember state now * adding action on column as a common column property * Taking PR comments * Changing Arg name as per CR comment * Taking a PR comment --- .../schema-compare/src/schemaCompareResult.ts | 87 ++++++++++++++- src/sql/azdata.proposed.d.ts | 31 ++++++ .../plugins/checkboxSelectColumn.plugin.ts | 103 +++++++++++++++++- .../workbench/api/common/sqlExtHostTypes.ts | 14 ++- .../workbench/api/node/extHostModelView.ts | 6 + .../workbench/api/node/sqlExtHost.api.impl.ts | 4 +- .../modelComponents/interfaces.ts | 3 +- .../modelComponents/table.component.ts | 62 ++++++++++- 8 files changed, 293 insertions(+), 17 deletions(-) diff --git a/extensions/schema-compare/src/schemaCompareResult.ts b/extensions/schema-compare/src/schemaCompareResult.ts index 15a15ceef8..0ab61f3e5c 100644 --- a/extensions/schema-compare/src/schemaCompareResult.ts +++ b/extensions/schema-compare/src/schemaCompareResult.ts @@ -35,6 +35,10 @@ export class SchemaCompareResult { private targetNameComponent: azdata.TableComponent; private deploymentOptions: azdata.DeploymentOptions; private schemaCompareOptionDialog: SchemaCompareOptionsDialog; + private tablelistenersToDispose: vscode.Disposable[] = []; + private originalSourceExcludes = {}; + private originalTargetExcludes = {}; + private sourceTargetSwitched = false; constructor(private sourceName: string, private targetName: string, private sourceEndpointInfo: azdata.SchemaCompareEndpointInfo, private targetEndpointInfo: azdata.SchemaCompareEndpointInfo) { this.SchemaCompareActionMap = new Map(); @@ -191,24 +195,37 @@ export class SchemaCompareResult { columns: [ { value: localize('schemaCompare.typeColumn', 'Type'), + toolTip: localize('schemaCompare.typeColumn', 'Type'), cssClass: 'align-with-header', width: 50 }, { value: localize('schemaCompare.sourceNameColumn', 'Source Name'), + toolTip: localize('schemaCompare.sourceNameColumn', 'Source Name'), cssClass: 'align-with-header', width: 90 }, + { + value: localize('schemaCompare.includeColumnName', 'Include'), + toolTip: localize('schemaCompare.includeColumnName', 'Include'), + cssClass: 'align-with-header', + width: 60, + type: azdata.ColumnType.checkBox, + options: { actionOnCheckbox: azdata.ActionOnCellCheckboxCheck.customAction } + }, { value: localize('schemaCompare.actionColumn', 'Action'), + toolTip: localize('schemaCompare.actionColumn', 'Action'), cssClass: 'align-with-header', width: 30 }, { value: localize('schemaCompare.targetNameColumn', 'Target Name'), + toolTip: localize('schemaCompare.targetNameColumn', 'Target Name'), cssClass: 'align-with-header', width: 150 - }] + } + ], }); this.splitView.addItem(this.differencesTable); @@ -224,6 +241,16 @@ export class SchemaCompareResult { this.compareButton.enabled = true; this.optionsButton.enabled = true; + // explicitly exclude things that were excluded in previous compare + const thingsToExclude = this.sourceTargetSwitched ? this.originalTargetExcludes : this.originalSourceExcludes; + if (thingsToExclude) { + for (let item in thingsToExclude) { + if (thingsToExclude[item]) { + service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, thingsToExclude[item], false, azdata.TaskExecutionMode.execute); + } + } + } + if (this.comparisonResult.differences.length > 0) { this.flexModel.addItem(this.splitView); @@ -241,7 +268,7 @@ export class SchemaCompareResult { let sourceText = ''; let targetText = ''; - this.differencesTable.onRowSelected(() => { + this.tablelistenersToDispose.push(this.differencesTable.onRowSelected(() => { let difference = this.comparisonResult.differences[this.differencesTable.selectedRows[0]]; if (difference !== undefined) { sourceText = this.getFormattedScript(difference, true); @@ -253,7 +280,53 @@ export class SchemaCompareResult { title: diffEditorTitle }); } - }); + })); + this.tablelistenersToDispose.push(this.differencesTable.onCellAction(async (rowState) => { + let checkboxState = rowState; + if (checkboxState) { + let diff = this.comparisonResult.differences[checkboxState.row]; + await service.schemaCompareIncludeExcludeNode(this.comparisonResult.operationId, diff, checkboxState.checked, azdata.TaskExecutionMode.execute); + this.saveExcludeState(checkboxState); + } + })); + } + + // save state based on source name if present otherwise target name (parity with SSDT) + private saveExcludeState(rowState: azdata.ICheckboxCellActionEventArgs) { + if (rowState) { + let diff = this.comparisonResult.differences[rowState.row]; + let key = diff.sourceValue ? diff.sourceValue : diff.targetValue; + if (key) { + if (!this.sourceTargetSwitched) { + delete this.originalSourceExcludes[key]; + if (!rowState.checked) { + this.originalSourceExcludes[key] = diff; + } + } + else { + delete this.originalTargetExcludes[key]; + if (!rowState.checked) { + this.originalTargetExcludes[key] = diff; + } + } + } + } + } + + private shouldDiffBeIncluded(diff: azdata.DiffEntry): boolean { + let key = diff.sourceValue ? diff.sourceValue : diff.targetValue; + if (key) { + if (this.sourceTargetSwitched === true && this.originalTargetExcludes[key]) { + this.originalTargetExcludes[key] = diff; + return false; + } + if (this.sourceTargetSwitched === false && this.originalSourceExcludes[key]) { + this.originalSourceExcludes[key] = diff; + return false; + } + return true; + } + return true; } private getAllDifferences(differences: azdata.DiffEntry[]): string[][] { @@ -262,7 +335,8 @@ export class SchemaCompareResult { differences.forEach(difference => { if (difference.differenceType === azdata.SchemaDifferenceType.Object) { if (difference.sourceValue !== null || difference.targetValue !== null) { - data.push([difference.name, difference.sourceValue, this.SchemaCompareActionMap[difference.updateAction], difference.targetValue]); + let state: boolean = this.shouldDiffBeIncluded(difference); + data.push([difference.name, difference.sourceValue, state, this.SchemaCompareActionMap[difference.updateAction], difference.targetValue]); } } }); @@ -312,6 +386,9 @@ export class SchemaCompareResult { }); this.differencesTable.selectedRows = null; + if (this.tablelistenersToDispose) { + this.tablelistenersToDispose.forEach(x => x.dispose()); + } this.resetButtons(false); this.execute(); } @@ -443,6 +520,8 @@ export class SchemaCompareResult { ] }); + // remember that source target have been toggled + this.sourceTargetSwitched = this.sourceTargetSwitched ? false : true; this.startCompare(); }); } diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 0f13715d62..da04465f97 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -3126,6 +3126,26 @@ declare module 'azdata' { cssClass?: string; headerCssClass?: string; toolTip?: string; + type?: ColumnType; + options?: CheckboxColumnOption | TextColumnOption; + } + + export enum ColumnType { + text = 0, + checkBox = 1, + button = 2 + } + + export interface CheckboxColumnOption { + actionOnCheckbox: ActionOnCellCheckboxCheck; + } + + export interface TextColumnOption { + } + + export enum ActionOnCellCheckboxCheck { + selectRow = 0, + customAction = 1 } export interface TableComponentProperties extends ComponentProperties { @@ -3327,8 +3347,19 @@ declare module 'azdata' { onRowSelected: vscode.Event; } + export interface ICheckboxCellActionEventArgs extends ICellActionEventArgs { + checked: boolean; + } + + interface ICellActionEventArgs { + row: number; + column: number; + columnName: number; + } + export interface TableComponent extends Component, TableComponentProperties { onRowSelected: vscode.Event; + onCellAction?: vscode.Event; } export interface FileBrowserTreeComponent extends Component, FileBrowserTreeProperties { diff --git a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts index 7f35c6d672..a975673896 100644 --- a/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts +++ b/src/sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin.ts @@ -3,6 +3,7 @@ import { mixin } from 'vs/base/common/objects'; import * as nls from 'vs/nls'; import { ICheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox'; +import { Emitter, Event as vsEvent } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; @@ -14,6 +15,20 @@ export interface ICheckboxSelectColumnOptions extends Slick.PluginOptions, IChec toolTip?: string; width?: number; title?: string; + columnIndex?: number; + actionOnCheck?: ActionOnCheck; +} + +// Actions expected on checkbox click +export enum ActionOnCheck { + selectRow = 0, + customAction = 1 +} + +export interface ICheckboxCellActionEventArgs { + checked: boolean; + row: number; + column: number; } const defaultOptions: ICheckboxSelectColumnOptions = { @@ -32,9 +47,16 @@ export class CheckboxSelectColumn implements Slick.Plugin { private _grid: Slick.Grid; private _handler = new Slick.EventHandler(); private _selectedRowsLookup = {}; + private _selectedCheckBoxLookup = {}; + private _useState = false; - constructor(options?: ICheckboxSelectColumnOptions) { + private _onChange = new Emitter(); + public readonly onChange: vsEvent = this._onChange.event; + public index: number; + + constructor(options?: ICheckboxSelectColumnOptions, columnIndex?: number) { this._options = mixin(options, defaultOptions, false); + this.index = columnIndex ? columnIndex : 0; } public init(grid: Slick.Grid): void { @@ -51,6 +73,12 @@ export class CheckboxSelectColumn implements Slick.Plugin { } private handleSelectedRowsChanged(e: Event, args: Slick.OnSelectedRowsChangedEventArgs): void { + if (this.isCustomActionRequested()) { + // do not assume anything for column based on row selection + // we can emit event here later if required. + return; + } + const selectedRows = this._grid.getSelectedRows(); let lookup = {}, row, i; for (i = 0; i < selectedRows.length; i++) { @@ -85,7 +113,12 @@ export class CheckboxSelectColumn implements Slick.Plugin { if (this._grid.getColumns()[args.cell].id === this._options.columnId) { // if editing, try to commit if (!this._grid.getEditorLock().isActive() || this._grid.getEditorLock().commitCurrentEdit()) { - this.toggleRowSelection(args.row); + if (this.isCustomActionRequested()) { + this.toggleCheckBox(args.row, args.cell, true); + } + else { + this.toggleRowSelection(args.row); + } } e.preventDefault(); e.stopImmediatePropagation(); @@ -95,7 +128,12 @@ export class CheckboxSelectColumn implements Slick.Plugin { if (event.equals(KeyCode.Enter)) { // clicking on a row select checkbox if (this._grid.getColumns()[args.cell].id === this._options.columnId) { - this.toggleRowSelection(args.row); + if (this.isCustomActionRequested()) { + this.toggleCheckBox(args.row, args.cell, true); + } + else { + this.toggleRowSelection(args.row); + } e.stopPropagation(); e.stopImmediatePropagation(); } @@ -113,7 +151,12 @@ export class CheckboxSelectColumn implements Slick.Plugin { return; } - this.toggleRowSelection(args.row); + if (this.isCustomActionRequested()) { + this.toggleCheckBox(args.row, args.cell, false); + } + else { + this.toggleRowSelection(args.row); + } e.stopPropagation(); e.stopImmediatePropagation(); } @@ -127,7 +170,30 @@ export class CheckboxSelectColumn implements Slick.Plugin { } } + private toggleCheckBox(row: number, col: number, reRender: boolean): void { + this._useState = true; + + if (this._selectedCheckBoxLookup[row]) { + delete this._selectedCheckBoxLookup[row]; + this._onChange.fire({ checked: false, row: row, column: col }); + } else { + this._selectedCheckBoxLookup[row] = true; + this._onChange.fire({ checked: true, row: row, column: col }); + } + + if (reRender) { + // ensure that grid reflects the change + this._grid.invalidateRow(row); + this._grid.render(); + } + } + private handleHeaderClick(e: Event, args: Slick.OnHeaderClickEventArgs): void { + if (this.isCustomActionRequested()) { + // do not assume action for column based on header click. + // we can emit event here later if required. + return; + } if (!this._options.title && args.column.id === this._options.columnId && jQuery(e.target!).is('input[type="checkbox"]')) { // if editing, try to commit if (this._grid.getEditorLock().isActive() && !this._grid.getEditorLock().commitCurrentEdit()) { @@ -167,8 +233,37 @@ export class CheckboxSelectColumn implements Slick.Plugin { } private checkboxSelectionFormatter(row, cell, value, columnDef: Slick.Column, dataContext): string { + if (this.isCustomActionRequested()) { + return this.checkboxTemplateCustom(row); + } + return this._selectedRowsLookup[row] ? strings.format(checkboxTemplate, 'checked') : strings.format(checkboxTemplate, ''); } + + checkboxTemplateCustom(row: number): string { + // use state after toggles + if (this._useState) { + return this._selectedCheckBoxLookup[row] + ? strings.format(checkboxTemplate, 'checked') + : strings.format(checkboxTemplate, ''); + } + + // use data for first time rendering + // note: make sure Init is called before using this._grid + let rowVal = (this._grid) ? this._grid.getDataItem(row) : null; + if (rowVal && this._options.title && rowVal[this._options.title] === true) { + this._selectedCheckBoxLookup[row] = true; + return strings.format(checkboxTemplate, 'checked'); + } + else { + delete this._selectedCheckBoxLookup[row]; + return strings.format(checkboxTemplate, ''); + } + } + + private isCustomActionRequested(): boolean { + return (this._options.actionOnCheck === ActionOnCheck.customAction); + } } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index a9b7b0dcfc..871a11442d 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -189,7 +189,8 @@ export enum ComponentEventType { validityChanged, onMessage, onSelectedRowChanged, - onComponentCreated + onComponentCreated, + onCellAction, } export interface IComponentEventArgs { @@ -633,4 +634,15 @@ export enum SchemaObjectType { ServerRoleMembership = 63, ServerRoles = 64, ServerTriggers = 65 +} + +export enum ColumnType { + text = 0, + checkBox = 1, + button = 2 +} + +export enum ActionOnCellCheckboxCheck { + selectRow = 0, + customAction = 1 } \ No newline at end of file diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 87f629cb06..8e2bc52909 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -1094,6 +1094,7 @@ class TableComponentWrapper extends ComponentWrapper implements azdata.TableComp super(proxy, handle, ModelComponentTypes.Table, id); this.properties = {}; this._emitterMap.set(ComponentEventType.onSelectedRowChanged, new Emitter()); + this._emitterMap.set(ComponentEventType.onCellAction, new Emitter()); } public get data(): any[][] { @@ -1129,6 +1130,11 @@ class TableComponentWrapper extends ComponentWrapper implements azdata.TableComp let emitter = this._emitterMap.get(ComponentEventType.onSelectedRowChanged); return emitter && emitter.event; } + + public get onCellAction(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onCellAction); + return emitter && emitter.event; + } } class DropDownWrapper extends ComponentWrapper implements azdata.DropDownComponent { diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index c51c000ea2..9c8d91095f 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -542,7 +542,9 @@ export function createApiFactory( SchemaUpdateAction: sqlExtHostTypes.SchemaUpdateAction, SchemaDifferenceType: sqlExtHostTypes.SchemaDifferenceType, SchemaCompareEndpointType: sqlExtHostTypes.SchemaCompareEndpointType, - SchemaObjectType: sqlExtHostTypes.SchemaObjectType + SchemaObjectType: sqlExtHostTypes.SchemaObjectType, + ColumnType: sqlExtHostTypes.ColumnType, + ActionOnCellCheckboxCheck: sqlExtHostTypes.ActionOnCellCheckboxCheck, }; }, diff --git a/src/sql/workbench/electron-browser/modelComponents/interfaces.ts b/src/sql/workbench/electron-browser/modelComponents/interfaces.ts index 53c77d9dd6..bf246361b1 100644 --- a/src/sql/workbench/electron-browser/modelComponents/interfaces.ts +++ b/src/sql/workbench/electron-browser/modelComponents/interfaces.ts @@ -67,7 +67,8 @@ export enum ComponentEventType { validityChanged, onMessage, onSelectedRowChanged, - onComponentCreated + onComponentCreated, + onCellAction, } export interface IModelStore { diff --git a/src/sql/workbench/electron-browser/modelComponents/table.component.ts b/src/sql/workbench/electron-browser/modelComponents/table.component.ts index 641d7a46c2..dd7f5ff7aa 100644 --- a/src/sql/workbench/electron-browser/modelComponents/table.component.ts +++ b/src/sql/workbench/electron-browser/modelComponents/table.component.ts @@ -20,6 +20,8 @@ import { attachTableStyler } from 'sql/platform/theme/common/styler'; import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; import { getContentHeight, getContentWidth, Dimension } from 'vs/base/browser/dom'; import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin'; +import { CheckboxSelectColumn, ICheckboxCellActionEventArgs, ActionOnCheck } from 'sql/base/browser/ui/table/plugins/checkboxSelectColumn.plugin'; +import { Emitter, Event as vsEvent } from 'vs/base/common/event'; @Component({ selector: 'modelview-table', @@ -33,6 +35,9 @@ export default class TableComponent extends ComponentBase implements IComponent, private _table: Table; private _tableData: TableDataView; private _tableColumns; + private _checkboxColumns: CheckboxSelectColumn<{}>[] = []; + private _onCheckBoxChanged = new Emitter(); + public readonly onCheckBoxChanged: vsEvent = this._onCheckBoxChanged.event; @ViewChild('table', { read: ElementRef }) private _inputContainer: ElementRef; constructor( @@ -50,9 +55,14 @@ export default class TableComponent extends ComponentBase implements IComponent, transformColumns(columns: string[] | azdata.TableColumn[]): Slick.Column[] { let tableColumns: any[] = columns; if (tableColumns) { - return (columns).map(col => { - if (col.value) { - return >{ + let mycolumns: Slick.Column[] = []; + let index: number = 0; + (columns).map(col => { + if (col.type && col.type === 1) { + this.createCheckBoxPlugin(col, index); + } + else if (col.value) { + mycolumns.push(>{ name: col.value, id: col.value, field: col.value, @@ -60,15 +70,17 @@ export default class TableComponent extends ComponentBase implements IComponent, cssClass: col.cssClass, headerCssClass: col.headerCssClass, toolTip: col.toolTip - }; + }); } else { - return >{ + mycolumns.push(>{ name: col, id: col, field: col - }; + }); } + index++; }); + return mycolumns; } else { return (columns).map(col => { return >{ @@ -80,6 +92,8 @@ export default class TableComponent extends ComponentBase implements IComponent, } } + + public static transformData(rows: string[][], columns: any[]): { [key: string]: string }[] { if (rows && columns) { return rows.map(row => { @@ -166,10 +180,46 @@ export default class TableComponent extends ComponentBase implements IComponent, this._table.setSelectedRows(this.selectedRows); } + for (let col in this._checkboxColumns) { + this.registerCheckboxPlugin(this._checkboxColumns[col]); + } + this.layoutTable(); this.validate(); } + private createCheckBoxPlugin(col: any, index: number) { + let name = col.value; + if (!this._checkboxColumns[col.value]) { + this._checkboxColumns[col.value] = new CheckboxSelectColumn({ + title: col.value, + toolTip: col.toolTip, + width: col.width, + cssClass: col.cssClass, + actionOnCheck: col.options ? col.options.actionOnCheckbox : null + }, index); + + this._register(this._checkboxColumns[col.value].onChange((state) => { + this.fireEvent({ + eventType: ComponentEventType.onCellAction, + args: { + row: state.row, + column: state.column, + checked: state.checked, + name: name + } + }); + })); + } + } + + private registerCheckboxPlugin(checkboxSelectColumn: CheckboxSelectColumn<{}>): void { + this._tableColumns.splice(checkboxSelectColumn.index, 0, checkboxSelectColumn.getColumnDefinition()); + this._table.registerPlugin(checkboxSelectColumn); + this._table.columns = this._tableColumns; + this._table.autosizeColumns(); + } + // CSS-bound properties public get data(): any[][] {