Add wizard navigation validator (#1587)

This commit is contained in:
Matt Irvine
2018-06-08 15:32:37 -07:00
committed by GitHub
parent 20c4f085c8
commit e3a2ed95d4
8 changed files with 104 additions and 8 deletions

View File

@@ -139,6 +139,7 @@ export class Wizard {
public readonly onPageAdded = this._pageAddedEmitter.event; public readonly onPageAdded = this._pageAddedEmitter.event;
private _pageRemovedEmitter = new Emitter<WizardPage>(); private _pageRemovedEmitter = new Emitter<WizardPage>();
public readonly onPageRemoved = this._pageRemovedEmitter.event; public readonly onPageRemoved = this._pageRemovedEmitter.event;
private _navigationValidator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable<boolean>;
constructor(public title: string) { } constructor(public title: string) { }
@@ -191,4 +192,19 @@ export class Wizard {
this.pages.splice(index, 1); this.pages.splice(index, 1);
this._pageRemovedEmitter.fire(removedPage); this._pageRemovedEmitter.fire(removedPage);
} }
public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable<boolean>): void {
this._navigationValidator = validator;
}
public validateNavigation(newPage: number): Thenable<boolean> {
if (this._navigationValidator) {
return Promise.resolve(this._navigationValidator({
lastPage: this._currentPage,
newPage: newPage
}));
} else {
return Promise.resolve(true);
}
}
} }

View File

