From 8e234d9b2d4ea63fa1f155267aa764fce2e6c99d Mon Sep 17 00:00:00 2001 From: Matt Irvine Date: Mon, 21 May 2018 15:19:21 -0700 Subject: [PATCH] Enable basic wizard API (#1450) --- .../modelEditor/modelViewEditor.ts | 3 +- .../platform/dialog/customDialogService.ts | 21 +- src/sql/platform/dialog/dialogModal.ts | 3 +- src/sql/platform/dialog/dialogPane.ts | 28 +- src/sql/platform/dialog/dialogTypes.ts | 125 ++++++- src/sql/platform/dialog/media/dialogModal.css | 4 + src/sql/platform/dialog/wizardModal.ts | 209 +++++++++++ src/sql/sqlops.proposed.d.ts | 154 +++++++- .../workbench/api/common/sqlExtHostTypes.ts | 19 + .../api/node/extHostModelViewDialog.ts | 329 ++++++++++++++---- .../api/node/mainThreadModelViewDialog.ts | 118 ++++++- .../workbench/api/node/sqlExtHost.api.impl.ts | 10 +- .../workbench/api/node/sqlExtHost.protocol.ts | 33 +- .../platform/dialog/dialogPane.test.ts | 10 +- .../api/extHostModelViewDialog.test.ts | 166 ++++++++- .../api/mainThreadModelViewDialog.test.ts | 172 ++++++++- 16 files changed, 1247 insertions(+), 157 deletions(-) create mode 100644 src/sql/platform/dialog/wizardModal.ts diff --git a/src/sql/parts/modelComponents/modelEditor/modelViewEditor.ts b/src/sql/parts/modelComponents/modelEditor/modelViewEditor.ts index 95baf87973..7802484451 100644 --- a/src/sql/parts/modelComponents/modelEditor/modelViewEditor.ts +++ b/src/sql/parts/modelComponents/modelEditor/modelViewEditor.ts @@ -50,8 +50,7 @@ export class ModelViewEditor extends BaseEditor { if (!this._modelViewMap.get(input.modelViewId)) { let modelViewContainer = DOM.$('div.model-view-container'); - let dialog = new Dialog(input.title, input.modelViewId); - let dialogPane = new DialogPane(dialog, this._bootstrapService); + let dialogPane = new DialogPane(input.title, input.modelViewId, () => undefined, this._bootstrapService); dialogPane.createBody(modelViewContainer); this._modelViewMap.set(input.modelViewId, modelViewContainer); } diff --git a/src/sql/platform/dialog/customDialogService.ts b/src/sql/platform/dialog/customDialogService.ts index 5146e1df80..ccf11c19a9 100644 --- a/src/sql/platform/dialog/customDialogService.ts +++ b/src/sql/platform/dialog/customDialogService.ts @@ -8,14 +8,17 @@ import * as sqlops from 'sqlops'; import { OptionsDialog } from 'sql/base/browser/ui/modal/optionsDialog'; import { DialogModal } from 'sql/platform/dialog/dialogModal'; -import { Dialog } from 'sql/platform/dialog/dialogTypes'; +import { WizardModal } from 'sql/platform/dialog/wizardModal'; +import { Dialog, Wizard, DialogTab } from 'sql/platform/dialog/dialogTypes'; import { IModalOptions } from 'sql/base/browser/ui/modal/modal'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -const defaultOptions: IModalOptions = { hasBackButton: true, isWide: false }; +const defaultOptions: IModalOptions = { hasBackButton: false, isWide: false }; +const defaultWizardOptions: IModalOptions = { hasBackButton: false, isWide: true }; export class CustomDialogService { private _dialogModals = new Map(); + private _wizardModals = new Map(); constructor( @IInstantiationService private _instantiationService: IInstantiationService) { } @@ -26,10 +29,24 @@ export class CustomDialogService { dialogModal.open(); } + public showWizard(wizard: Wizard, options?: IModalOptions): void { + let wizardModal = this._instantiationService.createInstance(WizardModal, wizard, 'WizardPage', options || defaultWizardOptions); + this._wizardModals.set(wizard, wizardModal); + wizardModal.render(); + wizardModal.open(); + } + public closeDialog(dialog: Dialog): void { let dialogModal = this._dialogModals.get(dialog); if (dialogModal) { dialogModal.cancel(); } } + + public closeWizard(wizard: Wizard): void { + let wizardModal = this._wizardModals.get(wizard); + if (wizardModal) { + wizardModal.cancel(); + } + } } diff --git a/src/sql/platform/dialog/dialogModal.ts b/src/sql/platform/dialog/dialogModal.ts index 919979353a..953a104e14 100644 --- a/src/sql/platform/dialog/dialogModal.ts +++ b/src/sql/platform/dialog/dialogModal.ts @@ -98,7 +98,8 @@ export class DialogModal extends Modal { body = bodyBuilder.getHTMLElement(); }); - this._dialogPane = new DialogPane(this._dialog, this._bootstrapService); + this._dialogPane = new DialogPane(this._dialog.title, this._dialog.content, + valid => this._dialog.notifyValidityChanged(valid), this._bootstrapService); this._dialogPane.createBody(body); } diff --git a/src/sql/platform/dialog/dialogPane.ts b/src/sql/platform/dialog/dialogPane.ts index da5676ede2..b35c98607f 100644 --- a/src/sql/platform/dialog/dialogPane.ts +++ b/src/sql/platform/dialog/dialogPane.ts @@ -8,7 +8,7 @@ import 'vs/css!./media/dialogModal'; import { NgModuleRef } from '@angular/core'; import { IModalDialogStyles } from 'sql/base/browser/ui/modal/modal'; -import { Dialog } from 'sql/platform/dialog/dialogTypes'; +import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes'; import { TabbedPanel, IPanelTab, IPanelView } from 'sql/base/browser/ui/panel/panel'; import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; import { DialogModule } from 'sql/platform/dialog/dialog.module'; @@ -32,7 +32,9 @@ export class DialogPane extends Disposable implements IThemable { private _tabContent: HTMLElement[]; constructor( - private _dialog: Dialog, + private _title: string, + private _content: string | DialogTab[], + private _validityChangedCallback: (valid: boolean) => void, private _bootstrapService: IBootstrapService ) { super(); @@ -43,19 +45,19 @@ export class DialogPane extends Disposable implements IThemable { public createBody(container: HTMLElement): HTMLElement { new Builder(container).div({ class: 'dialogModal-pane' }, (bodyBuilder) => { this._body = bodyBuilder.getHTMLElement(); - if (typeof this._dialog.content === 'string' || this._dialog.content.length < 2) { - let modelViewId = typeof this._dialog.content === 'string' ? this._dialog.content : this._dialog.content[0].content; + if (typeof this._content === 'string' || this._content.length < 2) { + let modelViewId = typeof this._content === 'string' ? this._content : this._content[0].content; this.initializeModelViewContainer(this._body, modelViewId); } else { this._tabbedPanel = new TabbedPanel(this._body); - this._dialog.content.forEach((tab, tabIndex) => { + this._content.forEach((tab, tabIndex) => { let tabContainer = document.createElement('div'); tabContainer.style.display = 'none'; this._body.appendChild(tabContainer); - this.initializeModelViewContainer(tabContainer, tab.content); + this.initializeModelViewContainer(tabContainer, tab.content, tab); this._tabbedPanel.pushTab({ title: tab.title, - identifier: 'dialogPane.' + this._dialog.title + '.' + tabIndex, + identifier: 'dialogPane.' + this._title + '.' + tabIndex, view: { render: (container) => { if (tabContainer.parentElement === this._body) { @@ -77,14 +79,19 @@ export class DialogPane extends Disposable implements IThemable { /** * Bootstrap angular for the dialog's model view controller with the given model view ID */ - private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string) { + private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string, tab?: DialogTab) { this._bootstrapService.bootstrap( DialogModule, bodyContainer, 'dialog-modelview-container', { modelViewId: modelViewId, - validityChangedCallback: (valid: boolean) => this._setValidity(modelViewId, valid) + validityChangedCallback: (valid: boolean) => { + this._setValidity(modelViewId, valid); + if (tab) { + tab.notifyValidityChanged(valid); + } + } } as DialogComponentParams, undefined, (moduleRef) => this._moduleRefs.push(moduleRef)); @@ -111,7 +118,7 @@ export class DialogPane extends Disposable implements IThemable { this._modelViewValidityMap.set(modelViewId, valid); let newValidity = this.isValid(); if (newValidity !== oldValidity) { - this._dialog.notifyValidityChanged(newValidity); + this._validityChangedCallback(newValidity); } } @@ -123,6 +130,7 @@ export class DialogPane extends Disposable implements IThemable { public dispose() { super.dispose(); + this._body.remove(); this._moduleRefs.forEach(moduleRef => moduleRef.destroy()); } } diff --git a/src/sql/platform/dialog/dialogTypes.ts b/src/sql/platform/dialog/dialogTypes.ts index 2c44371ea8..33f59ea0d8 100644 --- a/src/sql/platform/dialog/dialogTypes.ts +++ b/src/sql/platform/dialog/dialogTypes.ts @@ -9,17 +9,35 @@ import * as sqlops from 'sqlops'; import { localize } from 'vs/nls'; import Event, { Emitter } from 'vs/base/common/event'; -export class DialogTab { +export class ModelViewPane { + private _valid: boolean = true; + private _validityChangedEmitter = new Emitter(); + public readonly onValidityChanged = this._validityChangedEmitter.event; + + public get valid(): boolean { + return this._valid; + } + + public notifyValidityChanged(valid: boolean) { + if (this._valid !== valid) { + this._valid = valid; + this._validityChangedEmitter.fire(this._valid); + } + } +} + +export class DialogTab extends ModelViewPane { public content: string; constructor(public title: string, content?: string) { + super(); if (content) { this.content = content; } } } -export class Dialog { +export class Dialog extends ModelViewPane { private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done'); private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel'); @@ -28,24 +46,12 @@ export class Dialog { public cancelButton: DialogButton = new DialogButton(Dialog.CANCEL_BUTTON_LABEL, true); public customButtons: DialogButton[]; - private _valid: boolean = true; - private _validityChangedEmitter = new Emitter(); - public readonly onValidityChanged = this._validityChangedEmitter.event; - constructor(public title: string, content?: string | DialogTab[]) { + super(); if (content) { this.content = content; } } - - public get valid(): boolean { - return this._valid; - } - - public notifyValidityChanged(valid: boolean) { - this._valid = valid; - this._validityChangedEmitter.fire(valid); - } } export class DialogButton implements sqlops.window.modelviewdialog.Button { @@ -96,4 +102,93 @@ export class DialogButton implements sqlops.window.modelviewdialog.Button { public registerClickEvent(clickEvent: Event): void { clickEvent(() => this._onClick.fire()); } +} + +export class WizardPage extends DialogTab { + public customButtons: DialogButton[]; + private _enabled: boolean; + private _onUpdate: Emitter = new Emitter(); + public readonly onUpdate: Event = this._onUpdate.event; + + constructor(public title: string, content?: string) { + super(title, content); + } + + public get enabled(): boolean { + return this._enabled; + } + + public set enabled(enabled: boolean) { + this._enabled = enabled; + this._onUpdate.fire(); + } +} + +export class Wizard { + public pages: WizardPage[]; + public nextButton: DialogButton; + public backButton: DialogButton; + public generateScriptButton: DialogButton; + public doneButton: DialogButton; + public cancelButton: DialogButton; + public customButtons: DialogButton[]; + private _currentPage: number; + private _pageChangedEmitter = new Emitter(); + public readonly onPageChanged = this._pageChangedEmitter.event; + private _pageAddedEmitter = new Emitter(); + public readonly onPageAdded = this._pageAddedEmitter.event; + private _pageRemovedEmitter = new Emitter(); + public readonly onPageRemoved = this._pageRemovedEmitter.event; + + constructor(public title: string) { } + + public get currentPage(): number { + return this._currentPage; + } + + public setCurrentPage(index: number): void { + if (index === undefined || index < 0 || index >= this.pages.length) { + throw new Error('Index is out of bounds'); + } + let lastPage = this._currentPage; + this._currentPage = index; + if (lastPage !== undefined && this._currentPage !== undefined && lastPage !== this._currentPage) { + this._pageChangedEmitter.fire({ + lastPage: lastPage, + newPage: this._currentPage + }); + } + } + + public addPage(page: WizardPage, index?: number): void { + if (index !== undefined && (index < 0 || index > this.pages.length)) { + throw new Error('Index is out of bounds'); + } + if (index !== undefined && this.currentPage !== undefined && index <= this.currentPage) { + ++this._currentPage; + } + if (index === undefined) { + this.pages.push(page); + } else { + this.pages = this.pages.slice(0, index).concat([page], this.pages.slice(index)); + } + this._pageAddedEmitter.fire(page); + } + + public removePage(index: number): void { + if (index === undefined || index < 0 || index >= this.pages.length) { + throw new Error('Index is out of bounds'); + } + if (index === this.currentPage) { + // Switch to the new page before deleting the current page + let newPage = this._currentPage > 0 ? this._currentPage - 1 : this._currentPage + 1; + this.setCurrentPage(newPage); + } + if (this.currentPage !== undefined && index < this.currentPage) { + --this._currentPage; + } + let removedPage = this.pages[index]; + this.pages.splice(index, 1); + this._pageRemovedEmitter.fire(removedPage); + } } \ No newline at end of file diff --git a/src/sql/platform/dialog/media/dialogModal.css b/src/sql/platform/dialog/media/dialogModal.css index e2572ae07d..155d4802fc 100644 --- a/src/sql/platform/dialog/media/dialogModal.css +++ b/src/sql/platform/dialog/media/dialogModal.css @@ -26,3 +26,7 @@ .dialogModal-hidden { display: none; } + +.footer-button.dialogModal-hidden { + margin: 0; +} diff --git a/src/sql/platform/dialog/wizardModal.ts b/src/sql/platform/dialog/wizardModal.ts new file mode 100644 index 0000000000..e4ea2056b8 --- /dev/null +++ b/src/sql/platform/dialog/wizardModal.ts @@ -0,0 +1,209 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./media/dialogModal'; +import { Modal, IModalOptions } from 'sql/base/browser/ui/modal/modal'; +import { attachModalDialogStyler } from 'sql/common/theme/styler'; +import { Wizard, Dialog, DialogButton, WizardPage } from 'sql/platform/dialog/dialogTypes'; +import { DialogPane } from 'sql/platform/dialog/dialogPane'; +import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; +import { Button } from 'vs/base/browser/ui/button/button'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { Builder } from 'vs/base/browser/builder'; +import { IPartService } from 'vs/workbench/services/part/common/partService'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; +import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { attachButtonStyler } from 'vs/platform/theme/common/styler'; +import { localize } from 'vs/nls'; +import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; +import { Emitter } from 'vs/base/common/event'; + +export class WizardModal extends Modal { + private _dialogPanes = new Map(); + private _onDone = new Emitter(); + private _onCancel = new Emitter(); + + // Wizard HTML elements + private _body: HTMLElement; + + // Buttons + private _previousButton: Button; + private _nextButton: Button; + private _generateScriptButton: Button; + private _doneButton: Button; + private _cancelButton: Button; + + constructor( + private _wizard: Wizard, + name: string, + options: IModalOptions, + @IPartService partService: IPartService, + @IWorkbenchThemeService private _themeService: IWorkbenchThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IBootstrapService private _bootstrapService: IBootstrapService + ) { + super(_wizard.title, name, partService, telemetryService, contextKeyService, options); + } + + public layout(): void { + + } + + public render() { + super.render(); + attachModalDialogStyler(this, this._themeService); + + if (this.backButton) { + this.backButton.onDidClick(() => this.cancel()); + attachButtonStyler(this.backButton, this._themeService, { buttonBackground: SIDE_BAR_BACKGROUND, buttonHoverBackground: SIDE_BAR_BACKGROUND }); + } + + this._previousButton = this.addDialogButton(this._wizard.backButton, () => this.showPage(this.getCurrentPage() - 1)); + this._nextButton = this.addDialogButton(this._wizard.nextButton, () => this.showPage(this.getCurrentPage() + 1)); + this._generateScriptButton = this.addDialogButton(this._wizard.generateScriptButton, () => undefined); + this._doneButton = this.addDialogButton(this._wizard.doneButton, () => this.done(), false); + this._wizard.doneButton.registerClickEvent(this._onDone.event); + this._cancelButton = this.addDialogButton(this._wizard.cancelButton, () => this.cancel(), false); + this._wizard.cancelButton.registerClickEvent(this._onCancel.event); + } + + private addDialogButton(button: DialogButton, onSelect: () => void = () => undefined, registerClickEvent: boolean = true): Button { + let buttonElement = this.addFooterButton(button.label, onSelect); + buttonElement.enabled = button.enabled; + if (registerClickEvent) { + button.registerClickEvent(buttonElement.onDidClick); + } + button.onUpdate(() => { + this.updateButtonElement(buttonElement, button); + }); + attachButtonStyler(buttonElement, this._themeService); + this.updateButtonElement(buttonElement, button); + return buttonElement; + } + + private updateButtonElement(buttonElement: Button, dialogButton: DialogButton) { + buttonElement.label = dialogButton.label; + buttonElement.enabled = dialogButton.enabled; + dialogButton.hidden ? buttonElement.element.classList.add('dialogModal-hidden') : buttonElement.element.classList.remove('dialogModal-hidden'); + } + + protected renderBody(container: HTMLElement): void { + new Builder(container).div({ class: 'dialogModal-body' }, (bodyBuilder) => { + this._body = bodyBuilder.getHTMLElement(); + }); + + let builder = new Builder(this._body); + this._wizard.pages.forEach(page => { + this.registerPage(page); + }); + this._wizard.onPageAdded(page => { + this.registerPage(page); + this.showPage(this.getCurrentPage()); + }); + this._wizard.onPageRemoved(page => { + let dialogPane = this._dialogPanes.get(page); + this._dialogPanes.delete(page); + this.showPage(this.getCurrentPage()); + dialogPane.dispose(); + }); + } + + private registerPage(page: WizardPage): void { + let dialogPane = new DialogPane(page.title, page.content, valid => page.notifyValidityChanged(valid), this._bootstrapService); + dialogPane.createBody(this._body); + this._dialogPanes.set(page, dialogPane); + page.onUpdate(() => this.setButtonsForPage(this._wizard.currentPage)); + } + + private showPage(index: number): void { + let pageToShow = this._wizard.pages[index]; + if (!pageToShow) { + this.done(); + return; + } + this._dialogPanes.forEach((dialogPane, page) => { + if (page === pageToShow) { + dialogPane.show(); + } else { + dialogPane.hide(); + } + }); + this.setButtonsForPage(index); + this._wizard.setCurrentPage(index); + } + + private setButtonsForPage(index: number) { + if (this._wizard.pages[index - 1]) { + this._previousButton.element.parentElement.classList.remove('dialogModal-hidden'); + this._previousButton.enabled = this._wizard.pages[index - 1].enabled; + } else { + this._previousButton.element.parentElement.classList.add('dialogModal-hidden'); + } + + if (this._wizard.pages[index + 1]) { + this._nextButton.element.parentElement.classList.remove('dialogModal-hidden'); + this._nextButton.enabled = this._wizard.pages[index + 1].enabled; + this._doneButton.element.parentElement.classList.add('dialogModal-hidden'); + } else { + this._nextButton.element.parentElement.classList.add('dialogModal-hidden'); + this._doneButton.element.parentElement.classList.remove('dialogModal-hidden'); + } + } + + private getCurrentPage(): number { + return this._wizard.currentPage; + } + + public open(): void { + this.showPage(0); + this.show(); + } + + public done(): void { + if (this._wizard.doneButton.enabled) { + this._onDone.fire(); + this.dispose(); + this.hide(); + } + } + + public cancel(): void { + this._onCancel.fire(); + this.dispose(); + this.hide(); + } + + protected hide(): void { + super.hide(); + } + + protected show(): void { + super.show(); + } + + /** + * Overridable to change behavior of escape key + */ + protected onClose(e: StandardKeyboardEvent) { + this.cancel(); + } + + /** + * Overridable to change behavior of enter key + */ + protected onAccept(e: StandardKeyboardEvent) { + this.done(); + } + + public dispose(): void { + super.dispose(); + this._dialogPanes.forEach(dialogPane => dialogPane.dispose()); + } +} \ No newline at end of file diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 3e689c15f8..63223e1a41 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -396,6 +396,18 @@ declare module 'sqlops' { */ export function closeDialog(dialog: Dialog): void; + /** + * Create a wizard page with the given title, for inclusion in a wizard + * @param title The title of the page + */ + export function createWizardPage(title: string): WizardPage; + + /** + * Create a wizard with the given title and pages + * @param title The title of the wizard + */ + export function createWizard(title: string): Wizard; + export interface ModelViewPanel { /** * Register model view content for the dialog. @@ -407,6 +419,16 @@ declare module 'sqlops' { * Returns the model view content if registered. Returns undefined if model review is not registered */ readonly modelView: ModelView; + + /** + * Whether the panel's content is valid + */ + readonly valid: boolean; + + /** + * Fired whenever the panel's valid property changes + */ + readonly onValidityChanged: vscode.Event; } // Model view dialog classes @@ -436,16 +458,6 @@ declare module 'sqlops' { * Any additional buttons that should be displayed */ customButtons: Button[]; - - /** - * Whether the dialog's content is valid - */ - readonly valid: boolean; - - /** - * Fired whenever the dialog's valid property changes - */ - readonly onValidityChanged: vscode.Event; } export interface DialogTab extends ModelViewPanel { @@ -481,6 +493,128 @@ declare module 'sqlops' { */ readonly onClick: vscode.Event; } + + export interface WizardPageChangeInfo { + /** + * The page number that the wizard changed from + */ + lastPage: number, + + /** + * The new page number + */ + newPage: number + } + + export interface WizardPage extends ModelViewPanel { + /** + * The title of the page + */ + title: string; + + /** + * A string giving the ID of the page's model view content + */ + content: string; + + /** + * Any additional buttons that should be displayed while the page is open + */ + customButtons: Button[]; + + /** + * Whether the page is enabled. If the page is not enabled, the user will not be + * able to advance to it. Defaults to true. + */ + enabled: boolean; + } + + export interface Wizard { + /** + * The title of the wizard + */ + title: string, + + /** + * The wizard's pages. Pages can be added/removed while the dialog is open by using + * the addPage and removePage methods + */ + pages: WizardPage[]; + + /** + * The index in the pages array of the active page, or undefined if the wizard is + * not currently visible + */ + readonly currentPage: number; + + /** + * The done button + */ + doneButton: Button; + + /** + * The cancel button + */ + cancelButton: Button; + + /** + * The generate script button + */ + generateScriptButton: Button; + + /** + * The next button + */ + nextButton: Button; + + /** + * The back button + */ + backButton: Button; + + /** + * Any additional buttons that should be displayed for all pages of the dialog. If + * buttons are needed for specific pages they can be added using the customButtons + * property on each page. + */ + customButtons: Button[]; + + /** + * Event fired when the wizard's page changes, containing information about the + * previous page and the new page + */ + onPageChanged: vscode.Event; + + /** + * Add a page to the wizard at the given index + * @param page The page to add + * @param index The index in the pages array to add the page at, or undefined to + * add it at the end + */ + addPage(page: WizardPage, index?: number): Thenable; + + /** + * Remove the page at the given index from the wizard + * @param index The index in the pages array to remove + */ + removePage(index: number): Thenable; + + /** + * Go to the page at the given index in the pages array. + * @param index The index of the page to go to + */ + setCurrentPage(index: number): Thenable; + + /** + * Open the wizard. Does nothing if the wizard is already open. + */ + open(): Thenable; + + /** + * Close the wizard. Does nothing if the wizard is not open. + */ + close(): Thenable; + } } } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 918a2eda2b..03836e0b49 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -121,6 +121,25 @@ export interface IModelViewButtonDetails { hidden: boolean; } +export interface IModelViewWizardPageDetails { + title: string; + content: string; + enabled: boolean; + customButtons: number[]; +} + +export interface IModelViewWizardDetails { + title: string; + pages: number[]; + currentPage: number; + doneButton: number; + cancelButton: number; + generateScriptButton: number; + nextButton: number; + backButton: number; + customButtons: number[]; +} + /// Card-related APIs that need to be here to avoid early load issues // with enums causing requiring of sqlops API to fail. export enum StatusIndicator { diff --git a/src/sql/workbench/api/node/extHostModelViewDialog.ts b/src/sql/workbench/api/node/extHostModelViewDialog.ts index 6c0720e378..199aad5fdf 100644 --- a/src/sql/workbench/api/node/extHostModelViewDialog.ts +++ b/src/sql/workbench/api/node/extHostModelViewDialog.ts @@ -15,13 +15,24 @@ import * as sqlops from 'sqlops'; import { SqlMainContext, ExtHostModelViewDialogShape, MainThreadModelViewDialogShape, ExtHostModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; +const DONE_LABEL = nls.localize('dialogDoneLabel', 'Done'); +const CANCEL_LABEL = nls.localize('dialogCancelLabel', 'Cancel'); +const GENERATE_SCRIPT_LABEL = nls.localize('generateScriptLabel', 'Generate script'); +const NEXT_LABEL = nls.localize('dialogNextLabel', 'Next'); +const PREVIOUS_LABEL = nls.localize('dialogPreviousLabel', 'Previous'); + class ModelViewPanelImpl implements sqlops.window.modelviewdialog.ModelViewPanel { private _modelView: sqlops.ModelView; private _handle: number; protected _modelViewId: string; + protected _valid: boolean = true; + protected _onValidityChanged: vscode.Event; constructor(private _viewType: string, + protected _extHostModelViewDialog: ExtHostModelViewDialog, protected _extHostModelView: ExtHostModelViewShape) { + this._onValidityChanged = this._extHostModelViewDialog.getValidityChangedEvent(this); + this._onValidityChanged(valid => this._valid = valid); } public registerContent(handler: (view: sqlops.ModelView) => void): void { @@ -50,15 +61,24 @@ class ModelViewPanelImpl implements sqlops.window.modelviewdialog.ModelViewPanel public set modelView(value: sqlops.ModelView) { this._modelView = value; } + + public get valid(): boolean { + return this._valid; + } + + public get onValidityChanged(): Event { + return this._onValidityChanged; + } } class ModelViewEditorImpl extends ModelViewPanelImpl implements sqlops.workspace.ModelViewEditor { constructor( + extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape, private _proxy: MainThreadModelViewDialogShape, private _title: string ) { - super('modelViewEditor', extHostModelView); + super('modelViewEditor', extHostModelViewDialog, extHostModelView); } public openEditor(position?: vscode.ViewColumn): Thenable { @@ -72,21 +92,12 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi public okButton: sqlops.window.modelviewdialog.Button; public cancelButton: sqlops.window.modelviewdialog.Button; public customButtons: sqlops.window.modelviewdialog.Button[]; - public readonly onValidityChanged: vscode.Event; - private _valid: boolean = true; - - constructor(private _extHostModelViewDialog: ExtHostModelViewDialog, + constructor(extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape) { - super('modelViewDialog', extHostModelView); - this.okButton = this._extHostModelViewDialog.createButton(nls.localize('dialogOkLabel', 'Done')); - this.cancelButton = this._extHostModelViewDialog.createButton(nls.localize('dialogCancelLabel', 'Cancel')); - this.onValidityChanged = this._extHostModelViewDialog.getValidityChangedEvent(this); - this.onValidityChanged(valid => this._valid = valid); - } - - public get valid(): boolean { - return this._valid; + super('modelViewDialog', extHostModelViewDialog, extHostModelView); + this.okButton = this._extHostModelViewDialog.createButton(DONE_LABEL); + this.cancelButton = this._extHostModelViewDialog.createButton(CANCEL_LABEL); } public setModelViewId(value: string) { @@ -97,9 +108,9 @@ class DialogImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdi class TabImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdialog.DialogTab { constructor( - private _extHostModelViewDialog: ExtHostModelViewDialog, + extHostModelViewDialog: ExtHostModelViewDialog, extHostModelView: ExtHostModelViewShape) { - super('modelViewDialogTab', extHostModelView); + super('modelViewDialogTab', extHostModelViewDialog, extHostModelView); } public title: string; @@ -157,17 +168,110 @@ class ButtonImpl implements sqlops.window.modelviewdialog.Button { } } +class WizardPageImpl extends ModelViewPanelImpl implements sqlops.window.modelviewdialog.WizardPage { + public customButtons: sqlops.window.modelviewdialog.Button[]; + private _enabled: boolean = true; + + constructor(public title: string, _extHostModelViewDialog: ExtHostModelViewDialog, _extHostModelView: ExtHostModelViewShape) { + super('modelViewWizardPage', _extHostModelViewDialog, _extHostModelView); + } + + public get enabled(): boolean { + return this._enabled; + } + + public set enabled(enabled: boolean) { + this._enabled = enabled; + this._extHostModelViewDialog.updateWizardPage(this); + } + + public get content(): string { + return this._modelViewId; + } + + public set content(content: string) { + this._modelViewId = content; + } +} + +export enum WizardPageInfoEventType { + PageChanged, + PageAddedOrRemoved +} + +export interface WizardPageEventInfo { + eventType: WizardPageInfoEventType; + pageChangeInfo: sqlops.window.modelviewdialog.WizardPageChangeInfo; + pages?: sqlops.window.modelviewdialog.WizardPage[]; +} + +class WizardImpl implements sqlops.window.modelviewdialog.Wizard { + private _currentPage: number = undefined; + public pages: sqlops.window.modelviewdialog.WizardPage[] = []; + public doneButton: sqlops.window.modelviewdialog.Button; + public cancelButton: sqlops.window.modelviewdialog.Button; + public generateScriptButton: sqlops.window.modelviewdialog.Button; + public nextButton: sqlops.window.modelviewdialog.Button; + public backButton: sqlops.window.modelviewdialog.Button; + public customButtons: sqlops.window.modelviewdialog.Button[]; + private _pageChangedEmitter = new Emitter(); + public readonly onPageChanged = this._pageChangedEmitter.event; + + constructor(public title: string, private _extHostModelViewDialog: ExtHostModelViewDialog) { + this.doneButton = this._extHostModelViewDialog.createButton(DONE_LABEL); + this.cancelButton = this._extHostModelViewDialog.createButton(CANCEL_LABEL); + this.generateScriptButton = this._extHostModelViewDialog.createButton(GENERATE_SCRIPT_LABEL); + this.nextButton = this._extHostModelViewDialog.createButton(NEXT_LABEL); + this.backButton = this._extHostModelViewDialog.createButton(PREVIOUS_LABEL); + this._extHostModelViewDialog.registerWizardPageInfoChangedCallback(this, info => this.handlePageInfoChanged(info)); + this.onPageChanged(info => this._currentPage = info.newPage); + } + + public get currentPage(): number { + return this._currentPage; + } + + public addPage(page: sqlops.window.modelviewdialog.WizardPage, index?: number): Thenable { + return this._extHostModelViewDialog.updateWizardPage(page).then(() => { + this._extHostModelViewDialog.addPage(this, page, index); + }); + } + + public removePage(index: number): Thenable { + return this._extHostModelViewDialog.removePage(this, index); + } + + public setCurrentPage(index: number): Thenable { + return this._extHostModelViewDialog.setWizardPage(this, index); + } + + public open(): Thenable { + return this._extHostModelViewDialog.openWizard(this); + } + + public close(): Thenable { + return this._extHostModelViewDialog.closeWizard(this); + } + + private handlePageInfoChanged(info: WizardPageEventInfo): void { + this._currentPage = info.pageChangeInfo.newPage; + if (info.eventType === WizardPageInfoEventType.PageAddedOrRemoved) { + this.pages = info.pages; + } else if (info.eventType === WizardPageInfoEventType.PageChanged) { + this._pageChangedEmitter.fire(info.pageChangeInfo); + } + } +} + export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { private static _currentHandle = 0; private readonly _proxy: MainThreadModelViewDialogShape; - private readonly _dialogHandles = new Map(); - private readonly _tabHandles = new Map(); - private readonly _buttonHandles = new Map(); - private readonly _editorHandles = new Map(); - + private readonly _objectHandles = new Map(); + private readonly _objectsByHandle = new Map(); private readonly _validityEmitters = new Map>(); + private readonly _pageInfoChangedCallbacks = new Map void>(); private readonly _onClickCallbacks = new Map void>(); constructor( @@ -183,38 +287,13 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { return handle; } - private getEditorHandle(editor: sqlops.workspace.ModelViewEditor) { - let handle = this._editorHandles.get(editor); + private getHandle(item: sqlops.window.modelviewdialog.Button | sqlops.window.modelviewdialog.Dialog | sqlops.window.modelviewdialog.DialogTab + | sqlops.window.modelviewdialog.ModelViewPanel | sqlops.window.modelviewdialog.Wizard | sqlops.window.modelviewdialog.WizardPage | sqlops.workspace.ModelViewEditor) { + let handle = this._objectHandles.get(item); if (handle === undefined) { handle = ExtHostModelViewDialog.getNewHandle(); - this._editorHandles.set(editor, handle); - } - return handle; - } - - private getDialogHandle(dialog: sqlops.window.modelviewdialog.Dialog) { - let handle = this._dialogHandles.get(dialog); - if (handle === undefined) { - handle = ExtHostModelViewDialog.getNewHandle(); - this._dialogHandles.set(dialog, handle); - } - return handle; - } - - private getTabHandle(tab: sqlops.window.modelviewdialog.DialogTab) { - let handle = this._tabHandles.get(tab); - if (handle === undefined) { - handle = ExtHostModelViewDialog.getNewHandle(); - this._tabHandles.set(tab, handle); - } - return handle; - } - - private getButtonHandle(button: sqlops.window.modelviewdialog.Button) { - let handle = this._buttonHandles.get(button); - if (handle === undefined) { - handle = ExtHostModelViewDialog.getNewHandle(); - this._buttonHandles.set(button, handle); + this._objectHandles.set(item, handle); + this._objectsByHandle.set(handle, item); } return handle; } @@ -223,32 +302,57 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { this._onClickCallbacks.get(handle)(); } - public $onDialogValidityChanged(handle: number, valid: boolean): void { + public $onPanelValidityChanged(handle: number, valid: boolean): void { let emitter = this._validityEmitters.get(handle); if (emitter) { emitter.fire(valid); } } - public open(dialog: sqlops.window.modelviewdialog.Dialog): void { - let handle = this.getDialogHandle(dialog); - this.updateDialogContent(dialog); - this._proxy.$open(handle); + public $onWizardPageChanged(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): void { + let callback = this._pageInfoChangedCallbacks.get(handle); + if (callback) { + callback({ + eventType: WizardPageInfoEventType.PageChanged, + pageChangeInfo: info + }); + } } - public close(dialog: sqlops.window.modelviewdialog.Dialog): void { - let handle = this.getDialogHandle(dialog); - this._proxy.$close(handle); + public $updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void { + let callback = this._pageInfoChangedCallbacks.get(handle); + if (callback) { + let pages = pageHandles.map(pageHandle => this._objectsByHandle.get(handle) as sqlops.window.modelviewdialog.WizardPage); + callback({ + eventType: WizardPageInfoEventType.PageAddedOrRemoved, + pageChangeInfo: { + lastPage: undefined, + newPage: currentPageIndex + }, + pages: pages + }); + } + } + + public openDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + let handle = this.getHandle(dialog); + this.updateDialogContent(dialog); + this._proxy.$openDialog(handle); + } + + public closeDialog(dialog: sqlops.window.modelviewdialog.Dialog): void { + let handle = this.getHandle(dialog); + this._proxy.$closeDialog(handle); } public createModelViewEditor(title: string): sqlops.workspace.ModelViewEditor { - let editor = new ModelViewEditorImpl(this._extHostModelView, this._proxy, title); - editor.handle = this.getEditorHandle(editor); + let editor = new ModelViewEditorImpl(this, this._extHostModelView, this._proxy, title); + editor.handle = this.getHandle(editor); return editor; } public updateDialogContent(dialog: sqlops.window.modelviewdialog.Dialog): void { - let handle = this.getDialogHandle(dialog); + let handle = this.getHandle(dialog); let tabs = dialog.content; if (tabs && typeof tabs !== 'string') { tabs.forEach(tab => this.updateTabContent(tab)); @@ -260,15 +364,15 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { this.updateButton(dialog.cancelButton); this._proxy.$setDialogDetails(handle, { title: dialog.title, - okButton: this.getButtonHandle(dialog.okButton), - cancelButton: this.getButtonHandle(dialog.cancelButton), - content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getTabHandle(tab)) : dialog.content as string, - customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getButtonHandle(button)) : undefined + okButton: this.getHandle(dialog.okButton), + cancelButton: this.getHandle(dialog.cancelButton), + content: dialog.content && typeof dialog.content !== 'string' ? dialog.content.map(tab => this.getHandle(tab)) : dialog.content as string, + customButtons: dialog.customButtons ? dialog.customButtons.map(button => this.getHandle(button)) : undefined }); } public updateTabContent(tab: sqlops.window.modelviewdialog.DialogTab): void { - let handle = this.getTabHandle(tab); + let handle = this.getHandle(tab); this._proxy.$setTabDetails(handle, { title: tab.title, content: tab.content @@ -276,7 +380,7 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } public updateButton(button: sqlops.window.modelviewdialog.Button): void { - let handle = this.getButtonHandle(button); + let handle = this.getHandle(button); this._proxy.$setButtonDetails(handle, { label: button.label, enabled: button.enabled, @@ -285,34 +389,34 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } public registerOnClickCallback(button: sqlops.window.modelviewdialog.Button, callback: () => void) { - let handle = this.getButtonHandle(button); + let handle = this.getHandle(button); this._onClickCallbacks.set(handle, callback); } public createDialog(title: string): sqlops.window.modelviewdialog.Dialog { let dialog = new DialogImpl(this, this._extHostModelView); dialog.title = title; - dialog.handle = this.getDialogHandle(dialog); + dialog.handle = this.getHandle(dialog); return dialog; } public createTab(title: string): sqlops.window.modelviewdialog.DialogTab { let tab = new TabImpl(this, this._extHostModelView); tab.title = title; - tab.handle = this.getTabHandle(tab); + tab.handle = this.getHandle(tab); return tab; } public createButton(label: string): sqlops.window.modelviewdialog.Button { let button = new ButtonImpl(this); - this.getButtonHandle(button); + this.getHandle(button); this.registerOnClickCallback(button, button.getOnClickCallback()); button.label = label; return button; } - public getValidityChangedEvent(dialog: sqlops.window.modelviewdialog.Dialog) { - let handle = this.getDialogHandle(dialog); + public getValidityChangedEvent(panel: sqlops.window.modelviewdialog.ModelViewPanel) { + let handle = this.getHandle(panel); let emitter = this._validityEmitters.get(handle); if (!emitter) { emitter = new Emitter(); @@ -320,4 +424,81 @@ export class ExtHostModelViewDialog implements ExtHostModelViewDialogShape { } return emitter.event; } + + public registerWizardPageInfoChangedCallback(wizard: sqlops.window.modelviewdialog.Wizard, callback: (info: WizardPageEventInfo) => void): void { + let handle = this.getHandle(wizard); + this._pageInfoChangedCallbacks.set(handle, callback); + } + + public createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { + let page = new WizardPageImpl(title, this, this._extHostModelView); + page.handle = this.getHandle(page); + return page; + } + + public createWizard(title: string): sqlops.window.modelviewdialog.Wizard { + let wizard = new WizardImpl(title, this); + this.getHandle(wizard); + return wizard; + } + + public updateWizardPage(page: sqlops.window.modelviewdialog.WizardPage): Thenable { + let handle = this.getHandle(page); + if (page.customButtons) { + page.customButtons.forEach(button => this.updateButton(button)); + } + return this._proxy.$setWizardPageDetails(handle, { + content: page.content, + customButtons: page.customButtons ? page.customButtons.map(button => this.getHandle(button)) : undefined, + enabled: page.enabled, + title: page.title + }); + } + + public updateWizard(wizard: sqlops.window.modelviewdialog.Wizard): Thenable { + let handle = this.getHandle(wizard); + wizard.pages.forEach(page => this.updateWizardPage(page)); + this.updateButton(wizard.backButton); + this.updateButton(wizard.cancelButton); + this.updateButton(wizard.generateScriptButton); + this.updateButton(wizard.doneButton); + this.updateButton(wizard.nextButton); + if (wizard.customButtons) { + wizard.customButtons.forEach(button => this.updateButton(button)); + } + return this._proxy.$setWizardDetails(handle, { + title: wizard.title, + pages: wizard.pages.map(page => this.getHandle(page)), + currentPage: wizard.currentPage, + backButton: this.getHandle(wizard.backButton), + cancelButton: this.getHandle(wizard.cancelButton), + generateScriptButton: this.getHandle(wizard.generateScriptButton), + doneButton: this.getHandle(wizard.doneButton), + nextButton: this.getHandle(wizard.nextButton), + customButtons: wizard.customButtons ? wizard.customButtons.map(button => this.getHandle(button)) : undefined + }); + } + + public addPage(wizard: sqlops.window.modelviewdialog.Wizard, page: sqlops.window.modelviewdialog.WizardPage, pageIndex?: number): Thenable { + return this._proxy.$addWizardPage(this.getHandle(wizard), this.getHandle(page), pageIndex); + } + + public removePage(wizard: sqlops.window.modelviewdialog.Wizard, pageIndex: number): Thenable { + return this._proxy.$removeWizardPage(this.getHandle(wizard), pageIndex); + } + + public setWizardPage(wizard: sqlops.window.modelviewdialog.Wizard, pageIndex: number): Thenable { + return this._proxy.$setWizardPage(this.getHandle(wizard), pageIndex); + } + + public openWizard(wizard: sqlops.window.modelviewdialog.Wizard): Thenable { + let handle = this.getHandle(wizard); + this.updateWizard(wizard); + return this._proxy.$openWizard(handle); + } + + public closeWizard(wizard: sqlops.window.modelviewdialog.Wizard): Thenable { + let handle = this.getHandle(wizard); + return this._proxy.$closeWizard(handle); + } } \ No newline at end of file diff --git a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts index 04af0cfa1d..04dd3ddc5a 100644 --- a/src/sql/workbench/api/node/mainThreadModelViewDialog.ts +++ b/src/sql/workbench/api/node/mainThreadModelViewDialog.ts @@ -11,9 +11,9 @@ import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { MainThreadModelViewDialogShape, SqlMainContext, ExtHostModelViewDialogShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; -import { Dialog, DialogTab, DialogButton } from 'sql/platform/dialog/dialogTypes'; +import { Dialog, DialogTab, DialogButton, WizardPage, Wizard } from 'sql/platform/dialog/dialogTypes'; import { CustomDialogService } from 'sql/platform/dialog/customDialogService'; -import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, IModelViewWizardPageDetails, IModelViewWizardDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; import { ModelViewInput } from 'sql/parts/modelComponents/modelEditor/modelViewInput'; import * as vscode from 'vscode'; @@ -24,6 +24,9 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape private readonly _dialogs = new Map(); private readonly _tabs = new Map(); private readonly _buttons = new Map(); + private readonly _wizardPages = new Map(); + private readonly _wizardPageHandles = new Map(); + private readonly _wizards = new Map(); private _dialogService: CustomDialogService; constructor( @@ -55,13 +58,13 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape }); } - public $open(handle: number): Thenable { + public $openDialog(handle: number): Thenable { let dialog = this.getDialog(handle); this._dialogService.showDialog(dialog); return Promise.resolve(); } - public $close(handle: number): Thenable { + public $closeDialog(handle: number): Thenable { let dialog = this.getDialog(handle); this._dialogService.closeDialog(dialog); return Promise.resolve(); @@ -75,7 +78,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape let cancelButton = this.getButton(details.cancelButton); dialog.okButton = okButton; dialog.cancelButton = cancelButton; - dialog.onValidityChanged(valid => this._proxy.$onDialogValidityChanged(handle, valid)); + dialog.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); this._dialogs.set(handle, dialog); } @@ -97,6 +100,7 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape let tab = this._tabs.get(handle); if (!tab) { tab = new DialogTab(details.title); + tab.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); this._tabs.set(handle, tab); } @@ -121,12 +125,91 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape return Promise.resolve(); } + public $setWizardPageDetails(handle: number, details: IModelViewWizardPageDetails): Thenable { + let page = this._wizardPages.get(handle); + if (!page) { + page = new WizardPage(details.title, details.content); + page.onValidityChanged(valid => this._proxy.$onPanelValidityChanged(handle, valid)); + this._wizardPages.set(handle, page); + this._wizardPageHandles.set(page, handle); + } + + page.title = details.title; + page.content = details.content; + page.enabled = details.enabled; + if (details.customButtons !== undefined) { + page.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); + } + + return Promise.resolve(); + } + + public $setWizardDetails(handle: number, details: IModelViewWizardDetails): Thenable { + let wizard = this._wizards.get(handle); + if (!wizard) { + wizard = new Wizard(details.title); + wizard.backButton = this.getButton(details.backButton); + wizard.cancelButton = this.getButton(details.cancelButton); + wizard.generateScriptButton = this.getButton(details.generateScriptButton); + wizard.doneButton = this.getButton(details.doneButton); + wizard.nextButton = this.getButton(details.nextButton); + wizard.onPageChanged(info => this._proxy.$onWizardPageChanged(handle, info)); + wizard.onPageAdded(() => this.handleWizardPageAddedOrRemoved(handle)); + wizard.onPageRemoved(() => this.handleWizardPageAddedOrRemoved(handle)); + this._wizards.set(handle, wizard); + } + + wizard.title = details.title; + wizard.pages = details.pages.map(handle => this.getWizardPage(handle)); + if (details.currentPage !== undefined) { + wizard.setCurrentPage(details.currentPage); + } + if (details.customButtons !== undefined) { + wizard.customButtons = details.customButtons.map(buttonHandle => this.getButton(buttonHandle)); + } + + return Promise.resolve(); + } + + public $addWizardPage(wizardHandle: number, pageHandle: number, pageIndex?: number): Thenable { + if (pageIndex === null) { + pageIndex = undefined; + } + let wizard = this.getWizard(wizardHandle); + let page = this.getWizardPage(pageHandle); + wizard.addPage(page, pageIndex); + return Promise.resolve(); + } + + public $removeWizardPage(wizardHandle: number, pageIndex: number): Thenable { + let wizard = this.getWizard(wizardHandle); + wizard.removePage(pageIndex); + return Promise.resolve(); + } + + public $setWizardPage(wizardHandle: number, pageIndex: number): Thenable { + let wizard = this.getWizard(wizardHandle); + wizard.setCurrentPage(pageIndex); + return Promise.resolve(); + } + + public $openWizard(handle: number): Thenable { + let wizard = this.getWizard(handle); + this._dialogService.showWizard(wizard); + return Promise.resolve(); + } + + public $closeWizard(handle: number): Thenable { + let wizard = this.getWizard(handle); + this._dialogService.closeWizard(wizard); + return Promise.resolve(); + } + private getDialog(handle: number): Dialog { let dialog = this._dialogs.get(handle); if (!dialog) { throw new Error('No dialog matching the given handle'); } - return dialog; } @@ -135,7 +218,6 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape if (!tab) { throw new Error('No tab matching the given handle'); } - return tab; } @@ -144,11 +226,31 @@ export class MainThreadModelViewDialog implements MainThreadModelViewDialogShape if (!button) { throw new Error('No button matching the given handle'); } - return button; } private onButtonClick(handle: number): void { this._proxy.$onButtonClick(handle); } + + private getWizardPage(handle: number): WizardPage { + let page = this._wizardPages.get(handle); + if (!page) { + throw new Error('No page matching the given handle'); + } + return page; + } + + private getWizard(handle: number): Wizard { + let wizard = this._wizards.get(handle); + if (!wizard) { + throw new Error('No wizard matching the given handle'); + } + return wizard; + } + + private handleWizardPageAddedOrRemoved(handle: number): void { + let wizard = this._wizards.get(handle); + this._proxy.$updateWizardPageInfo(handle, wizard.pages.map(page => this._wizardPageHandles.get(page)), wizard.currentPage); + } } \ No newline at end of file diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index baca85f688..810fdf4138 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -295,10 +295,16 @@ export function createApiFactory( return extHostModelViewDialog.createButton(label); }, openDialog(dialog: sqlops.window.modelviewdialog.Dialog) { - return extHostModelViewDialog.open(dialog); + return extHostModelViewDialog.openDialog(dialog); }, closeDialog(dialog: sqlops.window.modelviewdialog.Dialog) { - return extHostModelViewDialog.close(dialog); + return extHostModelViewDialog.closeDialog(dialog); + }, + createWizardPage(title: string): sqlops.window.modelviewdialog.WizardPage { + return extHostModelViewDialog.createWizardPage(title); + }, + createWizard(title: string): sqlops.window.modelviewdialog.Wizard { + return extHostModelViewDialog.createWizard(title); } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 4ce7295bec..038b43160d 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -7,7 +7,8 @@ import { createMainContextProxyIdentifier as createMainId, createExtHostContextProxyIdentifier as createExtId, - ProxyIdentifier, IRPCProtocol } from 'vs/workbench/services/extensions/node/proxyIdentifier'; + ProxyIdentifier, IRPCProtocol +} from 'vs/workbench/services/extensions/node/proxyIdentifier'; import { TPromise } from 'vs/base/common/winjs.base'; import { IDisposable } from 'vs/base/common/lifecycle'; @@ -16,7 +17,10 @@ import * as sqlops from 'sqlops'; import * as vscode from 'vscode'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; -import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { + IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, + IModelViewWizardDetails, IModelViewWizardPageDetails +} from 'sql/workbench/api/common/sqlExtHostTypes'; import Event, { Emitter } from 'vs/base/common/event'; export abstract class ExtHostAccountManagementShape { @@ -313,17 +317,17 @@ export abstract class ExtHostDataProtocolShape { /** * Get Agent Job list */ - $getJobs(handle: number, ownerUri: string): Thenable{ throw ni(); } + $getJobs(handle: number, ownerUri: string): Thenable { throw ni(); } /** * Get a Agent Job's history */ - $getJobHistory(handle: number, ownerUri: string, jobID: string): Thenable{ throw ni(); } + $getJobHistory(handle: number, ownerUri: string, jobID: string): Thenable { throw ni(); } /** * Run an action on a Job */ - $jobAction(handle: number, ownerUri: string, jobName: string, action: string): Thenable{ throw ni(); } + $jobAction(handle: number, ownerUri: string, jobName: string, action: string): Thenable { throw ni(); } } /** @@ -530,7 +534,7 @@ export interface MainThreadModelViewShape extends IDisposable { $addToContainer(handle: number, containerId: string, item: IItemConfig): Thenable; $setLayout(handle: number, componentId: string, layout: any): Thenable; $setProperties(handle: number, componentId: string, properties: { [key: string]: any }): Thenable; - $registerEvent(handle: number, componentId: string): Thenable; + $registerEvent(handle: number, componentId: string): Thenable; $validate(handle: number, componentId: string): Thenable; } @@ -539,7 +543,7 @@ export interface ExtHostObjectExplorerShape { export interface MainThreadObjectExplorerShape extends IDisposable { $getNode(connectionId: string, nodePath?: string): Thenable; - $getActiveConnectionNodes(): Thenable<{ nodeInfo: sqlops.NodeInfo, connectionId: string}[]>; + $getActiveConnectionNodes(): Thenable<{ nodeInfo: sqlops.NodeInfo, connectionId: string }[]>; $setExpandedState(connectionId: string, nodePath: string, expandedState: vscode.TreeItemCollapsibleState): Thenable; $setSelected(connectionId: string, nodePath: string, selected: boolean, clearOtherSelections?: boolean): Thenable; $getChildren(connectionId: string, nodePath: string): Thenable; @@ -549,16 +553,25 @@ export interface MainThreadObjectExplorerShape extends IDisposable { export interface ExtHostModelViewDialogShape { $onButtonClick(handle: number): void; - $onDialogValidityChanged(handle: number, valid: boolean): void; + $onPanelValidityChanged(handle: number, valid: boolean): void; + $onWizardPageChanged(handle: number, info: sqlops.window.modelviewdialog.WizardPageChangeInfo): void; + $updateWizardPageInfo(handle: number, pageHandles: number[], currentPageIndex: number): void; } export interface MainThreadModelViewDialogShape extends IDisposable { $openEditor(modelViewId: string, title: string, position?: vscode.ViewColumn): Thenable; - $open(handle: number): Thenable; - $close(handle: number): Thenable; + $openDialog(handle: number): Thenable; + $closeDialog(handle: number): Thenable; $setDialogDetails(handle: number, details: IModelViewDialogDetails): Thenable; $setTabDetails(handle: number, details: IModelViewTabDetails): Thenable; $setButtonDetails(handle: number, details: IModelViewButtonDetails): Thenable; + $openWizard(handle: number): Thenable; + $closeWizard(handle: number): Thenable; + $setWizardPageDetails(handle: number, details: IModelViewWizardPageDetails): Thenable; + $setWizardDetails(handle: number, details: IModelViewWizardDetails): Thenable; + $addWizardPage(wizardHandle: number, pageHandle: number, pageIndex: number): Thenable; + $removeWizardPage(wizardHandle: number, pageIndex: number): Thenable; + $setWizardPage(wizardHandle: number, pageIndex: number): Thenable; } export interface ExtHostQueryEditorShape { } diff --git a/src/sqltest/platform/dialog/dialogPane.test.ts b/src/sqltest/platform/dialog/dialogPane.test.ts index e1c8ddfcef..fc48110051 100644 --- a/src/sqltest/platform/dialog/dialogPane.test.ts +++ b/src/sqltest/platform/dialog/dialogPane.test.ts @@ -30,7 +30,7 @@ suite('Dialog Pane Tests', () => { // If I fill in a dialog's content with the ID of a specific model view provider and then render the dialog let modelViewId = 'test_content'; dialog.content = modelViewId; - let dialogPane = new DialogPane(dialog, mockBootstrapService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockBootstrapService.object); dialogPane.createBody(container); // Then a single dialog-modelview-container element is added directly to the dialog pane @@ -47,7 +47,7 @@ suite('Dialog Pane Tests', () => { // If I fill in a dialog's content with a single tab and then render the dialog let modelViewId = 'test_content'; dialog.content = [new DialogTab('', modelViewId)]; - let dialogPane = new DialogPane(dialog, mockBootstrapService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockBootstrapService.object); dialogPane.createBody(container); // Then a single dialog-modelview-container element is added directly to the dialog pane @@ -65,7 +65,7 @@ suite('Dialog Pane Tests', () => { let modelViewId1 = 'test_content_1'; let modelViewId2 = 'test_content_2'; dialog.content = [new DialogTab('tab1', modelViewId1), new DialogTab('tab2', modelViewId2)]; - let dialogPane = new DialogPane(dialog, mockBootstrapService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, () => undefined, mockBootstrapService.object); dialogPane.createBody(container); // Then a dialog-modelview-container element is added for the first tab (subsequent ones get added when the tab is actually clicked) @@ -90,7 +90,7 @@ suite('Dialog Pane Tests', () => { let modelViewId1 = 'test_content_1'; let modelViewId2 = 'test_content_2'; dialog.content = [new DialogTab('tab1', modelViewId1), new DialogTab('tab2', modelViewId2)]; - let dialogPane = new DialogPane(dialog, mockBootstrapService.object); + let dialogPane = new DialogPane(dialog.title, dialog.content, valid => dialog.notifyValidityChanged(valid), mockBootstrapService.object); dialogPane.createBody(container); let validityChanges: boolean[] = []; @@ -114,7 +114,7 @@ suite('Dialog Pane Tests', () => { // If I set tab 1's validation to false validationCallbacks[0](false); - + // Then the whole dialog's validation is false assert.equal(dialog.valid, false); assert.equal(validityChanges.length, 3); diff --git a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts index 8e7d4377af..85f9290dd1 100644 --- a/src/sqltest/workbench/api/extHostModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/extHostModelViewDialog.test.ts @@ -18,17 +18,21 @@ suite('ExtHostModelViewDialog Tests', () => { setup(() => { mockProxy = Mock.ofInstance({ - $open: handle => undefined, - $close: handle => undefined, + $openDialog: handle => undefined, + $closeDialog: handle => undefined, $setDialogDetails: (handle, details) => undefined, $setTabDetails: (handle, details) => undefined, - $setButtonDetails: (handle, details) => undefined + $setButtonDetails: (handle, details) => undefined, + $openWizard: handle => undefined, + $closeWizard: handle => undefined, + $setWizardPageDetails: (handle, details) => undefined, + $setWizardDetails: (handle, details) => undefined }); let mainContext = { getProxy: proxyType => mockProxy.object }; - extHostModelView = Mock.ofInstance( { + extHostModelView = Mock.ofInstance({ $registerProvider: (widget, handler) => undefined }); extHostModelViewDialog = new ExtHostModelViewDialog(mainContext, extHostModelView.object); @@ -59,7 +63,7 @@ suite('ExtHostModelViewDialog Tests', () => { }); test('Opening a dialog updates its tabs and buttons on the main thread', () => { - mockProxy.setup(x => x.$open(It.isAny())); + mockProxy.setup(x => x.$openDialog(It.isAny())); mockProxy.setup(x => x.$setDialogDetails(It.isAny(), It.isAny())); mockProxy.setup(x => x.$setTabDetails(It.isAny(), It.isAny())); mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny())); @@ -77,25 +81,26 @@ suite('ExtHostModelViewDialog Tests', () => { button1.enabled = false; let button2Label = 'button_2'; let button2 = extHostModelViewDialog.createButton(button2Label); + dialog.customButtons = [button1, button2]; // Open the dialog and verify that the correct main thread methods were called - extHostModelViewDialog.open(dialog); + extHostModelViewDialog.openDialog(dialog); mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => { return details.enabled === false && details.label === button1Label; - })), Times.once()); + })), Times.atLeastOnce()); mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => { return details.enabled === true && details.label === button2Label; - })), Times.once()); + })), Times.atLeastOnce()); mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => { return details.title === tab1Title; - })), Times.once()); + })), Times.atLeastOnce()); mockProxy.verify(x => x.$setTabDetails(It.isAny(), It.is(details => { return details.title === tab2Title; - })), Times.once()); + })), Times.atLeastOnce()); mockProxy.verify(x => x.$setDialogDetails(It.isAny(), It.is(details => { - return details.title === dialogTitle; - })), Times.once()); - mockProxy.verify(x => x.$open(It.isAny()), Times.once()); + return details.title === dialogTitle && details.content.length === 2 && details.customButtons.length === 2; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$openDialog(It.isAny()), Times.once()); }); test('Button clicks are forwarded to the correct button', () => { @@ -123,4 +128,139 @@ suite('ExtHostModelViewDialog Tests', () => { // Then the clicks should have been handled by the expected handlers assert.deepEqual(clickEvents, [1, 2, 2, 1]); }); + + test('Creating a wizard returns a wizard with initialized buttons and the given title', () => { + let title = 'wizard_title'; + let wizard = extHostModelViewDialog.createWizard(title); + + assert.equal(wizard.title, title); + assert.equal(wizard.doneButton.enabled, true); + assert.equal(wizard.cancelButton.enabled, true); + assert.equal(wizard.nextButton.enabled, true); + assert.equal(wizard.backButton.enabled, true); + assert.deepEqual(wizard.pages, []); + }); + + test('Opening a wizard updates its pages and buttons on the main thread', () => { + mockProxy.setup(x => x.$openWizard(It.isAny())); + mockProxy.setup(x => x.$setWizardDetails(It.isAny(), It.isAny())); + mockProxy.setup(x => x.$setWizardPageDetails(It.isAny(), It.isAny())); + mockProxy.setup(x => x.$setButtonDetails(It.isAny(), It.isAny())); + + // Create a wizard with 2 pages and 2 custom buttons + let wizardTitle = 'wizard_title'; + let wizard = extHostModelViewDialog.createWizard(wizardTitle); + let page1Title = 'page_1'; + let page1 = extHostModelViewDialog.createWizardPage(page1Title); + let page2Title = 'page_2'; + let page2 = extHostModelViewDialog.createWizardPage(page2Title); + wizard.pages = [page1, page2]; + let button1Label = 'button_1'; + let button1 = extHostModelViewDialog.createButton(button1Label); + button1.enabled = false; + let button2Label = 'button_2'; + let button2 = extHostModelViewDialog.createButton(button2Label); + wizard.customButtons = [button1, button2]; + + // Open the wizard and verify that the correct main thread methods were called + extHostModelViewDialog.openWizard(wizard); + mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => { + return details.enabled === false && details.label === button1Label; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$setButtonDetails(It.isAny(), It.is(details => { + return details.enabled === true && details.label === button2Label; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$setWizardPageDetails(It.isAny(), It.is(details => { + return details.title === page1Title; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$setWizardPageDetails(It.isAny(), It.is(details => { + return details.title === page2Title; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$setWizardDetails(It.isAny(), It.is(details => { + return details.title === wizardTitle && details.pages.length === 2 && details.customButtons.length === 2; + })), Times.atLeastOnce()); + mockProxy.verify(x => x.$openWizard(It.isAny()), Times.once()); + }); + + test('Wizard page changed events are handled correctly', () => { + // Set up the main thread mock to record the handle assigned to the wizard + let wizardHandle: number; + mockProxy.setup(x => x.$setWizardDetails(It.isAny(), It.isAny())).callback((handle, details) => wizardHandle = handle); + + // Set up the wizard with 2 pages + let wizard = extHostModelViewDialog.createWizard('test_wizard'); + let page1 = extHostModelViewDialog.createWizardPage('page_1'); + let page2 = extHostModelViewDialog.createWizardPage('page_2'); + wizard.pages = [page1, page2]; + extHostModelViewDialog.updateWizard(wizard); + + // Record page changed events + let actualPageChangeInfo = []; + wizard.onPageChanged(pageChangeInfo => { + actualPageChangeInfo.push(pageChangeInfo); + }); + + // Call the page changed event and verify that it was handled + let expectedPageChangeInfo = { + lastPage: 0, + newPage: 1 + }; + extHostModelViewDialog.$onWizardPageChanged(wizardHandle, expectedPageChangeInfo); + assert.equal(actualPageChangeInfo.length, 1); + assert.equal(actualPageChangeInfo[0], expectedPageChangeInfo); + assert.equal(wizard.currentPage, expectedPageChangeInfo.newPage); + }); + + test('Validity changed events are handled correctly', () => { + // Set up the main thread mock to record handles assigned to tabs + let tabHandles = []; + mockProxy.setup(x => x.$setTabDetails(It.isAny(), It.isAny())).callback((handle, details) => tabHandles.push(handle)); + + // Set up the dialog with 2 tabs + let dialog = extHostModelViewDialog.createDialog('test_dialog'); + let tab1 = extHostModelViewDialog.createTab('tab_1'); + let tab2 = extHostModelViewDialog.createTab('tab_2'); + dialog.content = [tab1, tab2]; + extHostModelViewDialog.updateDialogContent(dialog); + + // Record tab validity changed events + let tab1ValidityChangedEvents = []; + let tab2ValidityChangedEvents = []; + tab1.onValidityChanged(valid => tab1ValidityChangedEvents.push(valid)); + tab2.onValidityChanged(valid => tab2ValidityChangedEvents.push(valid)); + + // Call the validity changed event on tab 2 and verify that it was handled but tab 1 is still not valid + extHostModelViewDialog.$onPanelValidityChanged(tabHandles[1], false); + assert.equal(tab1ValidityChangedEvents.length, 0); + assert.equal(tab1.valid, true); + assert.equal(tab2ValidityChangedEvents.length, 1); + assert.equal(tab2ValidityChangedEvents[0], false); + assert.equal(tab2.valid, false); + }); + + test('Verify validity changed events update validity for all panel types', () => { + // Set up the main thread mock to record handles for the tab, dialog, and page + let tabHandle: number; + let dialogHandle: number; + let pageHandle: number; + mockProxy.setup(x => x.$setTabDetails(It.isAny(), It.isAny())).callback((handle, details) => tabHandle = handle); + mockProxy.setup(x => x.$setDialogDetails(It.isAny(), It.isAny())).callback((handle, details) => dialogHandle = handle); + mockProxy.setup(x => x.$setWizardPageDetails(It.isAny(), It.isAny())).callback((handle, details) => pageHandle = handle); + + // Initialize a tab, dialog, and page + let tab = extHostModelViewDialog.createTab('tab_1'); + extHostModelViewDialog.updateTabContent(tab); + let dialog = extHostModelViewDialog.createDialog('dialog_1'); + extHostModelViewDialog.updateDialogContent(dialog); + let page = extHostModelViewDialog.createWizardPage('page_1'); + extHostModelViewDialog.updateWizardPage(page); + + // Call the validity changed event on each object and verify that the object's validity was updated + extHostModelViewDialog.$onPanelValidityChanged(tabHandle, false); + assert.equal(tab.valid, false); + extHostModelViewDialog.$onPanelValidityChanged(dialogHandle, false); + assert.equal(dialog.valid, false); + extHostModelViewDialog.$onPanelValidityChanged(pageHandle, false); + assert.equal(page.valid, false); + }); }); \ 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 e3c998961f..8c704d2549 100644 --- a/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts +++ b/src/sqltest/workbench/api/mainThreadModelViewDialog.test.ts @@ -7,9 +7,9 @@ import * as assert from 'assert'; import { Mock, It, Times } from 'typemoq'; import { MainThreadModelViewDialog } from 'sql/workbench/api/node/mainThreadModelViewDialog'; import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; -import { IModelViewButtonDetails, IModelViewTabDetails, IModelViewDialogDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IModelViewButtonDetails, IModelViewTabDetails, IModelViewDialogDetails, IModelViewWizardPageDetails, IModelViewWizardDetails } from 'sql/workbench/api/common/sqlExtHostTypes'; import { CustomDialogService } from 'sql/platform/dialog/customDialogService'; -import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes'; +import { Dialog, DialogTab, Wizard } from 'sql/platform/dialog/dialogTypes'; import { ExtHostModelViewDialogShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; import { Emitter } from 'vs/base/common/event'; @@ -20,6 +20,7 @@ suite('MainThreadModelViewDialog Tests', () => { let mockExtHostModelViewDialog: Mock; let mockDialogService: Mock; let openedDialog: Dialog; + let openedWizard: Wizard; // Dialog details let button1Details: IModelViewButtonDetails; @@ -37,9 +38,28 @@ suite('MainThreadModelViewDialog Tests', () => { let tab2Handle = 6; let dialogHandle = 7; + // Wizard details + let nextButtonDetails: IModelViewButtonDetails; + let backButtonDetails: IModelViewButtonDetails; + let generateScriptButtonDetails: IModelViewButtonDetails; + let page1Details: IModelViewWizardPageDetails; + let page2Details: IModelViewWizardPageDetails; + let page3Details: IModelViewWizardPageDetails; + let wizardDetails: IModelViewWizardDetails; + let nextButtonHandle = 8; + let backButtonHandle = 9; + let generateScriptButtonHandle = 10; + let page1Handle = 11; + let page2Handle = 12; + let wizardHandle = 13; + let page3Handle = 14; + setup(() => { mockExtHostModelViewDialog = Mock.ofInstance({ - $onButtonClick: handle => undefined + $onButtonClick: handle => undefined, + $onPanelValidityChanged: (handle, valid) => undefined, + $onWizardPageChanged: (handle, info) => undefined, + $updateWizardPageInfo: (wizardHandle, pageHandles, currentPageIndex) => undefined }); let extHostContext = { getProxy: proxyType => mockExtHostModelViewDialog.object @@ -50,6 +70,11 @@ suite('MainThreadModelViewDialog Tests', () => { mockDialogService = Mock.ofType(CustomDialogService, undefined, undefined); openedDialog = undefined; mockDialogService.setup(x => x.showDialog(It.isAny())).callback(dialog => openedDialog = dialog); + mockDialogService.setup(x => x.showWizard(It.isAny())).callback(wizard => { + openedWizard = wizard; + // The actual service will set the page to 0 when it opens the wizard + openedWizard.setCurrentPage(0); + }); (mainThreadModelViewDialog as any)._dialogService = mockDialogService.object; // Set up the dialog details @@ -89,6 +114,46 @@ suite('MainThreadModelViewDialog Tests', () => { customButtons: [button1Handle, button2Handle] }; + // Set up the wizard details + nextButtonDetails = { + label: 'next_label', + enabled: true, + hidden: false + }; + backButtonDetails = { + label: 'back_label', + enabled: true, + hidden: false + }; + generateScriptButtonDetails = { + label: 'generate_script_label', + enabled: true, + hidden: false + }; + page1Details = { + title: 'page1', + content: 'content1', + enabled: true, + customButtons: [] + }; + page2Details = { + title: 'page2', + content: 'content2', + enabled: true, + customButtons: [button1Handle, button2Handle] + }; + wizardDetails = { + backButton: backButtonHandle, + nextButton: nextButtonHandle, + generateScriptButton: generateScriptButtonHandle, + cancelButton: cancelButtonHandle, + doneButton: okButtonHandle, + currentPage: undefined, + title: 'wizard_title', + customButtons: [], + pages: [page1Handle, page2Handle] + }; + // Register the buttons, tabs, and dialog mainThreadModelViewDialog.$setButtonDetails(button1Handle, button1Details); mainThreadModelViewDialog.$setButtonDetails(button2Handle, button2Details); @@ -97,11 +162,19 @@ suite('MainThreadModelViewDialog Tests', () => { mainThreadModelViewDialog.$setTabDetails(tab1Handle, tab1Details); mainThreadModelViewDialog.$setTabDetails(tab2Handle, tab2Details); mainThreadModelViewDialog.$setDialogDetails(dialogHandle, dialogDetails); + + // Register the wizard and its pages and buttons + mainThreadModelViewDialog.$setButtonDetails(nextButtonHandle, nextButtonDetails); + mainThreadModelViewDialog.$setButtonDetails(backButtonHandle, backButtonDetails); + mainThreadModelViewDialog.$setButtonDetails(generateScriptButtonHandle, generateScriptButtonDetails); + mainThreadModelViewDialog.$setWizardPageDetails(page1Handle, page1Details); + mainThreadModelViewDialog.$setWizardPageDetails(page2Handle, page2Details); + mainThreadModelViewDialog.$setWizardDetails(wizardHandle, wizardDetails); }); test('Creating a dialog and calling open on it causes a dialog with correct content and buttons to open', () => { // If I open the dialog - mainThreadModelViewDialog.$open(dialogHandle); + mainThreadModelViewDialog.$openDialog(dialogHandle); // Then the opened dialog's content and buttons match what was set mockDialogService.verify(x => x.showDialog(It.isAny()), Times.once()); @@ -129,7 +202,7 @@ suite('MainThreadModelViewDialog Tests', () => { mockExtHostModelViewDialog.setup(x => x.$onButtonClick(It.isAny())).callback(handle => pressedHandles.push(handle)); // Open the dialog so that its buttons can be accessed - mainThreadModelViewDialog.$open(dialogHandle); + mainThreadModelViewDialog.$openDialog(dialogHandle); // Set up click emitters for each button let okEmitter = new Emitter(); @@ -154,4 +227,93 @@ suite('MainThreadModelViewDialog Tests', () => { // Verify that the correct button click notifications were sent to the proxy assert.deepEqual(pressedHandles, [button1Handle, button2Handle, okButtonHandle, cancelButtonHandle, button2Handle, cancelButtonHandle, button1Handle, okButtonHandle]); }); + + test('Creating a wizard and calling open on it causes a wizard with correct pages and buttons to open', () => { + // If I open the wizard + mainThreadModelViewDialog.$openWizard(wizardHandle); + + // Then the opened wizard's content and buttons match what was set + mockDialogService.verify(x => x.showWizard(It.isAny()), Times.once()); + assert.notEqual(openedWizard, undefined); + assert.equal(openedWizard.title, wizardDetails.title); + assert.equal(openedWizard.doneButton.label, okButtonDetails.label); + assert.equal(openedWizard.doneButton.enabled, okButtonDetails.enabled); + assert.equal(openedWizard.cancelButton.label, cancelButtonDetails.label); + assert.equal(openedWizard.cancelButton.enabled, cancelButtonDetails.enabled); + assert.equal(openedWizard.customButtons.length, 0); + assert.equal(openedWizard.pages.length, 2); + assert.equal(openedWizard.currentPage, 0); + let page1 = openedWizard.pages[0]; + assert.equal(page1.title, page1Details.title); + assert.equal(page1.content, page1Details.content); + assert.equal(page1.enabled, page1Details.enabled); + assert.equal(page1.valid, true); + assert.equal(page1.customButtons.length, 0); + let page2 = openedWizard.pages[1]; + assert.equal(page2.title, page2Details.title); + assert.equal(page2.content, page2Details.content); + assert.equal(page2.enabled, page2Details.enabled); + assert.equal(page2.valid, true); + assert.equal(page2.customButtons.length, 2); + }); + + test('The extension host gets notified when wizard page change events occur', () => { + mockExtHostModelViewDialog.setup(x => x.$onWizardPageChanged(It.isAny(), It.isAny())); + + // If I open the wizard and change the page to index 1 + mainThreadModelViewDialog.$openWizard(wizardHandle); + openedWizard.setCurrentPage(1); + + // Then a page changed event gets sent to the extension host + mockExtHostModelViewDialog.verify(x => x.$onWizardPageChanged(It.is(handle => handle === wizardHandle), + It.is(pageChangeInfo => pageChangeInfo.lastPage === 0 && pageChangeInfo.newPage === 1)), Times.once()); + }); + + test('Validity changed events are forwarded to the extension host', () => { + mockExtHostModelViewDialog.setup(x => x.$onPanelValidityChanged(It.isAny(), It.isAny())); + + // If I open the dialog and set its validity and its 2nd tab's validity to false + mainThreadModelViewDialog.$openDialog(dialogHandle); + (openedDialog.content[1] as DialogTab).notifyValidityChanged(false); + openedDialog.notifyValidityChanged(false); + + // Then a validity changed event gets sent to the extension host for the tab and the dialog + mockExtHostModelViewDialog.verify(x => x.$onPanelValidityChanged(It.is(handle => handle === dialogHandle), It.is(valid => valid === false)), Times.once()); + mockExtHostModelViewDialog.verify(x => x.$onPanelValidityChanged(It.is(handle => handle === tab2Handle), It.is(valid => valid === false)), Times.once()); + }); + + test('addWizardPage method inserts pages at the correct spot and notifies the extension host', () => { + mockExtHostModelViewDialog.setup(x => x.$updateWizardPageInfo(It.isAny(), It.isAny(), It.isAny())); + page3Details = { + title: 'page_3', + content: 'content_3', + customButtons: [], + enabled: true + }; + + // If I open the wizard and then add a page + mainThreadModelViewDialog.$openWizard(wizardHandle); + mainThreadModelViewDialog.$setWizardPageDetails(page3Handle, page3Details); + mainThreadModelViewDialog.$addWizardPage(wizardHandle, page3Handle, 0); + + // Then the updated page info gets sent to the extension host + mockExtHostModelViewDialog.verify(x => x.$updateWizardPageInfo( + It.is(handle => handle === wizardHandle), + It.is(pageHandles => pageHandles.length === 3 && pageHandles[0] === page3Handle), + It.is(currentPage => currentPage === 1)), Times.once()); + }); + + test('removeWizardPage method removes pages at the correct spot and notifies the extension host', () => { + mockExtHostModelViewDialog.setup(x => x.$updateWizardPageInfo(It.isAny(), It.isAny(), It.isAny())); + + // If I open the wizard and then remove a page + mainThreadModelViewDialog.$openWizard(wizardHandle); + mainThreadModelViewDialog.$removeWizardPage(wizardHandle, 0); + + // Then the updated page info gets sent to the extension host + mockExtHostModelViewDialog.verify(x => x.$updateWizardPageInfo( + It.is(handle => handle === wizardHandle), + It.is(pageHandles => pageHandles.length === 1 && pageHandles[0] === page2Handle), + It.is(currentPage => currentPage === 0)), Times.once()); + }); }); \ No newline at end of file