diff --git a/src/sql/platform/dialog/dialogModal.ts b/src/sql/platform/dialog/dialogModal.ts index 5d0ef52281..25b64d06f3 100644 --- a/src/sql/platform/dialog/dialogModal.ts +++ b/src/sql/platform/dialog/dialogModal.ts @@ -120,11 +120,13 @@ export class DialogModal extends Modal { this.show(); } - public done(): void { + public async done(): Promise { if (this._dialog.okButton.enabled) { - this._onDone.fire(); - this.dispose(); - this.hide(); + if (await this._dialog.validateClose()) { + this._onDone.fire(); + this.dispose(); + this.hide(); + } } } diff --git a/src/sql/platform/dialog/dialogTypes.ts b/src/sql/platform/dialog/dialogTypes.ts index 4a98c6377c..9c21640447 100644 --- a/src/sql/platform/dialog/dialogTypes.ts +++ b/src/sql/platform/dialog/dialogTypes.ts @@ -49,6 +49,7 @@ export class Dialog extends ModelViewPane { private _onMessageChange = new Emitter(); public readonly onMessageChange = this._onMessageChange.event; private _message: DialogMessage; + private _closeValidator: () => boolean | Thenable; constructor(public title: string, content?: string | DialogTab[]) { super(); @@ -67,6 +68,18 @@ export class Dialog extends ModelViewPane { this._onMessageChange.fire(this._message); } } + + public registerCloseValidator(validator: () => boolean | Thenable): void { + this._closeValidator = validator; + } + + public validateClose(): Thenable { + if (this._closeValidator) { + return Promise.resolve(this._closeValidator()); + } else { + return Promise.resolve(true); + } + } } export class DialogButton implements sqlops.window.modelviewdialog.Button { diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index a49f238be9..517d7cba0c 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -660,6 +660,15 @@ declare module 'sqlops' { * undefined or the text is empty or undefined. The default level is error. */ message: DialogMessage; + + /** + * Register a callback that will be called when the user tries to click done. Only + * one callback can be registered at once, so each registration call will clear + * the previous registration. + * @param validator The callback that gets executed when the user tries to click + * done. Return true to allow the dialog to close or false to block it from closing + */ + registerCloseValidator(validator: () => boolean | Thenable): void; } export interface DialogTab extends ModelViewPanel { diff --git a/src/sql/workbench/api/node/extHostModelViewDialog.ts b/src/sql/workbench/api/node/extHostModelViewDialog.ts index 7e0d7bd622..94f7755807 100644 --- a/src/sql/workbench/api/node/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/node/extHostModelViewDialog.ts @@ -94,6 +94,7 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi public cancelButton: sqlops.window.modelviewdialog.Button; public customButtons: sqlops.window.modelviewdialog.Button[]; private _message: sqlops.window.modelviewdialog.DialogMessage; + private _closeValidator: () => boolean | Thenable; constructor(extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape) { @@ -115,6 +116,18 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi this._message = value; this._extHostModelViewDialog.updateDialogContent(this); } + + public registerCloseValidator(validator: () => boolean | Thenable): void { + this._closeValidator = validator; + } + + public validateClose(): Thenable { + if (this._closeValidator) { + return Promise.resolve(this._closeValidator()); + } else { + return Promise.resolve(true); + } + } } class TabImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdialog.DialogTab { @@ -374,6 +387,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { return wizard.validateNavigation(info); } + public $validateDialogClose(handle: number): Thenable { + let dialog = this._objectsByHandle.get(handle) as DialogImpl; + return dialog.validateClose(); + } + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { let handle = this.getHandle(dialog); this.updateDialogContent(dialog); diff --git a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts index ff5748b298..94f7c807db 100644 --- a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts @@ -80,6 +80,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape dialog.okButton = okButton; dialog.cancelButton = cancelButton; dialog.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); + dialog.registerCloseValidator(() => this.validateDialogClose(handle)); this._dialogs.set(handle, dialog); } @@ -262,4 +263,8 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape private validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable { return this._proxy.$validateNavigation(handle, info); } + + private validateDialogClose(handle: number): Thenable { + return this._proxy.$validateDialogClose(handle); + } } \ No newline at end of file diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 7aa7e90383..f73a1aafb7 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -563,6 +563,7 @@ export interface ExtHostModelViewDialogShape { $onWizardPageChanged(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): void; $updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void; $validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable; + $validateDialogClose(handle: number): Thenable; } export interface MainThreadModelViewDialogShape extends IDisposable { diff --git a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts index b109232a65..540e8de0f4 100644 --- a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts @@ -307,4 +307,23 @@ suite('ExtHostModelViewDialog Tests', () => { // Then the main thread gets notified of the new details mockProxy.verify(x => x.$setWizardDetails(It.isAny(), It.is(x => x.message === newMessage)), Times.once()); }); + + test('Main thread can execute dialog close validation', () => { + // Set up the main thread mock to record the dialog handle + let dialogHandle: number; + mockProxy.setup(x => x.$setDialogDetails(It.isAny(), It.isAny())).callback((handle, details) => dialogHandle = handle); + + // Create the dialog and add a validation that records that it has been called + let dialog = extHostModelViewDialog.createDialog('dialog_1'); + extHostModelViewDialog.updateDialogContent(dialog); + let callCount = 0; + dialog.registerCloseValidator(() => { + callCount++; + return true; + }); + + // If I call the validation from the main thread then it should run + extHostModelViewDialog.$validateDialogClose(dialogHandle); + assert.equal(callCount, 1); + }); }); \ No newline at end of file diff --git a/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts b/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts index e4b00138b8..0750b4a04c 100644 --- a/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts @@ -60,7 +60,8 @@ suite('MainThreadModelViewDialog Tests', () => { $onPanelValidityChanged: (handle, valid) => undefined, $onWizardPageChanged: (handle, info) => undefined, $updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined, - $validateNavigation: (handle, info) => undefined + $validateNavigation: (handle, info) => undefined, + $validateDialogClose: handle => undefined }); let extHostContext = { getProxy: proxyType => mockExtHostModelViewDialog.object @@ -347,4 +348,15 @@ suite('MainThreadModelViewDialog Tests', () => { assert.equal(newMessage, wizardDetails.message, 'New message was not included in the fired event'); assert.equal(openedWizard.message, wizardDetails.message, 'New message was not set on the wizard'); }); + + test('Creating a dialog adds a close validation that calls the extension host', () => { + mockExtHostModelViewDialog.setup(x => x.$validateDialogClose(It.isAny())); + + // If I call validateClose on the dialog that gets created + mainThreadModelViewDialog.$openDialog(dialogHandle); + openedDialog.validateClose(); + + // Then the call gets forwarded to the extension host + mockExtHostModelViewDialog.verify(x => x.$validateDialogClose(It.is(handle => handle === dialogHandle)), Times.once()); + }); }); \ No newline at end of file