diff --git a/src/sql/platform/dialog/dialogTypes.ts b/src/sql/platform/dialog/dialogTypes.ts index e60d8ade58..ffd3583dd6 100644 --- a/src/sql/platform/dialog/dialogTypes.ts +++ b/src/sql/platform/dialog/dialogTypes.ts @@ -139,6 +139,7 @@ export class Wizard { public readonly onPageAdded = this._pageAddedEmitter.event; private _pageRemovedEmitter = new Emitter(); public readonly onPageRemoved = this._pageRemovedEmitter.event; + private _navigationValidator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable; constructor(public title: string) { } @@ -191,4 +192,19 @@ export class Wizard { this.pages.splice(index, 1); this._pageRemovedEmitter.fire(removedPage); } + + public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable): void { + this._navigationValidator = validator; + } + + public validateNavigation(newPage: number): Thenable { + if (this._navigationValidator) { + return Promise.resolve(this._navigationValidator({ + lastPage: this._currentPage, + newPage: newPage + })); + } else { + return Promise.resolve(true); + } + } } \ No newline at end of file diff --git a/src/sql/platform/dialog/wizardModal.ts b/src/sql/platform/dialog/wizardModal.ts index 0efa2070aa..6960991dd7 100644 --- a/src/sql/platform/dialog/wizardModal.ts +++ b/src/sql/platform/dialog/wizardModal.ts @@ -106,12 +106,12 @@ export class WizardModal extends Modal { }); this._wizard.onPageAdded(page => { this.registerPage(page); - this.showPage(this.getCurrentPage()); + this.showPage(this.getCurrentPage(), false); }); this._wizard.onPageRemoved(page => { let dialogPane = this._dialogPanes.get(page); this._dialogPanes.delete(page); - this.showPage(this.getCurrentPage()); + this.showPage(this.getCurrentPage(), false); dialogPane.dispose(); }); } @@ -123,10 +123,13 @@ export class WizardModal extends Modal { page.onUpdate(() => this.setButtonsForPage(this._wizard.currentPage)); } - private showPage(index: number): void { + private async showPage(index: number, validate: boolean = true): Promise { let pageToShow = this._wizard.pages[index]; if (!pageToShow) { - this.done(); + this.done(validate); + return; + } + if (validate && !await this._wizard.validateNavigation(index)) { return; } this._dialogPanes.forEach((dialogPane, page) => { @@ -163,12 +166,15 @@ export class WizardModal extends Modal { } public open(): void { - this.showPage(0); + this.showPage(0, false); this.show(); } - public done(): void { + public async done(validate: boolean = true): Promise { if (this._wizard.doneButton.enabled) { + if (validate && !await this._wizard.validateNavigation(undefined)) { + return; + } this._onDone.fire(); this.dispose(); this.hide(); diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 7e5952c98c..edb345ddd0 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -621,7 +621,7 @@ declare module 'sqlops' { lastPage: number, /** - * The new page number + * The new page number or undefined if the user is closing the wizard */ newPage: number } @@ -734,6 +734,16 @@ declare module 'sqlops' { * Close the wizard. Does nothing if the wizard is not open. */ close(): Thenable; + + /** + * Register a callback that will be called when the user tries to navigate by + * changing pages or clicking 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 + * navigate. Return true to allow the navigation to proceed, or false to + * cancel it. + */ + registerNavigationValidator(validator: (pageChangeInfo: WizardPageChangeInfo) => boolean | Thenable): void; } } } diff --git a/src/sql/workbench/api/node/extHostModelViewDialog.ts b/src/sql/workbench/api/node/extHostModelViewDialog.ts index c351c349de..701f68d6a7 100644 --- a/src/sql/workbench/api/node/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/node/extHostModelViewDialog.ts @@ -217,6 +217,7 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard { public customButtons: sqlops.window.modelviewdialog.Button[]; private _pageChangedEmitter = new Emitter(); public readonly onPageChanged = this._pageChangedEmitter.event; + private _navigationValidator: (info: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable; constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog) { this.doneButton = this._extHostModelViewDialog.createButton(DONE_LABEL); @@ -225,6 +226,7 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard { this.nextButton = this._extHostModelViewDialog.createButton(NEXT_LABEL); this.backButton = this._extHostModelViewDialog.createButton(PREVIOUS_LABEL); this._extHostModelViewDialog.registerWizardPageInfoChangedCallback(this, info => this.handlePageInfoChanged(info)); + this._currentPage = 0; this.onPageChanged(info => this._currentPage = info.newPage); } @@ -254,6 +256,18 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard { return this._extHostModelViewDialog.closeWizard(this); } + public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable): void { + this._navigationValidator = validator; + } + + public validateNavigation(info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable { + if (this._navigationValidator) { + return Promise.resolve(this._navigationValidator(info)); + } else { + return Promise.resolve(true); + } + } + private handlePageInfoChanged(info: WizardPageEventInfo): void { this._currentPage = info.pageChangeInfo.newPage; if (info.eventType === WizardPageInfoEventType.PageAddedOrRemoved) { @@ -335,6 +349,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } } + public $validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable { + let wizard = this._objectsByHandle.get(handle) as WizardImpl; + return wizard.validateNavigation(info); + } + 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 ae14350571..6ea0fd28a1 100644 --- a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts @@ -157,6 +157,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape wizard.onPageChanged(info => this._proxy.$onWizardPageChanged(handle, info)); wizard.onPageAdded(() => this.handleWizardPageAddedOrRemoved(handle)); wizard.onPageRemoved(() => this.handleWizardPageAddedOrRemoved(handle)); + wizard.registerNavigationValidator(info => this.validateNavigation(handle, info)); this._wizards.set(handle, wizard); } @@ -254,4 +255,8 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape let wizard = this._wizards.get(handle); this._proxy.$updateWizardPageInfo(handle, wizard.pages.map(page => this._wizardPageHandles.get(page)), wizard.currentPage); } + + private validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable { + return this._proxy.$validateNavigation(handle, info); + } } \ 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 88750b69ae..5547c2df05 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -556,6 +556,7 @@ export interface ExtHostModelViewDialogShape { $onPanelValidityChanged(handle: number, valid: boolean): void; $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; } export interface MainThreadModelViewDialogShape extends IDisposable { diff --git a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts index 85f9290dd1..b267f0c44b 100644 --- a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts @@ -3,6 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as sqlops from 'sqlops'; import * as assert from 'assert'; import { Mock, It, Times } from 'typemoq'; import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog'; @@ -263,4 +264,30 @@ suite('ExtHostModelViewDialog Tests', () => { extHostModelViewDialog.$onPanelValidityChanged(pageHandle, false); assert.equal(page.valid, false); }); + + test('Main thread can execute wizard navigation validation', () => { + // Set up the main thread mock to record the wizard handle + let wizardHandle: number; + mockProxy.setup(x => x.$setWizardDetails(It.isAny(), It.isAny())).callback((handle, details) => wizardHandle = handle); + + // Create the wizard and add a validation that records that it has been called + let wizard = extHostModelViewDialog.createWizard('wizard_1'); + extHostModelViewDialog.updateWizard(wizard); + let validationInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo; + wizard.registerNavigationValidator(info => { + validationInfo = info; + return true; + }); + + // If I call the validation from the main thread then it should run and record the correct page change info + let lastPage = 0; + let newPage = 1; + extHostModelViewDialog.$validateNavigation(wizardHandle, { + lastPage: lastPage, + newPage: newPage + }); + assert.notEqual(validationInfo, undefined); + assert.equal(validationInfo.lastPage, lastPage); + assert.equal(validationInfo.newPage, newPage); + }); }); \ 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 8c704d2549..d970613b42 100644 --- a/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts @@ -59,7 +59,8 @@ suite('MainThreadModelViewDialog Tests', () => { $onButtonClick: handle => undefined, $onPanelValidityChanged: (handle, valid) => undefined, $onWizardPageChanged: (handle, info) => undefined, - $updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined + $updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined, + $validateNavigation: (handle, info) => undefined }); let extHostContext = { getProxy: proxyType => mockExtHostModelViewDialog.object @@ -316,4 +317,15 @@ suite('MainThreadModelViewDialog Tests', () => { It.is(pageHandles => pageHandles.length === 1 && pageHandles[0] === page2Handle), It.is(currentPage => currentPage === 0)), Times.once()); }); + + test('Creating a wizard adds a navigation validation that calls the extension host', () => { + mockExtHostModelViewDialog.setup(x => x.$validateNavigation(It.isAny(), It.isAny())); + + // If I call validateNavigation on the wizard that gets created + let wizard: Wizard = (mainThreadModelViewDialog as any).getWizard(wizardHandle); + wizard.validateNavigation(1); + + // Then the call gets forwarded to the extension host + mockExtHostModelViewDialog.verify(x => x.$validateNavigation(It.is(handle => handle === wizardHandle), It.is(info => info.newPage === 1)), Times.once()); + }); }); \ No newline at end of file