diff --git a/src/sql/parts/modelComponents/modelViewContent.component.ts b/src/sql/parts/modelComponents/modelViewContent.component.ts index 0eea1aeb08..0aeadb17ea 100644 --- a/src/sql/parts/modelComponents/modelViewContent.component.ts +++ b/src/sql/parts/modelComponents/modelViewContent.component.ts @@ -64,6 +64,10 @@ export class ModelViewContent extends ViewBase implements OnInit, IModelView { @memoize public get connection(): sqlops.connection.Connection { + if (!this._commonService.connectionManagementService) { + return undefined; + } + let currentConnection = this._commonService.connectionManagementService.connectionInfo.connectionProfile; let connection: sqlops.connection.Connection = { providerName: currentConnection.providerName, @@ -75,6 +79,10 @@ export class ModelViewContent extends ViewBase implements OnInit, IModelView { @memoize public get serverInfo(): sqlops.ServerInfo { + if (!this._commonService.connectionManagementService) { + return undefined; + } + return this._commonService.connectionManagementService.connectionInfo.serverInfo; } } diff --git a/src/sql/platform/dialog/customDialogService.ts b/src/sql/platform/dialog/customDialogService.ts new file mode 100644 index 0000000000..684673c2a5 --- /dev/null +++ b/src/sql/platform/dialog/customDialogService.ts @@ -0,0 +1,25 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * 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 { IModalOptions } from 'sql/base/browser/ui/modal/modal'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; + +const defaultOptions: IModalOptions = { hasBackButton: true, isWide: true }; + +export class CustomDialogService { + constructor( @IInstantiationService private _instantiationService: IInstantiationService) { } + + public showDialog(dialog: Dialog, options?: IModalOptions): void { + let optionsDialog = this._instantiationService.createInstance(DialogModal, dialog, 'CustomDialog', options || defaultOptions); + optionsDialog.render(); + optionsDialog.open(); + } +} diff --git a/src/sql/platform/dialog/dialog.module.ts b/src/sql/platform/dialog/dialog.module.ts new file mode 100644 index 0000000000..78324057b1 --- /dev/null +++ b/src/sql/platform/dialog/dialog.module.ts @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- +* 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 { forwardRef, NgModule, ComponentFactoryResolver, Inject, ApplicationRef } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { CommonModule, APP_BASE_HREF } from '@angular/common'; +import { BrowserModule } from '@angular/platform-browser'; +import { DialogContainer } from 'sql/platform/dialog/dialogContainer.component'; +import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; +import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component'; +import { ModelComponentWrapper } from 'sql/parts/modelComponents/modelComponentWrapper.component'; +import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive'; +import { BOOTSTRAP_SERVICE_ID, IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { Registry } from 'vs/platform/registry/common/platform'; + +/* Model-backed components */ +let extensionComponents = Registry.as(Extensions.ComponentContribution).getAllCtors(); + +@NgModule({ + declarations: [ + DialogContainer, + ModelViewContent, + ModelComponentWrapper, + ComponentHostDirective, + ...extensionComponents + ], + entryComponents: [DialogContainer, ...extensionComponents], + imports: [ + FormsModule, + CommonModule, + BrowserModule + ], + providers: [{ provide: APP_BASE_HREF, useValue: '/' }, CommonServiceInterface] +}) +export class DialogModule { + + constructor( + @Inject(forwardRef(() => ComponentFactoryResolver)) private _resolver: ComponentFactoryResolver, + @Inject(BOOTSTRAP_SERVICE_ID) private _bootstrapService: IBootstrapService, + @Inject(forwardRef(() => CommonServiceInterface)) bootstrap: CommonServiceInterface, + ) { + } + + ngDoBootstrap(appRef: ApplicationRef) { + const factoryWrapper: any = this._resolver.resolveComponentFactory(DialogContainer); + const uniqueSelector: string = this._bootstrapService.getUniqueSelector('dialog-modelview-container'); + factoryWrapper.factory.selector = uniqueSelector; + appRef.bootstrap(factoryWrapper); + } +} diff --git a/src/sql/platform/dialog/dialogContainer.component.ts b/src/sql/platform/dialog/dialogContainer.component.ts new file mode 100644 index 0000000000..f67f9aac0b --- /dev/null +++ b/src/sql/platform/dialog/dialogContainer.component.ts @@ -0,0 +1,45 @@ +/*--------------------------------------------------------------------------------------------- +* 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 { Component, AfterContentInit, ViewChild, Input, Inject, forwardRef, ElementRef } from '@angular/core'; +import { ModelViewContent } from 'sql/parts/modelComponents/modelViewContent.component'; +import { BootstrapParams } from 'sql/services/bootstrap/bootstrapParams'; +import { BOOTSTRAP_SERVICE_ID, IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; +import Event, { Emitter } from 'vs/base/common/event'; + +export interface DialogComponentParams extends BootstrapParams { + modelViewId: string; +} + +@Component({ + selector: 'dialog-modelview-container', + providers: [], + template: ` + + + ` +}) +export class DialogContainer implements AfterContentInit { + private _onResize = new Emitter(); + public readonly onResize: Event = this._onResize.event; + + public modelViewId: string; + @ViewChild(ModelViewContent) private _modelViewContent: ModelViewContent; + constructor( + @Inject(forwardRef(() => ElementRef)) el: ElementRef, + @Inject(BOOTSTRAP_SERVICE_ID) bootstrapService: IBootstrapService) { + this.modelViewId = (bootstrapService.getBootstrapParams(el.nativeElement.tagName) as DialogComponentParams).modelViewId; + } + + ngAfterContentInit(): void { + } + + public layout(): void { + this._modelViewContent.layout(); + } +} diff --git a/src/sql/platform/dialog/dialogModal.ts b/src/sql/platform/dialog/dialogModal.ts new file mode 100644 index 0000000000..a5eec9a9f5 --- /dev/null +++ b/src/sql/platform/dialog/dialogModal.ts @@ -0,0 +1,105 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Dialog } from 'sql/platform/dialog/dialogTypes'; +import { DialogPane } from 'sql/platform/dialog/dialogPane'; +import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; +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 { Button } from 'vs/base/browser/ui/button/button'; +import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; +import { localize } from 'vs/nls'; + +export class DialogModal extends Modal { + private static readonly DONE_BUTTON_LABEL = localize('dialogModalDoneButtonLabel', 'Done'); + private static readonly CANCEL_BUTTON_LABEL = localize('dialogModalCancelButtonLabel', 'Cancel'); + + private _dialogPane: DialogPane; + + // Wizard HTML elements + private _body: HTMLElement; + + // Buttons + private _cancelButton: Button; + private _doneButton: Button; + + constructor( + private _dialog: Dialog, + name: string, + options: IModalOptions, + @IPartService partService: IPartService, + @IWorkbenchThemeService private _themeService: IWorkbenchThemeService, + @ITelemetryService telemetryService: ITelemetryService, + @IContextKeyService contextKeyService: IContextKeyService, + @IBootstrapService private _bootstrapService: IBootstrapService + ) { + super(_dialog.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._cancelButton = this.addFooterButton(DialogModal.CANCEL_BUTTON_LABEL, () => this.cancel()); + this._doneButton = this.addFooterButton(DialogModal.DONE_BUTTON_LABEL, () => this.done()); + attachButtonStyler(this._cancelButton, this._themeService); + attachButtonStyler(this._doneButton, this._themeService); + } + + protected renderBody(container: HTMLElement): void { + new Builder(container).div({ class: 'dialogModal-body' }, (bodyBuilder) => { + this._body = bodyBuilder.getHTMLElement(); + }); + + this._dialogPane = new DialogPane(this._dialog, this._bootstrapService); + this._dialogPane.createBody(this._body); + } + + public open(): void { + this.show(); + } + + public done(): void { + this.dispose(); + this.hide(); + } + + public cancel(): void { + this.dispose(); + this.hide(); + } + + protected hide(): void { + super.hide(); + } + + protected show(): void { + super.show(); + } + + public dispose(): void { + super.dispose(); + this._dialogPane.dispose(); + } +} diff --git a/src/sql/platform/dialog/dialogPane.ts b/src/sql/platform/dialog/dialogPane.ts new file mode 100644 index 0000000000..c8aec8b9e2 --- /dev/null +++ b/src/sql/platform/dialog/dialogPane.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { NgModuleRef } from '@angular/core'; +import { IModalDialogStyles } from 'sql/base/browser/ui/modal/modal'; +import { Dialog } 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'; +import { DialogComponentParams } from 'sql/platform/dialog/dialogContainer.component'; +import { Builder } from 'vs/base/browser/builder'; +import { IThemable } from 'vs/platform/theme/common/styler'; +import { Disposable } from 'vs/base/common/lifecycle'; + +export class DialogPane extends Disposable implements IThemable { + private _activeTabIndex: number; + private _tabbedPanel: TabbedPanel; + private _moduleRef: NgModuleRef<{}>; + + // HTML Elements + private _body: HTMLElement; + private _tabBar: HTMLElement; + private _tabs: HTMLElement[]; + private _tabContent: HTMLElement[]; + + constructor( + private _dialog: Dialog, + private _bootstrapService: IBootstrapService + ) { + super(); + this._tabs = []; + this._tabContent = []; + } + + 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; + this.initializeModelViewContainer(this._body, modelViewId); + } else { + this._tabbedPanel = new TabbedPanel(this._body); + this._dialog.content.forEach((tab, tabIndex) => { + this._tabbedPanel.pushTab({ + title: tab.title, + identifier: 'dialogPane.' + this._dialog.title + '.' + tabIndex, + view: { + render: (container) => { + this.initializeModelViewContainer(container, tab.content); + }, + layout: (dimension) => { } + } as IPanelView + } as IPanelTab); + }); + } + }); + + this._activeTabIndex = 0; + return this._body; + } + + /** + * Bootstrap angular for the dialog's model view controller with the given model view ID + */ + private initializeModelViewContainer(bodyContainer: HTMLElement, modelViewId: string) { + this._bootstrapService.bootstrap( + DialogModule, + bodyContainer, + 'dialog-modelview-container', + { modelViewId: modelViewId } as DialogComponentParams, + undefined, + (moduleRef) => this._moduleRef = moduleRef); + } + + public show(): void { + this._body.classList.remove('dialogModal-hidden'); + } + + public hide(): void { + this._body.classList.add('dialogModal-hidden'); + } + + /** + * Called by the theme registry on theme change to style the component + */ + public style(styles: IModalDialogStyles): void { + this._body.style.backgroundColor = styles.dialogBodyBackground ? styles.dialogBodyBackground.toString() : undefined; + this._body.style.color = styles.dialogForeground ? styles.dialogForeground.toString() : undefined; + } +} diff --git a/src/sql/platform/dialog/dialogTypes.ts b/src/sql/platform/dialog/dialogTypes.ts new file mode 100644 index 0000000000..d58e229b0a --- /dev/null +++ b/src/sql/platform/dialog/dialogTypes.ts @@ -0,0 +1,55 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as sqlops from 'sqlops'; +import Event, { Emitter } from 'vs/base/common/event'; + +export class DialogTab implements sqlops.window.modelviewdialog.DialogTab { + public content: string; + + constructor(public title: string, content?: string) { + if (content) { + this.content = content; + } + } + + public updateContent(): void { } +} + +export class Dialog implements sqlops.window.modelviewdialog.Dialog { + public content: string | DialogTab[]; + public okTitle: string; + public cancelTitle: string; + public customButtons: DialogButton[]; + + private _onOk: Emitter = new Emitter(); + public readonly onOk: Event = this._onOk.event; + private _onCancel: Emitter = new Emitter(); + public readonly onCancel: Event = this._onCancel.event; + + constructor(public title: string, content?: string | DialogTab[]) { + if (content) { + this.content = content; + } + } + + public open(): void { } + public close(): void { } + public updateContent(): void { } +} + +export class DialogButton implements sqlops.window.modelviewdialog.Button { + public label: string; + public enabled: boolean; + private _onClick: Emitter = new Emitter(); + public readonly onClick: Event = this._onClick.event; + + constructor(label: string, enabled: boolean) { + this.label = label; + this.enabled = enabled; + } +} \ No newline at end of file diff --git a/src/sql/platform/dialog/media/dialogModal.css b/src/sql/platform/dialog/media/dialogModal.css new file mode 100644 index 0000000000..3fc5b41fed --- /dev/null +++ b/src/sql/platform/dialog/media/dialogModal.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.dialogModal-body { + display: flex; + flex-direction: row; + height: 100%; + width: 100%; + min-width: 500px; + min-height: 600px; +} + +.modal.wide .dialogModal-body { + min-width: 800px; +} + +.dialogModal-pane { + display: flex; + flex-direction: column; + width: 100%; + height: 100%; +} + +.dialogModal-pane.dialogModal-hidden { + display: none; +} diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 7fabd19f78..74826879ed 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -203,4 +203,116 @@ declare module 'sqlops' { */ export function registerModelViewProvider(widgetId: string, handler: (view: ModelView) => void): void; } + + export namespace window { + export namespace modelviewdialog { + /** + * Create a dialog with the given title + * @param title The title of the dialog, displayed at the top + */ + export function createDialog(title: string): Dialog; + + /** + * Create a dialog tab which can be included as part of the content of a dialog + * @param title The title of the page, displayed on the tab to select the page + */ + export function createTab(title: string): DialogTab; + + /** + * Create a button which can be included in a dialog + * @param label The label of the button + */ + export function createButton(label: string): Button; + + // Model view dialog classes + export interface Dialog { + /** + * The title of the dialog + */ + title: string, + + /** + * The content of the dialog. If multiple tabs are given they will be displayed with tabs + * If a string is given, it should be the ID of the dialog's model view content + * TODO mairvine 4/18/18: use a model view content type + */ + content: string | DialogTab[], + + /** + * The caption of the OK button + */ + okTitle: string; + + /** + * The caption of the Cancel button + */ + cancelTitle: string; + + /** + * Any additional buttons that should be displayed + */ + customButtons: Button[]; + + /** + * Opens the dialog + */ + open(): void; + + /** + * Closes the dialog + */ + close(): void; + + /** + * Updates the dialog on screen to reflect changes to the buttons or content + */ + updateContent(): void; + + /** + * Raised when dialog's ok button is pressed + */ + readonly onOk: vscode.Event; + + /** + * Raised when dialog is canceled + */ + readonly onCancel: vscode.Event; + } + + export interface DialogTab { + /** + * The title of the tab + */ + title: string, + + /** + * A string giving the ID of the tab's model view content + * TODO mairvine 4/18/18: use a model view content type + */ + content: string; + + /** + * Updates the dialog on screen to reflect changes to the content + */ + updateContent(): void; + } + + export interface Button { + /** + * The label displayed on the button + */ + label: string, + + /** + * Whether the button is enabled + */ + enabled: boolean, + + /** + * Raised when the button is clicked + */ + readonly onClick: vscode.Event; + } + } + } } diff --git a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 3d0ee5a363..63f7a14196 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -280,10 +280,19 @@ export function createApiFactory( } }; + const modelViewDialog: typeof sqlops.window.modelviewdialog = { + // TODO mairvine 4/18/18: Implement the extension layer for custom dialogs + createDialog(title: string): sqlops.window.modelviewdialog.Dialog { return undefined; }, + createTab(title: string): sqlops.window.modelviewdialog.DialogTab { return undefined; }, + createButton(label: string): sqlops.window.modelviewdialog.Button { return undefined; } + }; + const window: typeof sqlops.window = { createDialog(name: string) { return extHostModalDialogs.createDialog(name); - } + }, + + modelviewdialog: modelViewDialog }; const tasks: typeof sqlops.tasks = { diff --git a/src/sqltest/platform/dialog/dialogPane.test.ts b/src/sqltest/platform/dialog/dialogPane.test.ts new file mode 100644 index 0000000000..12ac7a868b --- /dev/null +++ b/src/sqltest/platform/dialog/dialogPane.test.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + import { Dialog, DialogTab } from 'sql/platform/dialog/dialogTypes'; +import { Mock, It, Times } from 'typemoq'; +import { IBootstrapService } from 'sql/services/bootstrap/bootstrapService'; +import { DialogPane } from 'sql/platform/dialog/dialogPane'; +import { DialogComponentParams } from 'sql/platform/dialog/dialogContainer.component'; + +'use strict'; + +suite('Dialog Pane Tests', () => { + let dialog: Dialog; + let mockBootstrapService: Mock; + let container: HTMLElement; + + setup(() => { + dialog = new Dialog('test_dialog'); + mockBootstrapService = Mock.ofInstance({ + bootstrap: () => undefined + } as any); + mockBootstrapService.setup(x => x.bootstrap(It.isAny(), It.isAny(), It.isAny(), It.isAny(), undefined, It.isAny())); + container = document.createElement('div'); + }); + + test('Creating a pane from content without tabs initializes the model view content correctly', () => { + // 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); + dialogPane.createBody(container); + + // Then a single dialog-modelview-container element is added directly to the dialog pane + mockBootstrapService.verify(x => x.bootstrap( + It.isAny(), + It.isAny(), + It.isAny(), + It.is((x: DialogComponentParams) => x.modelViewId === modelViewId), + undefined, + It.isAny()), Times.once()); + }); + + test('Creating a pane from content with a single tab initializes without showing tabs', () => { + // 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); + dialogPane.createBody(container); + + // Then a single dialog-modelview-container element is added directly to the dialog pane + mockBootstrapService.verify(x => x.bootstrap( + It.isAny(), + It.isAny(), + It.isAny(), + It.is((x: DialogComponentParams) => x.modelViewId === modelViewId), + undefined, + It.isAny()), Times.once()); + }); + + test('Creating a pane from content with multiple tabs initializes multiple model view tabs', () => { + // If I fill in a dialog's content with a single tab and then render the dialog + 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); + 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) + mockBootstrapService.verify(x => x.bootstrap( + It.isAny(), + It.isAny(), + It.isAny(), + It.is((x: DialogComponentParams) => x.modelViewId === modelViewId1), + undefined, + It.isAny()), Times.once()); + }); +}); \ No newline at end of file