Enable basic wizard API (#1450)

This commit is contained in:
Matt Irvine
2018-05-21 15:19:21 -07:00
committed by GitHub
parent 70819252a9
commit 8e234d9b2d
16 changed files with 1247 additions and 157 deletions

View File

@@ -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<Dialog, DialogModal>();
private _wizardModals = new Map<Wizard, WizardModal>();
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();
}
}
}

View File

@@ -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);
}

View File

@@ -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());
}
}

View File

@@ -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<boolean>();
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<boolean>();
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>): void {
clickEvent(() => this._onClick.fire());
}
}
export class WizardPage extends DialogTab {
public customButtons: DialogButton[];
private _enabled: boolean;
private _onUpdate: Emitter<void> = new Emitter<void>();
public readonly onUpdate: Event<void> = 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<sqlops.window.modelviewdialog.WizardPageChangeInfo>();
public readonly onPageChanged = this._pageChangedEmitter.event;
private _pageAddedEmitter = new Emitter<WizardPage>();
public readonly onPageAdded = this._pageAddedEmitter.event;
private _pageRemovedEmitter = new Emitter<WizardPage>();
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);
}
}

View File

@@ -26,3 +26,7 @@
.dialogModal-hidden {
display: none;
}
.footer-button.dialogModal-hidden {
margin: 0;
}

View File

@@ -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<WizardPage, DialogPane>();
private _onDone = new Emitter<void>();
private _onCancel = new Emitter<void>();
// 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());
}
}