mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-18 09:35:39 -05:00
handle edit and save race condition (#20462)
* handle edit and save race condition * handle more race condition scenarios * fix error
This commit is contained in:
@@ -51,6 +51,8 @@ import { RowMoveManager, RowMoveOnDragEventData } from 'sql/base/browser/ui/tabl
|
||||
import { ITaskbarContent, Taskbar } from 'sql/base/browser/ui/taskbar/taskbar';
|
||||
import { RowSelectionModel } from 'sql/base/browser/ui/table/plugins/rowSelectionModel.plugin';
|
||||
import { listFocusAndSelectionBackground } from 'sql/platform/theme/common/colors';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
|
||||
export interface IDesignerStyle {
|
||||
tabbedPanelStyles?: ITabbedPanelStyles;
|
||||
@@ -282,6 +284,9 @@ export class Designer extends Disposable implements IThemable {
|
||||
|
||||
|
||||
public setInput(input: DesignerComponentInput): void {
|
||||
if (this._input) {
|
||||
void this.submitPendingChanges().catch(onUnexpectedError);
|
||||
}
|
||||
this.saveUIState();
|
||||
if (this._loadingTimeoutHandle) {
|
||||
this.stopLoading();
|
||||
@@ -303,7 +308,9 @@ export class Designer extends Disposable implements IThemable {
|
||||
this._inputDisposable.add(this._input.onRefreshRequested(() => {
|
||||
this.refresh();
|
||||
}));
|
||||
|
||||
this._inputDisposable.add(this._input.onSubmitPendingEditRequested(async () => {
|
||||
await this.submitPendingChanges();
|
||||
}));
|
||||
if (this._input.view === undefined) {
|
||||
this._input.initialize();
|
||||
} else {
|
||||
@@ -319,6 +326,14 @@ export class Designer extends Disposable implements IThemable {
|
||||
this._inputDisposable?.dispose();
|
||||
}
|
||||
|
||||
public async submitPendingChanges(): Promise<void> {
|
||||
if (this._container.contains(document.activeElement) && document.activeElement instanceof HTMLInputElement) {
|
||||
// Force the elements to fire the blur event to submit the pending changes.
|
||||
document.activeElement.blur();
|
||||
return timeout(10);
|
||||
}
|
||||
}
|
||||
|
||||
private clearUI(): void {
|
||||
this._componentMap.forEach(item => item.component.dispose());
|
||||
this._componentMap.clear();
|
||||
|
||||
@@ -28,6 +28,11 @@ export interface DesignerComponentInput {
|
||||
*/
|
||||
readonly onRefreshRequested: Event<void>;
|
||||
|
||||
/**
|
||||
* The event that is triggerd when force submit of the pending edit is requested.
|
||||
*/
|
||||
readonly onSubmitPendingEditRequested: Event<void>;
|
||||
|
||||
/**
|
||||
* Gets the object type display name.
|
||||
*/
|
||||
|
||||
@@ -21,6 +21,7 @@ export class DesignerTableAction extends Action {
|
||||
protected _table: Table<Slick.SlickData>;
|
||||
|
||||
constructor(
|
||||
private _designer: Designer,
|
||||
id: string,
|
||||
label: string,
|
||||
icon: string,
|
||||
@@ -43,6 +44,10 @@ export class DesignerTableAction extends Action {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||
await this._designer.submitPendingChanges();
|
||||
}
|
||||
}
|
||||
|
||||
export class AddRowAction extends DesignerTableAction {
|
||||
@@ -54,12 +59,13 @@ export class AddRowAction extends DesignerTableAction {
|
||||
private designer: Designer,
|
||||
tableProperties: DesignerTableProperties,
|
||||
) {
|
||||
super(AddRowAction.ID, tableProperties.labelForAddNewButton || AddRowAction.LABEL, AddRowAction.ICON, false);
|
||||
super(designer, AddRowAction.ID, tableProperties.labelForAddNewButton || AddRowAction.LABEL, AddRowAction.ICON, false);
|
||||
this.designer = designer;
|
||||
this._tooltip = localize('designer.newRowButtonAriaLabel', "Add new row to '{0}' table", tableProperties.ariaLabel);
|
||||
}
|
||||
|
||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||
await super.run(context);
|
||||
const lastIndex = context.table.getData().getItems().length;
|
||||
return new Promise((resolve) => {
|
||||
this.designer.handleEdit({
|
||||
@@ -78,13 +84,14 @@ export class MoveRowUpAction extends DesignerTableAction {
|
||||
public static LABEL = localize('designer.moveRowUpAction', 'Move Up');
|
||||
|
||||
constructor(private designer: Designer) {
|
||||
super(MoveRowUpAction.ID, MoveRowUpAction.LABEL, MoveRowUpAction.ICON, true);
|
||||
super(designer, MoveRowUpAction.ID, MoveRowUpAction.LABEL, MoveRowUpAction.ICON, true);
|
||||
this.designer = designer;
|
||||
this._tooltip = localize('designer.moveRowUpButtonAriaLabel', "Move selected row up one position");
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||
await super.run(context);
|
||||
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
||||
if (rowIndex - 1 < 0) {
|
||||
return;
|
||||
@@ -116,13 +123,14 @@ export class MoveRowDownAction extends DesignerTableAction {
|
||||
public static LABEL = localize('designer.moveRowDownAction', 'Move Down');
|
||||
|
||||
constructor(private designer: Designer) {
|
||||
super(MoveRowDownAction.ID, MoveRowDownAction.LABEL, MoveRowDownAction.ICON, true);
|
||||
super(designer, MoveRowDownAction.ID, MoveRowDownAction.LABEL, MoveRowDownAction.ICON, true);
|
||||
this.designer = designer;
|
||||
this._tooltip = localize('designer.moveRowDownButtonAriaLabel', "Move selected row down one position");
|
||||
this.enabled = false;
|
||||
}
|
||||
|
||||
public override async run(context: DesignerTableActionContext): Promise<void> {
|
||||
await super.run(context);
|
||||
let rowIndex = context.selectedRow ?? context.table.getSelectedRows()[0];
|
||||
const tableData = context.table.getData().getItems();
|
||||
if (rowIndex + 1 >= tableData.length) {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as azdata from 'azdata';
|
||||
import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerIssue, ScriptProperty } from 'sql/workbench/browser/designer/interfaces';
|
||||
import { DesignerViewModel, DesignerEdit, DesignerComponentInput, DesignerView, DesignerTab, DesignerDataPropertyInfo, DropDownProperties, DesignerTableProperties, DesignerEditProcessedEventArgs, DesignerAction, DesignerStateChangedEventArgs, DesignerPropertyPath, DesignerIssue, ScriptProperty, DesignerUIState } from 'sql/workbench/browser/designer/interfaces';
|
||||
import { TableDesignerProvider } from 'sql/workbench/services/tableDesigner/common/interface';
|
||||
import { localize } from 'vs/nls';
|
||||
import { designers } from 'sql/workbench/api/common/sqlExtHostTypes';
|
||||
@@ -18,6 +18,7 @@ import { IAdsTelemetryService, ITelemetryEventProperties } from 'sql/platform/te
|
||||
import { TelemetryAction, TelemetryView } from 'sql/platform/telemetry/common/telemetryKeys';
|
||||
import { IErrorMessageService } from 'sql/platform/errorMessage/common/errorMessageService';
|
||||
import { TableDesignerMetadata } from 'sql/workbench/services/tableDesigner/browser/tableDesignerMetadata';
|
||||
import { Queue, timeout } from 'vs/base/common/async';
|
||||
|
||||
const ErrorDialogTitle: string = localize('tableDesigner.ErrorDialogTitle', "Table Designer Error");
|
||||
export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
@@ -32,13 +33,20 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
private _onInitialized = new Emitter<void>();
|
||||
private _onEditProcessed = new Emitter<DesignerEditProcessedEventArgs>();
|
||||
private _onRefreshRequested = new Emitter<void>();
|
||||
private _onSubmitPendingEditRequested = new Emitter<void>();
|
||||
private _originalViewModel: DesignerViewModel;
|
||||
private _tableDesignerView: azdata.designers.TableDesignerView;
|
||||
private _activeEditPromise: Promise<void>;
|
||||
private _isEditInProgress: boolean = false;
|
||||
private _recentEditAccepted: boolean = true;
|
||||
private _editQueue: Queue<void> = new Queue<void>();
|
||||
|
||||
public readonly onInitialized: Event<void> = this._onInitialized.event;
|
||||
public readonly onEditProcessed: Event<DesignerEditProcessedEventArgs> = this._onEditProcessed.event;
|
||||
public readonly onStateChange: Event<DesignerStateChangedEventArgs> = this._onStateChange.event;
|
||||
public readonly onRefreshRequested: Event<void> = this._onRefreshRequested.event;
|
||||
public readonly onSubmitPendingEditRequested: Event<void> = this._onSubmitPendingEditRequested.event;
|
||||
|
||||
|
||||
private readonly designerEditTypeDisplayValue: { [key: number]: string } = {
|
||||
0: 'Add', 1: 'Remove', 2: 'Update'
|
||||
@@ -54,6 +62,8 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
@IErrorMessageService private readonly _errorMessageService: IErrorMessageService) {
|
||||
}
|
||||
|
||||
public designerUIState?: DesignerUIState = undefined;
|
||||
|
||||
get valid(): boolean {
|
||||
return this._valid;
|
||||
}
|
||||
@@ -87,44 +97,11 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
}
|
||||
|
||||
processEdit(edit: DesignerEdit): void {
|
||||
const telemetryInfo = this.createTelemetryInfo();
|
||||
telemetryInfo.tableObjectType = this.getObjectTypeFromPath(edit.path);
|
||||
const editAction = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner,
|
||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo);
|
||||
const startTime = new Date().getTime();
|
||||
this.updateState(this.valid, this.dirty, 'processEdit');
|
||||
this._provider.processTableEdit(this.tableInfo, edit).then(
|
||||
result => {
|
||||
if (result.inputValidationError) {
|
||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.inputValidationError', "The input validation failed with error: {0}", result.inputValidationError));
|
||||
}
|
||||
this._viewModel = result.viewModel;
|
||||
if (result.view) {
|
||||
this.setDesignerView(result.view);
|
||||
}
|
||||
this._issues = result.issues;
|
||||
this.updateState(result.isValid, this.isDirty(), undefined);
|
||||
|
||||
this._onEditProcessed.fire({
|
||||
edit: edit,
|
||||
result: {
|
||||
isValid: result.isValid,
|
||||
issues: result.issues,
|
||||
refreshView: !!result.view
|
||||
}
|
||||
});
|
||||
const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, result.metadata);
|
||||
editAction.withAdditionalMeasurements({
|
||||
'elapsedTimeMs': new Date().getTime() - startTime
|
||||
}).withAdditionalProperties(metadataTelemetryInfo).send();
|
||||
},
|
||||
error => {
|
||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorProcessingEdit', "An error occured while processing the change: {0}", error?.message ?? error), error?.data);
|
||||
this.updateState(this.valid, this.dirty);
|
||||
this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner,
|
||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo).send();
|
||||
}
|
||||
);
|
||||
// If there is already an edit being processed, the new edit will be skipped if the previous edit is not accepted.
|
||||
const checkPreviousEditResult = this._editQueue.size !== 0;
|
||||
this._editQueue.queue(async () => {
|
||||
await this.doProcessEdit(edit, checkPreviousEditResult);
|
||||
});
|
||||
}
|
||||
|
||||
async generateScript(): Promise<void> {
|
||||
@@ -183,6 +160,14 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
}
|
||||
|
||||
async save(): Promise<void> {
|
||||
this._onSubmitPendingEditRequested.fire();
|
||||
await timeout(10);
|
||||
if (this._isEditInProgress) {
|
||||
await this._activeEditPromise;
|
||||
}
|
||||
if (!this.valid || !this._recentEditAccepted) {
|
||||
return;
|
||||
}
|
||||
if (!this.isDirty()) {
|
||||
return;
|
||||
}
|
||||
@@ -235,6 +220,60 @@ export class TableDesignerComponentInput implements DesignerComponentInput {
|
||||
this.updateState(true, false);
|
||||
}
|
||||
|
||||
private async doProcessEdit(edit: DesignerEdit, checkPreviousEditResult: boolean): Promise<void> {
|
||||
if (checkPreviousEditResult && !this._recentEditAccepted) {
|
||||
return;
|
||||
}
|
||||
const telemetryInfo = this.createTelemetryInfo();
|
||||
telemetryInfo.tableObjectType = this.getObjectTypeFromPath(edit.path);
|
||||
const editAction = this._adsTelemetryService.createActionEvent(TelemetryView.TableDesigner,
|
||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo);
|
||||
const startTime = new Date().getTime();
|
||||
this.updateState(this.valid, this.dirty, 'processEdit');
|
||||
this._activeEditPromise = new Promise(async (resolve) => {
|
||||
this._isEditInProgress = true;
|
||||
this._recentEditAccepted = true;
|
||||
try {
|
||||
const result = await this._provider.processTableEdit(this.tableInfo, edit);
|
||||
if (result.inputValidationError) {
|
||||
this._recentEditAccepted = false;
|
||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.inputValidationError', "The input validation failed with error: {0}", result.inputValidationError));
|
||||
}
|
||||
this._viewModel = result.viewModel;
|
||||
if (result.view) {
|
||||
this.setDesignerView(result.view);
|
||||
}
|
||||
this._issues = result.issues;
|
||||
this.updateState(result.isValid, this.isDirty(), undefined);
|
||||
|
||||
this._onEditProcessed.fire({
|
||||
edit: edit,
|
||||
result: {
|
||||
isValid: result.isValid,
|
||||
issues: result.issues,
|
||||
refreshView: !!result.view
|
||||
}
|
||||
});
|
||||
const metadataTelemetryInfo = TableDesignerMetadata.getTelemetryInfo(this._provider.providerId, result.metadata);
|
||||
editAction.withAdditionalMeasurements({
|
||||
'elapsedTimeMs': new Date().getTime() - startTime
|
||||
}).withAdditionalProperties(metadataTelemetryInfo).send();
|
||||
}
|
||||
catch (error) {
|
||||
this._errorMessageService.showDialog(Severity.Error, ErrorDialogTitle, localize('tableDesigner.errorProcessingEdit', "An error occured while processing the change: {0}", error?.message ?? error), error?.data);
|
||||
this.updateState(this.valid, this.dirty);
|
||||
this._adsTelemetryService.createErrorEvent(TelemetryView.TableDesigner,
|
||||
this.designerEditTypeDisplayValue[edit.type]).withAdditionalProperties(telemetryInfo).send();
|
||||
this._recentEditAccepted = false;
|
||||
}
|
||||
finally {
|
||||
this._isEditInProgress = false;
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
return this._activeEditPromise;
|
||||
}
|
||||
|
||||
private updateState(valid: boolean, dirty: boolean, pendingAction?: DesignerAction): void {
|
||||
if (this._dirty !== dirty || this._valid !== valid || this._pendingAction !== pendingAction) {
|
||||
const previousState = {
|
||||
|
||||
Reference in New Issue
Block a user