@@ -106,12 +106,12 @@ export class WizardModal extends Modal {
}); });
this._wizard.onPageAdded(page => { this._wizard.onPageAdded(page => {
this.registerPage(page); this.registerPage(page);
this.showPage(this.getCurrentPage()); this.showPage(this.getCurrentPage(), false);
}); });
this._wizard.onPageRemoved(page => { this._wizard.onPageRemoved(page => {
let dialogPane = this._dialogPanes.get(page); let dialogPane = this._dialogPanes.get(page);
this._dialogPanes.delete(page); this._dialogPanes.delete(page);
this.showPage(this.getCurrentPage()); this.showPage(this.getCurrentPage(), false);
dialogPane.dispose(); dialogPane.dispose();
}); });
} }
@@ -123,10 +123,13 @@ export class WizardModal extends Modal {
page.onUpdate(() => this.setButtonsForPage(this._wizard.currentPage)); page.onUpdate(() => this.setButtonsForPage(this._wizard.currentPage));
} }
private showPage(index: number): void { private async showPage(index: number, validate: boolean = true): Promise<void> {
let pageToShow = this._wizard.pages[index]; let pageToShow = this._wizard.pages[index];
if (!pageToShow) { if (!pageToShow) {
this.done(); this.done(validate);
return;
}
if (validate && !await this._wizard.validateNavigation(index)) {
return; return;
} }
this._dialogPanes.forEach((dialogPane, page) => { this._dialogPanes.forEach((dialogPane, page) => {
@@ -163,12 +166,15 @@ export class WizardModal extends Modal {
} }
public open(): void { public open(): void {
this.showPage(0); this.showPage(0, false);
this.show(); this.show();
} }
public done(): void { public async done(validate: boolean = true): Promise<void> {
if (this._wizard.doneButton.enabled) { if (this._wizard.doneButton.enabled) {
if (validate && !await this._wizard.validateNavigation(undefined)) {
return;
}
this._onDone.fire(); this._onDone.fire();
this.dispose(); this.dispose();
this.hide(); this.hide();

View File

@@ -621,7 +621,7 @@ declare module 'sqlops' {
lastPage: number, lastPage: number,
/** /**
* The new page number * The new page number or undefined if the user is closing the wizard
*/ */
newPage: number newPage: number
} }
@@ -734,6 +734,16 @@ declare module 'sqlops' {
* Close the wizard. Does nothing if the wizard is not open. * Close the wizard. Does nothing if the wizard is not open.
*/ */
close(): Thenable<void>; close(): Thenable<void>;
/**
* 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<boolean>): void;
} }
} }
} }

View File

@@ -217,6 +217,7 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard {
public customButtons: sqlops.window.modelviewdialog.Button[]; public customButtons: sqlops.window.modelviewdialog.Button[];
private _pageChangedEmitter = new Emitter<sqlops.window.modelviewdialog.WizardPageChangeInfo>(); private _pageChangedEmitter = new Emitter<sqlops.window.modelviewdialog.WizardPageChangeInfo>();
public readonly onPageChanged = this._pageChangedEmitter.event; public readonly onPageChanged = this._pageChangedEmitter.event;
private _navigationValidator: (info: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable<boolean>;
constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog) { constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog) {
this.doneButton = this._extHostModelViewDialog.createButton(DONE_LABEL); 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.nextButton = this._extHostModelViewDialog.createButton(NEXT_LABEL);
this.backButton = this._extHostModelViewDialog.createButton(PREVIOUS_LABEL); this.backButton = this._extHostModelViewDialog.createButton(PREVIOUS_LABEL);
this._extHostModelViewDialog.registerWizardPageInfoChangedCallback(this, info => this.handlePageInfoChanged(info)); this._extHostModelViewDialog.registerWizardPageInfoChangedCallback(this, info => this.handlePageInfoChanged(info));
this._currentPage = 0;
this.onPageChanged(info => this._currentPage = info.newPage); this.onPageChanged(info => this._currentPage = info.newPage);
} }
@@ -254,6 +256,18 @@ class WizardImpl implements sqlops.window.modelviewdialog.Wizard {
return this._extHostModelViewDialog.closeWizard(this); return this._extHostModelViewDialog.closeWizard(this);
} }
public registerNavigationValidator(validator: (pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo) => boolean | Thenable<boolean>): void {
this._navigationValidator = validator;
}
public validateNavigation(info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable<boolean> {
if (this._navigationValidator) {
return Promise.resolve(this._navigationValidator(info));
} else {
return Promise.resolve(true);
}
}
private handlePageInfoChanged(info: WizardPageEventInfo): void { private handlePageInfoChanged(info: WizardPageEventInfo): void {
this._currentPage = info.pageChangeInfo.newPage; this._currentPage = info.pageChangeInfo.newPage;
if (info.eventType === WizardPageInfoEventType.PageAddedOrRemoved) { if (info.eventType === WizardPageInfoEventType.PageAddedOrRemoved) {
@@ -335,6 +349,11 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape {
} }
} }
public $validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable<boolean> {
let wizard = this._objectsByHandle.get(handle) as WizardImpl;
return wizard.validateNavigation(info);
}
public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void {
let handle = this.getHandle(dialog); let handle = this.getHandle(dialog);
this.updateDialogContent(dialog); this.updateDialogContent(dialog);

View File

@@ -157,6 +157,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
wizard.onPageChanged(info => this._proxy.$onWizardPageChanged(handle, info)); wizard.onPageChanged(info => this._proxy.$onWizardPageChanged(handle, info));
wizard.onPageAdded(() => this.handleWizardPageAddedOrRemoved(handle)); wizard.onPageAdded(() => this.handleWizardPageAddedOrRemoved(handle));
wizard.onPageRemoved(() => this.handleWizardPageAddedOrRemoved(handle)); wizard.onPageRemoved(() => this.handleWizardPageAddedOrRemoved(handle));
wizard.registerNavigationValidator(info => this.validateNavigation(handle, info));
this._wizards.set(handle, wizard); this._wizards.set(handle, wizard);
} }
@@ -254,4 +255,8 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape
let wizard = this._wizards.get(handle); let wizard = this._wizards.get(handle);
this._proxy.$updateWizardPageInfo(handle, wizard.pages.map(page => this._wizardPageHandles.get(page)), wizard.currentPage); this._proxy.$updateWizardPageInfo(handle, wizard.pages.map(page => this._wizardPageHandles.get(page)), wizard.currentPage);
} }
private validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable<boolean> {
return this._proxy.$validateNavigation(handle, info);
}
} }

View File

@@ -556,6 +556,7 @@ export interface ExtHostModelViewDialogShape {
$onPanelValidityChanged(handle: number, valid: boolean): void; $onPanelValidityChanged(handle: number, valid: boolean): void;
$onWizardPageChanged(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): void; $onWizardPageChanged(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): void;
$updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void; $updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void;
$validateNavigation(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): Thenable<boolean>;
} }
export interface MainThreadModelViewDialogShape extends IDisposable { export interface MainThreadModelViewDialogShape extends IDisposable {

View File

@@ -3,6 +3,7 @@
* Licensed under the Source EULA. See License.txt in the project root for license information. * 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 * as assert from 'assert';
import { Mock, It, Times } from 'typemoq'; import { Mock, It, Times } from 'typemoq';
import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog'; import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog';
@@ -263,4 +264,30 @@ suite('ExtHostModelViewDialog Tests', () => {
extHostModelViewDialog.$onPanelValidityChanged(pageHandle, false); extHostModelViewDialog.$onPanelValidityChanged(pageHandle, false);
assert.equal(page.valid, 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);
});
}); });

View File

@@ -59,7 +59,8 @@ suite('MainThreadModelViewDialog Tests', () => {
$onButtonClick: handle => undefined, $onButtonClick: handle => undefined,
$onPanelValidityChanged: (handle, valid) => undefined, $onPanelValidityChanged: (handle, valid) => undefined,
$onWizardPageChanged: (handle, info) => undefined, $onWizardPageChanged: (handle, info) => undefined,
$updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined $updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined,
$validateNavigation: (handle, info) => undefined
}); });
let extHostContext = <IExtHostContext>{ let extHostContext = <IExtHostContext>{
getProxy: proxyType => mockExtHostModelViewDialog.object getProxy: proxyType => mockExtHostModelViewDialog.object
@@ -316,4 +317,15 @@ suite('MainThreadModelViewDialog Tests', () => {
It.is(pageHandles => pageHandles.length === 1 && pageHandles[0] === page2Handle), It.is(pageHandles => pageHandles.length === 1 && pageHandles[0] === page2Handle),
It.is(currentPage => currentPage === 0)), Times.once()); 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());
});
}); });