diff --git a/src/sql/base/browser/ui/designer/designer.ts b/src/sql/base/browser/ui/designer/designer.ts index 432bc8704a..f0cf253ad9 100644 --- a/src/sql/base/browser/ui/designer/designer.ts +++ b/src/sql/base/browser/ui/designer/designer.ts @@ -3,12 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerViewModel, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName } from 'sql/base/browser/ui/designer/interfaces'; +import { DesignerComponentInput, DesignerEditType, DesignerTab, DesignerEdit, DesignerEditIdentifier, DesignerViewModel, DesignerDataPropertyInfo, DesignerTableComponentRowData, DesignerTableProperties, InputBoxProperties, DropDownProperties, CheckBoxProperties, DesignerComponentTypeName, DesignerEditProcessedEventArgs, DesignerStateChangedEventArgs, DesignerAction, DesignerUIState } from 'sql/base/browser/ui/designer/interfaces'; import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; import * as DOM from 'vs/base/browser/dom'; import { Event } from 'vs/base/common/event'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview'; import 'vs/css!./media/designer'; @@ -63,6 +63,8 @@ export class Designer extends Disposable implements IThemable { private _tableCellEditorFactory: TableCellEditorFactory; private _propertiesPane: DesignerPropertiesPane; private _buttons: Button[] = []; + private _inputDisposable: DisposableStore; + private _loadingTimeoutHandle: any; constructor(private readonly _container: HTMLElement, private readonly _contextViewProvider: IContextViewProvider) { @@ -72,8 +74,8 @@ export class Designer extends Disposable implements IThemable { valueGetter: (item, column): string => { return item[column.field].value; }, - valueSetter: async (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column, value: string): Promise => { - await this.handleEdit({ + valueSetter: (context: string, row: number, item: DesignerTableComponentRowData, column: Slick.Column, value: string): void => { + this.handleEdit({ type: DesignerEditType.Update, property: { parentProperty: context, @@ -196,37 +198,127 @@ export class Designer extends Disposable implements IThemable { } - public async setInput(input: DesignerComponentInput): Promise { + public setInput(input: DesignerComponentInput): void { + // Save state + if (this._input) { + this._input.designerUIState = this.getUIState(); + } + + // Clean up + if (this._loadingTimeoutHandle) { + this.stopLoading(); + } + this._buttons = []; + this._componentMap.clear(); + DOM.clearNode(this._topContentContainer); + this._tabbedPanel.clearTabs(); + this._propertiesPane.clear(); + this._inputDisposable?.dispose(); + + + // Initialize with new input this._input = input; - await this.initializeDesignerView(); + this._inputDisposable = new DisposableStore(); + this._inputDisposable.add(this._input.onInitialized(() => { + this.initializeDesigner(); + })); + this._inputDisposable.add(this._input.onEditProcessed((args) => { + this.handleEditProcessedEvent(args); + })); + this._inputDisposable.add(this._input.onStateChange((args) => { + this.handleInputStateChangedEvent(args); + })); + + if (this._input.view === undefined) { + this._input.initialize(); + } else { + this.initializeDesigner(); + } + if (this._input.pendingAction) { + this.updateLoadingStatus(this._input.pendingAction, true, false); + } } - private async initializeDesignerView(): Promise { - this._propertiesPane.clear(); - DOM.clearNode(this._topContentContainer); - // For initialization, we would want to show the loading indicator immediately. - const handle = this.startLoading(localize('designer.loadingDesigner', "Loading designer..."), 0); - const view = await this._input.getView(); - this.stopLoading(handle, localize('designer.loadingDesignerCompleted', "Loading designer completed")); + public override dispose(): void { + super.dispose(); + this._inputDisposable?.dispose(); + } + + private initializeDesigner(): void { + const view = this._input.view; if (view.components) { view.components.forEach(component => { this.createComponent(this._topContentContainer, component, component.propertyName, true, true); }); } - this._tabbedPanel.clearTabs(); view.tabs.forEach(tab => { this._tabbedPanel.pushTab(this.createTabView(tab)); }); this.layoutTabbedPanel(); - await this.updateComponentValues(); + this.updateComponentValues(); + this.restoreUIState(); + } + + private handleEditProcessedEvent(args: DesignerEditProcessedEventArgs): void { + const edit = args.edit; + const result = args.result; + if (result.isValid) { + this._supressEditProcessing = true; + this.updateComponentValues(); + if (edit.type === DesignerEditType.Add) { + // Move focus to the first cell of the newly added row. + const propertyName = edit.property as string; + const tableData = this._input.viewModel[propertyName] as DesignerTableProperties; + const table = this._componentMap.get(propertyName).component as Table; + table.setActiveCell(tableData.data.length - 1, 0); + } + this._supressEditProcessing = false; + } else { + //TODO: add error notification + } + } + + private handleInputStateChangedEvent(args: DesignerStateChangedEventArgs): void { + if (args.previousState.pendingAction !== args.currentState.pendingAction) { + const showLoading = args.currentState.pendingAction !== undefined; + const action = args.currentState.pendingAction || args.previousState.pendingAction; + this.updateLoadingStatus(action, showLoading, true); + } + } + + private updateLoadingStatus(action: DesignerAction, showLoading: boolean, useDelay: boolean): void { + let message; + let timeout; + switch (action) { + case 'save': + message = showLoading ? localize('designer.savingChanges', "Saving changes...") : localize('designer.savingChangesCompleted', "Changes have been saved"); + timeout = 0; + break; + case 'initialize': + message = showLoading ? localize('designer.loadingDesigner', "Loading designer...") : localize('designer.loadingDesignerCompleted', "Designer is loaded"); + timeout = 0; + break; + case 'processEdit': + message = showLoading ? localize('designer.processingChanges', "Processing changes...") : localize('designer.processingChangesCompleted', "Changes have been processed"); + // To make the edit experience smoother, only show the loading indicator if the request is not returning in 500ms. + timeout = 500; + break; + default: + return; + } + if (showLoading) { + this.startLoading(message, useDelay ? timeout : 0); + } else { + this.stopLoading(message); + } } private layoutTabbedPanel() { this._tabbedPanel.layout(new DOM.Dimension(this._tabbedPanelContainer.clientWidth, this._tabbedPanelContainer.clientHeight)); } - private async updatePropertiesPane(newContext: PropertiesPaneObjectContext): Promise { - const viewModel = await this._input.getViewModel(); + private updatePropertiesPane(newContext: PropertiesPaneObjectContext): void { + const viewModel = this._input.viewModel; let type: string; let components: DesignerDataPropertyInfo[]; let inputViewModel: DesignerViewModel; @@ -260,42 +352,25 @@ export class Designer extends Disposable implements IThemable { } } - private async updateComponentValues(): Promise { - const viewModel = await this._input.getViewModel(); + private updateComponentValues(): void { + const viewModel = this._input.viewModel; // data[ScriptPropertyName] -- todo- set the script editor this._componentMap.forEach((value) => { this.setComponentValue(value.defintion, value.component, viewModel); }); - await this.updatePropertiesPane(this._propertiesPane.context ?? 'root'); + this.updatePropertiesPane(this._propertiesPane.context ?? 'root'); } - private async handleEdit(edit: DesignerEdit): Promise { + private handleEdit(edit: DesignerEdit): void { if (this._supressEditProcessing) { return; } - await this.applyEdit(edit); - const handle = this.startLoading(localize('designer.processingChanges', "Processing changes...")); - const result = await this._input.processEdit(edit); - this.stopLoading(handle, localize('designer.processingChangesCompleted', "Processing changes completed")); - if (result.isValid) { - this._supressEditProcessing = true; - await this.updateComponentValues(); - if (edit.type === DesignerEditType.Add) { - // Move focus to the first cell of the newly added row. - const data = await this._input.getViewModel(); - const propertyName = edit.property as string; - const tableData = data[propertyName] as DesignerTableProperties; - const table = this._componentMap.get(propertyName).component as Table; - table.setActiveCell(tableData.data.length - 1, 0); - } - this._supressEditProcessing = false; - } else { - //TODO: add error notification - } + this.applyEdit(edit); + this._input.processEdit(edit); } - private async applyEdit(edit: DesignerEdit): Promise { - const viewModel = await this._input.getViewModel(); + private applyEdit(edit: DesignerEdit): void { + const viewModel = this._input.viewModel; switch (edit.type) { case DesignerEditType.Update: if (typeof edit.property === 'string') { @@ -423,9 +498,9 @@ export class Designer extends Disposable implements IThemable { ariaLabel: inputProperties.title, type: inputProperties.inputType, }); - input.onLoseFocus(async (args) => { + input.onLoseFocus((args) => { if (args.hasChanged) { - await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value }); + this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: args.value }); } }); if (setWidth && inputProperties.width !== undefined) { @@ -440,8 +515,8 @@ export class Designer extends Disposable implements IThemable { const dropdown = new SelectBox(dropdownProperties.values as string[], undefined, this._contextViewProvider, undefined); dropdown.render(dropdownContainer); dropdown.selectElem.style.height = '25px'; - dropdown.onDidSelect(async (e) => { - await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected }); + dropdown.onDidSelect((e) => { + this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: e.selected }); }); component = dropdown; break; @@ -452,8 +527,8 @@ export class Designer extends Disposable implements IThemable { const checkbox = new Checkbox(checkboxContainer, { label: checkboxProperties.title }); - checkbox.onChange(async (newValue) => { - await this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue }); + checkbox.onChange((newValue) => { + this.handleEdit({ type: DesignerEditType.Update, property: editIdentifier, value: newValue }); }); component = checkbox; break; @@ -465,8 +540,8 @@ export class Designer extends Disposable implements IThemable { title: addNewText, secondary: true }); - addRowButton.onDidClick(async () => { - await this.handleEdit({ + addRowButton.onDidClick(() => { + this.handleEdit({ type: DesignerEditType.Add, property: componentDefinition.propertyName, }); @@ -502,8 +577,8 @@ export class Designer extends Disposable implements IThemable { width: propertyDefinition.componentProperties.width as number }); table.registerPlugin(checkboxColumn); - checkboxColumn.onChange(async (e) => { - await this.handleEdit({ + checkboxColumn.onChange((e) => { + this.handleEdit({ type: DesignerEditType.Update, property: { parentProperty: componentDefinition.propertyName, @@ -540,10 +615,9 @@ export class Designer extends Disposable implements IThemable { resizable: false, isFontIcon: true }); - deleteRowColumn.onClick(async (e) => { - const viewModel = await this._input.getViewModel(); - (viewModel[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1); - await this.handleEdit({ + deleteRowColumn.onClick((e) => { + (this._input.viewModel[componentDefinition.propertyName] as DesignerTableProperties).data.splice(e.row, 1); + this.handleEdit({ type: DesignerEditType.Remove, property: componentDefinition.propertyName, value: e.item @@ -555,9 +629,9 @@ export class Designer extends Disposable implements IThemable { table.grid.onBeforeEditCell.subscribe((e, data): boolean => { return data.item[data.column.field].enabled !== false; }); - table.grid.onActiveCellChanged.subscribe(async (e, data) => { + table.grid.onActiveCellChanged.subscribe((e, data) => { if (data.row !== undefined) { - await this.updatePropertiesPane({ + this.updatePropertiesPane({ parentProperty: componentDefinition.propertyName, index: data.row }); @@ -578,21 +652,37 @@ export class Designer extends Disposable implements IThemable { return component; } - private startLoading(message: string, timeout: number = 500): any { - // To make the experience smoother, only show the loading indicator if the request is not returning in 500ms(default value). - return setTimeout(() => { + private startLoading(message: string, timeout: number): void { + this._loadingTimeoutHandle = setTimeout(() => { this._loadingSpinner.loadingMessage = message; this._loadingSpinner.loading = true; - this._container.removeChild(this._verticalSplitViewContainer); + if (this._container.contains(this._verticalSplitViewContainer)) { + this._container.removeChild(this._verticalSplitViewContainer); + } }, timeout); } - private stopLoading(handle: any, message: string): void { - clearTimeout(handle); + private stopLoading(message: string = ''): void { + clearTimeout(this._loadingTimeoutHandle); + this._loadingTimeoutHandle = undefined; if (this._loadingSpinner.loading) { this._loadingSpinner.loadingCompletedMessage = message; this._loadingSpinner.loading = false; - this._container.appendChild(this._verticalSplitViewContainer); + if (!this._container.contains(this._verticalSplitViewContainer)) { + this._container.appendChild(this._verticalSplitViewContainer); + } + } + } + + private getUIState(): DesignerUIState { + return { + activeTabId: this._tabbedPanel.activeTabId + }; + } + + private restoreUIState(): void { + if (this._input.designerUIState) { + this._tabbedPanel.showTab(this._input.designerUIState.activeTabId); } } } diff --git a/src/sql/base/browser/ui/designer/interfaces.ts b/src/sql/base/browser/ui/designer/interfaces.ts index e99e7b305d..4146b5f3b3 100644 --- a/src/sql/base/browser/ui/designer/interfaces.ts +++ b/src/sql/base/browser/ui/designer/interfaces.ts @@ -3,34 +3,50 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { PanelTabIdentifier } from 'sql/base/browser/ui/panel/panel'; import { Event } from 'vs/base/common/event'; export interface DesignerComponentInput { /** * The event that is triggerd when the designer state changes. */ - readonly onStateChange: Event; + readonly onStateChange: Event; + + /** + * The event that is triggerd when the designer information is loaded. + */ + readonly onInitialized: Event; + + /** + * The event that is triggerd when an edit is processed. + */ + readonly onEditProcessed: Event; /** * Gets the object type display name. */ - readonly objectTypeDisplayName: string; + /** * Gets the designer view specification. */ - getView(): Promise; + readonly view: DesignerView; /** * Gets the view model. */ - getViewModel(): Promise; + readonly viewModel: DesignerViewModel; /** - * Process the edit made in the designer. + * Start initilizing the designer input object. + */ + initialize(): void; + + /** + * Start processing the edit made in the designer, the OnEditProcessed event will be fired when the processing is done. * @param edit the information about the edit. */ - processEdit(edit: DesignerEdit): Promise; + processEdit(edit: DesignerEdit): void; /** * A boolean value indicating whether the current state is valid. @@ -43,16 +59,35 @@ export interface DesignerComponentInput { readonly dirty: boolean; /** - * A boolean value indicating whether the changes are being saved. + * Current in progress action. */ - readonly saving: boolean; + readonly pendingAction?: DesignerAction; + + /** + * The UI state of the designer, used to restore the state. + */ + designerUIState?: DesignerUIState; } +export interface DesignerUIState { + activeTabId: PanelTabIdentifier; +} + +export type DesignerAction = 'save' | 'initialize' | 'processEdit'; + +export interface DesignerEditProcessedEventArgs { + result: DesignerEditResult; + edit: DesignerEdit +} + +export interface DesignerStateChangedEventArgs { + currentState: DesignerState, + previousState: DesignerState +} export interface DesignerState { valid: boolean; dirty: boolean; - saving: boolean; - processing: boolean; + pendingAction?: DesignerAction } export const NameProperty = 'name'; diff --git a/src/sql/base/browser/ui/panel/panel.ts b/src/sql/base/browser/ui/panel/panel.ts index ace5138f56..90bc1ba0b2 100644 --- a/src/sql/base/browser/ui/panel/panel.ts +++ b/src/sql/base/browser/ui/panel/panel.ts @@ -108,6 +108,10 @@ export class TabbedPanel extends Disposable { return this.parent; } + public get activeTabId(): string | undefined { + return this._shownTabId; + } + public override dispose() { this.header.remove(); this.tabList.remove(); diff --git a/src/sql/base/browser/ui/table/tableCellEditorFactory.ts b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts index 5b3eec8496..804216efb2 100644 --- a/src/sql/base/browser/ui/table/tableCellEditorFactory.ts +++ b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts @@ -7,7 +7,7 @@ import * as DOM from 'vs/base/browser/dom'; export interface ITableCellEditorOptions { valueGetter?: (item: Slick.SlickData, column: Slick.Column) => string, - valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column, value: string) => Promise, + valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column, value: string) => void, optionsGetter?: (item: Slick.SlickData, column: Slick.Column) => string[], editorStyler: (component: InputBox | SelectBox) => void } @@ -72,9 +72,9 @@ export class TableCellEditorFactory { this._input.value = this._originalValue; } - public async applyValue(item: Slick.SlickData, state: string): Promise { + public applyValue(item: Slick.SlickData, state: string): void { const activeCell = this._args.grid.getActiveCell(); - await self._options.valueSetter(context, activeCell.row, item, this._args.column, state); + self._options.valueSetter(context, activeCell.row, item, this._args.column, state); } public isValueChanged(): boolean { diff --git a/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts index 3bc4c83105..a384dc108d 100644 --- a/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts +++ b/src/sql/workbench/browser/editor/tableDesigner/tableDesignerInput.ts @@ -29,7 +29,9 @@ export class TableDesignerInput extends EditorInput { super(); this._designerComponentInput = this._instantiationService.createInstance(TableDesignerComponentInput, this._provider, this._tableInfo); this._register(this._designerComponentInput.onStateChange((e) => { - this._onDidChangeDirty.fire(); + if (e.currentState.dirty !== e.previousState.dirty) { + this._onDidChangeDirty.fire(); + } })); const existingNames = editorService.editors.map(editor => editor.getName()); @@ -66,7 +68,7 @@ export class TableDesignerInput extends EditorInput { } override isSaving(): boolean { - return this._designerComponentInput.saving; + return this._designerComponentInput.pendingAction === 'save'; } override async save(group: GroupIdentifier, options?: ISaveOptions): Promise { diff --git a/src/sql/workbench/contrib/tableDesigner/browser/actions.ts b/src/sql/workbench/contrib/tableDesigner/browser/actions.ts index b8bf70a6a9..328896ccc2 100644 --- a/src/sql/workbench/contrib/tableDesigner/browser/actions.ts +++ b/src/sql/workbench/contrib/tableDesigner/browser/actions.ts @@ -34,7 +34,7 @@ export class SaveTableChangesAction extends Action { } private updateState(): void { - this.enabled = this._input.dirty && this._input.valid && !this._input.processing; + this.enabled = this._input.dirty && this._input.valid && this._input.pendingAction === undefined; } override dispose() { diff --git a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts index 4bc6f2fa04..ec39e82090 100644 --- a/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts +++ b/src/sql/workbench/services/tableDesigner/browser/tableDesignerComponentInput.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as azdata from 'azdata'; -import { DesignerViewModel, DesignerEdit, DesignerEditResult, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerState } from 'sql/base/browser/ui/designer/interfaces'; +import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs } from 'sql/base/browser/ui/designer/interfaces'; import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface'; import { localize } from 'vs/nls'; import { designers } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -17,11 +17,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput { private _view: DesignerView; private _valid: boolean = true; private _dirty: boolean = false; - private _saving: boolean = false; - private _processing: boolean = false; - private _onStateChange = new Emitter(); + private _pendingAction?: DesignerAction = undefined; + private _onStateChange = new Emitter(); + private _onInitialized = new Emitter(); + private _onEditProcessed = new Emitter(); - public readonly onStateChange: Event = this._onStateChange.event; + public readonly onInitialized: Event = this._onInitialized.event; + public readonly onEditProcessed: Event = this._onEditProcessed.event; + public readonly onStateChange: Event = this._onStateChange.event; constructor(private readonly _provider: TableDesignerProvider, private _tableInfo: azdata.designers.TableInfo, @@ -36,43 +39,44 @@ export class TableDesignerComponentInput implements DesignerComponentInput { return this._dirty; } - get saving(): boolean { - return this._saving; - } - - get processing(): boolean { - return this._processing; + get pendingAction(): DesignerAction | undefined { + return this._pendingAction; } get objectTypeDisplayName(): string { return localize('tableDesigner.tableObjectType', "Table"); } - async getView(): Promise { - if (!this._view) { - await this.initialize(); - } + get view(): DesignerView { return this._view; } - async getViewModel(): Promise { - if (!this._viewModel) { - await this.initialize(); - } + get viewModel(): DesignerViewModel { return this._viewModel; } - async processEdit(edit: DesignerEdit): Promise { - this.updateState(this.valid, this.dirty, this.saving, true); - const result = await this._provider.processTableEdit(this._tableInfo, this._viewModel!, edit); - if (result.isValid) { - this._viewModel = result.viewModel; - } - this.updateState(result.isValid, true, this.saving, false); - return { - isValid: result.isValid, - errors: result.errors - }; + processEdit(edit: DesignerEdit): void { + this.updateState(this.valid, this.dirty, 'processEdit'); + this._provider.processTableEdit(this._tableInfo, this._viewModel!, edit).then( + result => { + if (result.isValid) { + this._viewModel = result.viewModel; + } + this.updateState(result.isValid, true, undefined); + + this._onEditProcessed.fire({ + edit: edit, + result: { + isValid: result.isValid, + errors: result.errors + } + }); + }, + error => { + this._notificationService.error(localize('tableDesigner.errorProcessingEdit', "An error occured while processing the change: {0}", error?.message ?? error)); + this.updateState(this.valid, this.dirty); + } + ); } async save(): Promise { @@ -81,40 +85,61 @@ export class TableDesignerComponentInput implements DesignerComponentInput { message: localize('tableDesigner.savingChanges', "Saving table designer changes...") }); try { - this.updateState(this.valid, this.dirty, true, true); + this.updateState(this.valid, this.dirty, 'save'); await this._provider.saveTable(this._tableInfo, this._viewModel); - this.updateState(true, false, false, false); + this.updateState(true, false); notificationHandle.updateMessage(localize('tableDesigner.savedChangeSuccess', "The changes have been successfully saved.")); } catch (error) { notificationHandle.updateSeverity(Severity.Error); notificationHandle.updateMessage(localize('tableDesigner.saveChangeError', "An error occured while saving changes: {0}", error?.message ?? error)); - this.updateState(this.valid, this.dirty, false, false); + this.updateState(this.valid, this.dirty); } } async revert(): Promise { - this.updateState(true, false, false, false); + this.updateState(true, false); } - private updateState(valid: boolean, dirty: boolean, saving: boolean, processing: boolean): void { - if (this._dirty !== dirty || this._valid !== valid || this._saving !== saving || this._processing !== processing) { - this._dirty = dirty; - this._valid = valid; - this._saving = saving; - this._processing = processing; - this._onStateChange.fire({ + private updateState(valid: boolean, dirty: boolean, pendingAction?: DesignerAction): void { + if (this._dirty !== dirty || this._valid !== valid || this._pendingAction !== pendingAction) { + const previousState = { valid: this._valid, dirty: this._dirty, - saving: this._saving, - processing: this._processing + pendingAction: this._pendingAction + }; + + this._dirty = dirty; + this._valid = valid; + this._pendingAction = pendingAction; + + const currentState = { + valid: this._valid, + dirty: this._dirty, + pendingAction: this._pendingAction + }; + this._onStateChange.fire({ + currentState, + previousState, }); } } - private async initialize(): Promise { - this.updateState(this.valid, this.dirty, this.saving, true); - const designerInfo = await this._provider.getTableDesignerInfo(this._tableInfo); - this.updateState(this.valid, this.dirty, this.saving, false); + initialize(): void { + if (this._view !== undefined || this.pendingAction === 'initialize') { + return; + } + + this.updateState(this.valid, this.dirty, 'initialize'); + this._provider.getTableDesignerInfo(this._tableInfo).then(result => { + this.doInitialization(result); + this._onInitialized.fire(); + }, error => { + this._notificationService.error(localize('tableDesigner.errorInitializingTableDesigner', "An error occured while initializing the table designer: {0}", error?.message ?? error)); + }); + } + + private doInitialization(designerInfo: azdata.designers.TableDesignerInfo): void { + this.updateState(true, false); this._viewModel = designerInfo.viewModel; this.setDefaultData();