diff --git a/src/sql/parts/modelComponents/componentBase.ts b/src/sql/parts/modelComponents/componentBase.ts index 805caa65f0..558019c63a 100644 --- a/src/sql/parts/modelComponents/componentBase.ts +++ b/src/sql/parts/modelComponents/componentBase.ts @@ -10,25 +10,29 @@ import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFacto import * as types from 'vs/base/common/types'; -import { IComponent, IComponentDescriptor, IModelStore } from 'sql/parts/modelComponents/interfaces'; +import { IComponent, IComponentDescriptor, IModelStore, IComponentEventArgs, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; import { FlexLayout, FlexItemLayout } from 'sqlops'; import { ComponentHostDirective } from 'sql/parts/dashboard/common/componentHost.directive'; import { DashboardServiceInterface } from 'sql/parts/dashboard/services/dashboardServiceInterface.service'; +import Event, { Emitter } from 'vs/base/common/event'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; export class ItemDescriptor { constructor(public descriptor: IComponentDescriptor, public config: T) {} } -export abstract class ComponentBase implements IComponent, OnDestroy, OnInit { +export abstract class ComponentBase extends Disposable implements IComponent, OnDestroy, OnInit { protected properties: { [key: string]: any; } = {}; - constructor( + constructor ( protected _changeRef: ChangeDetectorRef) { + super(); } /// IComponent implementation abstract descriptor: IComponentDescriptor; abstract modelStore: IModelStore; + protected _onEventEmitter = new Emitter(); public layout(): void { this._changeRef.detectChanges(); @@ -48,7 +52,9 @@ export abstract class ComponentBase implements IComponent, OnDestroy, OnInit { } } - abstract ngOnDestroy(): void; + ngOnDestroy(): void { + this.dispose(); + } abstract setLayout (layout: any): void; @@ -68,6 +74,18 @@ export abstract class ComponentBase implements IComponent, OnDestroy, OnInit { let property = propertyGetter(this.getProperties()); return types.isUndefinedOrNull(property) ? defaultVal : property; } + + protected setProperty(propertySetter: (TPropertyBag, TValue) => void, value: TValue) { + propertySetter(this.getProperties(), value); + this._onEventEmitter.fire({ + eventType: ComponentEventType.PropertiesChanged, + args: this.getProperties() + }); + } + + public get onEvent(): Event { + return this._onEventEmitter.event; + } } export abstract class ContainerBase extends ComponentBase { diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index 67d67d73de..25485ff4fb 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -5,6 +5,7 @@ import FlexContainer from './flexContainer.component'; import CardComponent from './card.component'; +import InputBoxComponent from './inputbox.component'; import { registerComponentType } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { ModelComponentTypes } from 'sql/workbench/api/common/sqlExtHostTypes'; @@ -13,3 +14,6 @@ registerComponentType(FLEX_CONTAINER, ModelComponentTypes.FlexContainer, FlexCon export const CARD_COMPONENT = 'card-component'; registerComponentType(CARD_COMPONENT, ModelComponentTypes.Card, CardComponent); + +export const INPUTBOX_COMPONENT = 'inputbox-component'; +registerComponentType(INPUTBOX_COMPONENT, ModelComponentTypes.InputBox, InputBoxComponent); diff --git a/src/sql/parts/modelComponents/inputbox.component.ts b/src/sql/parts/modelComponents/inputbox.component.ts new file mode 100644 index 0000000000..af8ec53fe2 --- /dev/null +++ b/src/sql/parts/modelComponents/inputbox.component.ts @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Component, Input, Inject, ChangeDetectorRef, forwardRef, ComponentFactoryResolver, + ViewChild, ViewChildren, ElementRef, Injector, OnDestroy, QueryList, AfterViewInit +} from '@angular/core'; + +import * as sqlops from 'sqlops'; +import Event, { Emitter } from 'vs/base/common/event'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { InputBox, IInputOptions } from 'vs/base/browser/ui/inputbox/inputBox'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { attachInputBoxStyler, attachListStyler } from 'vs/platform/theme/common/styler'; + +@Component({ + selector: 'inputBox', + template: ` +
+ ` +}) +export default class InputBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + private _input: InputBox; + + @ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef; + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef) { + super(changeRef); + } + + ngOnInit(): void { + this.baseInit(); + + } + + ngAfterViewInit(): void { + if (this._inputContainer) { + let inputOptions: IInputOptions = { + placeholder: '', + ariaLabel: '' + }; + + this._input = new InputBox(this._inputContainer.nativeElement, this._commonService.contextViewService, inputOptions); + + this._register(this._input); + this._register(attachInputBoxStyler(this._input, this._commonService.themeService)); + this._register(this._input.onDidChange(e => { + this.value = this._input.value; + this._onEventEmitter.fire({ + eventType: ComponentEventType.onDidChange, + args: e + }); + })); + } + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + /// IComponent implementation + + public layout(): void { + this._changeRef.detectChanges(); + } + + public setLayout (layout: any): void { + // TODO allow configuring the look and feel + this.layout(); + } + + public setProperties(properties: { [key: string]: any; }): void { + super.setProperties(properties); + this._input.value = this.value; + } + + // CSS-bound properties + + public get value(): string { + return this.getPropertyOrDefault((props) => props.value, ''); + } + + public set value(newValue: string) { + this.setProperty(this.setInputBoxProperties, newValue); + } + + private setInputBoxProperties(properties: sqlops.InputBoxProperties, value: string): void { + properties.value = value; + } +} diff --git a/src/sql/parts/modelComponents/interfaces.ts b/src/sql/parts/modelComponents/interfaces.ts index f360d0a1d1..b455d6d63e 100644 --- a/src/sql/parts/modelComponents/interfaces.ts +++ b/src/sql/parts/modelComponents/interfaces.ts @@ -5,6 +5,7 @@ import { InjectionToken } from '@angular/core'; import * as sqlops from 'sqlops'; +import Event, { Emitter } from 'vs/base/common/event'; /** * An instance of a model-backed component. This will be a UI element @@ -20,6 +21,7 @@ export interface IComponent { addToContainer?: (componentDescriptor: IComponentDescriptor, config: any) => void; setLayout?: (layout: any) => void; setProperties?: (properties: { [key: string]: any; }) => void; + onEvent?: Event; } export const COMPONENT_CONFIG = new InjectionToken('component_config'); @@ -48,6 +50,16 @@ export interface IComponentDescriptor { id: string; } +export interface IComponentEventArgs { + eventType: ComponentEventType; + args: any; +} + +export enum ComponentEventType { + PropertiesChanged, + onDidChange +} + export interface IModelStore { /** * Creates and saves the reference of a component descriptor. diff --git a/src/sql/parts/modelComponents/viewBase.ts b/src/sql/parts/modelComponents/viewBase.ts index fa05ab1de8..ab203ff81e 100644 --- a/src/sql/parts/modelComponents/viewBase.ts +++ b/src/sql/parts/modelComponents/viewBase.ts @@ -16,6 +16,7 @@ import { IModelView } from 'sql/services/model/modelViewService'; import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; import { AngularDisposable } from 'sql/base/common/lifecycle'; import { ModelStore } from 'sql/parts/modelComponents/modelStore'; +import Event, { Emitter } from 'vs/base/common/event'; const componentRegistry = Registry.as(Extensions.ComponentContribution); @@ -35,6 +36,8 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { abstract id: string; abstract connection: sqlops.connection.Connection; abstract serverInfo: sqlops.ServerInfo; + private _onEventEmitter = new Emitter(); + initializeModel(rootComponent: IComponentShape): void { let descriptor = this.defineComponent(rootComponent); @@ -52,11 +55,13 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { let descriptor = this.modelStore.createComponentDescriptor(typeId, component.id); this.setProperties(component.id, component.properties); this.setLayout(component.id, component.layout); + this.registerEvent(component.id); if (component.itemConfigs) { for(let item of component.itemConfigs) { this.addToContainer(component.id, item); } } + return descriptor; } @@ -91,4 +96,18 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { // TODO add error handling }); } + + registerEvent(componentId: string) { + this.queueAction(componentId, (component) => { + if (component.onEvent) { + this._register(component.onEvent(e => { + this._onEventEmitter.fire(e); + })); + } + }); + } + + public get onEvent(): Event { + return this._onEventEmitter.event; + } } \ No newline at end of file diff --git a/src/sql/services/model/modelViewService.ts b/src/sql/services/model/modelViewService.ts index 10a575c3ab..1361e5350c 100644 --- a/src/sql/services/model/modelViewService.ts +++ b/src/sql/services/model/modelViewService.ts @@ -6,6 +6,7 @@ 'use strict'; import * as sqlops from 'sqlops'; import { IItemConfig, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; +import Event, { Emitter } from 'vs/base/common/event'; export interface IView { readonly id: string; @@ -19,4 +20,6 @@ export interface IModelView extends IView { addToContainer(containerId: string, item: IItemConfig): void; setLayout(componentId: string, layout: any): void; setProperties(componentId: string, properties: { [key: string]: any }): void; + registerEvent(componentId: string); + onEvent: Event; } diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index 74826879ed..f3b5aaa5c5 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -19,6 +19,7 @@ declare module 'sqlops' { navContainer(): ContainerBuilder; flexContainer(): FlexBuilder; card(): ComponentBuilder; + inputBox(): ComponentBuilder; dashboardWidget(widgetId: string): ComponentBuilder; dashboardWebview(webviewId: string): ComponentBuilder; } @@ -142,7 +143,7 @@ declare module 'sqlops' { /** * Properties representing the card component, can be used - * when using ModelBuilder to create the comopnent + * when using ModelBuilder to create the component */ export interface CardProperties { label: string; @@ -150,12 +151,21 @@ declare module 'sqlops' { actions?: ActionDescriptor[]; } + export interface InputBoxProperties { + value?: string; + } + export interface CardComponent extends Component { label: string; value: string; actions?: ActionDescriptor[]; } + export interface InputBoxComponent extends Component { + value: string; + onTextChanged: vscode.Event; + } + export interface WidgetComponent extends Component { widgetId: string; } diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 6023b3876d..6167463fc8 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -67,6 +67,7 @@ export enum ModelComponentTypes { NavContainer, FlexContainer, Card, + InputBox, DashboardWidget, DashboardWebview } @@ -83,3 +84,13 @@ export interface IItemConfig { componentShape: IComponentShape; config: any; } + +export enum ComponentEventType { + PropertiesChanged, + onDidChange +} + +export interface IComponentEventArgs { + eventType: ComponentEventType; + args: any; +} diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 64d68aba71..c0458bd97f 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -13,10 +13,11 @@ import * as vscode from 'vscode'; import * as sqlops from 'sqlops'; import { SqlMainContext, ExtHostModelViewShape, MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; -import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, ComponentEventType } from 'sql/workbench/api/common/sqlExtHostTypes'; class ModelBuilderImpl implements sqlops.ModelBuilder { private nextComponentId: number; + private readonly _eventHandlers = new Map(); constructor(private readonly _proxy: MainThreadModelViewShape, private readonly _handle: number) { this.nextComponentId = 0; @@ -34,17 +35,35 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { card(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); - return new ComponentBuilderImpl(new CardWrapper(this._proxy, this._handle, id)); + return this.withEventHandler(new CardWrapper(this._proxy, this._handle, id), id); + } + + inputBox(): sqlops.ComponentBuilder { + let id = this.getNextComponentId(); + return this.withEventHandler(new InputBoxWrapper(this._proxy, this._handle, id), id); } dashboardWidget(widgetId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); - return new ComponentBuilderImpl(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id)); + return this.withEventHandler(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWidget, id), id); } dashboardWebview(webviewId: string): sqlops.ComponentBuilder { let id = this.getNextComponentId(); - return new ComponentBuilderImpl(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWebview, id)); + return this.withEventHandler(new ComponentWrapper(this._proxy, this._handle, ModelComponentTypes.DashboardWebview, id), id); + } + + withEventHandler(component: ComponentWrapper, id: string): sqlops.ComponentBuilder { + let componentBuilder: ComponentBuilderImpl = new ComponentBuilderImpl(component); + this._eventHandlers.set(id, componentBuilder); + return componentBuilder; + } + + handleEvent(componentId: string, eventArgs: IComponentEventArgs): void { + let eventHandler = this._eventHandlers.get(componentId); + if (eventHandler) { + eventHandler.handleEvent(eventArgs); + } } private getNextComponentId(): string { @@ -52,9 +71,14 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { } } -class ComponentBuilderImpl implements sqlops.ComponentBuilder { +interface IWithEventHandler { + handleEvent(eventArgs: IComponentEventArgs): void; +} + +class ComponentBuilderImpl implements sqlops.ComponentBuilder, IWithEventHandler { constructor(protected _component: ComponentWrapper) { + _component.registerEvent(); } component(): T { @@ -65,6 +89,10 @@ class ComponentBuilderImpl implements sqlops.Compone this._component.properties = properties; return this; } + + handleEvent(eventArgs: IComponentEventArgs) { + this._component.onEvent(eventArgs); + } } class GenericComponentBuilder extends ComponentBuilderImpl { @@ -150,7 +178,6 @@ class ComponentWrapper implements sqlops.Component { }; } - public clearItems(): Thenable { this.itemConfigs = []; return this._proxy.$clearContainer(this._handle, this.id); @@ -184,6 +211,16 @@ class ComponentWrapper implements sqlops.Component { return this._proxy.$setProperties(this._handle, this._id, this.properties).then(() => true); } + public registerEvent(): Thenable { + return this._proxy.$registerEvent(this._handle, this._id).then(() => true); + } + + public onEvent(eventArgs: IComponentEventArgs) { + if (eventArgs && eventArgs.eventType === ComponentEventType.PropertiesChanged) { + this.properties = eventArgs.args; + } + } + protected setProperty(key: string, value: any): Thenable { if (!this.properties[key] || this.properties[key] !== value) { // Only notify the front end if a value has been updated @@ -233,11 +270,45 @@ class CardWrapper extends ComponentWrapper implements sqlops.CardComponent { } } +class InputBoxWrapper extends ComponentWrapper implements sqlops.InputBoxComponent { + + constructor(proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.InputBox, id); + this.properties = {}; + this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); + } + + private _onTextChangedEmitter = new Emitter(); + private _emitterMap = new Map>(); + + public get value(): string { + return this.properties['value']; + } + public set value(v: string) { + this.setProperty('value', v); + } + + public get onTextChanged(): vscode.Event { + let emitter = this._emitterMap.get(ComponentEventType.onDidChange); + return emitter && emitter.event; + } + + public onEvent(eventArgs: IComponentEventArgs) { + super.onEvent(eventArgs); + if (eventArgs) { + let emitter = this._emitterMap.get(eventArgs.eventType); + if (emitter) { + emitter.fire(); + } + } + } +} + class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter(); - private _modelBuilder: sqlops.ModelBuilder; + private _modelBuilder: ModelBuilderImpl; constructor( private readonly _proxy: MainThreadModelViewShape, @@ -264,6 +335,10 @@ class ModelViewImpl implements sqlops.ModelView { return this._modelBuilder; } + public handleEvent(componentId: string, eventArgs: IComponentEventArgs): void { + this._modelBuilder.handleEvent(componentId, eventArgs); + } + public initializeModel(component: T): Thenable { let componentImpl = component as ComponentWrapper; if (!componentImpl) { @@ -301,4 +376,11 @@ export class ExtHostModelView implements ExtHostModelViewShape { this._modelViews.set(handle, view); this._handlers.get(id)(view); } + + $handleEvent(handle: number, componentId: string, eventArgs: IComponentEventArgs): void { + const view = this._modelViews.get(handle); + if (view) { + view.handleEvent(componentId, eventArgs); + } + } } diff --git a/src/sql/workbench/api/node/mainThreadModelView.ts b/src/sql/workbench/api/node/mainThreadModelView.ts index d66c5ed6ae..4c30fe6f5e 100644 --- a/src/sql/workbench/api/node/mainThreadModelView.ts +++ b/src/sql/workbench/api/node/mainThreadModelView.ts @@ -13,10 +13,11 @@ import * as sqlops from 'sqlops'; import { IModelViewService } from 'sql/services/modelComponents/modelViewService'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IModelView } from 'sql/services/model/modelViewService'; +import { Disposable } from 'vs/base/common/lifecycle'; @extHostNamedCustomer(SqlMainContext.MainThreadModelView) -export class MainThreadModelView implements MainThreadModelViewShape { +export class MainThreadModelView extends Disposable implements MainThreadModelViewShape { private static _handlePool = 0; private readonly _proxy: ExtHostModelViewShape; @@ -28,6 +29,7 @@ export class MainThreadModelView implements MainThreadModelViewShape { context: IExtHostContext, @IModelViewService viewService: IModelViewService ) { + super(); this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelView); viewService.onRegisteredModelView(view => { if (this.knownWidgets.includes(view.id)) { @@ -38,16 +40,14 @@ export class MainThreadModelView implements MainThreadModelViewShape { }); } - public dispose(): void { - throw new Error('Method not implemented.'); - } - $registerProvider(id: string) { this.knownWidgets.push(id); } $initializeModel(handle: number, rootComponent: IComponentShape): Thenable { - return this.execModelViewAction(handle, (modelView) => modelView.initializeModel(rootComponent)); + return this.execModelViewAction(handle, (modelView) => { + modelView.initializeModel(rootComponent); + }); } $clearContainer(handle: number, componentId: string): Thenable { @@ -63,6 +63,19 @@ export class MainThreadModelView implements MainThreadModelViewShape { return this.execModelViewAction(handle, (modelView) => modelView.setLayout(componentId, layout)); } + private onEvent(handle: number, componentId: string, eventArgs: any) { + this._proxy.$handleEvent(handle, componentId, eventArgs); + } + + $registerEvent(handle: number, componentId: string): Thenable { + let properties: { [key: string]: any; } = { eventName: this.onEvent }; + return this.execModelViewAction(handle, (modelView) => { + this._register(modelView.onEvent (e => { + this.onEvent(handle, componentId, e); + })); + }); + } + $setProperties(handle: number, componentId: string, properties: { [key: string]: any; }): Thenable { return this.execModelViewAction(handle, (modelView) => modelView.setProperties(componentId, properties)); } diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index c183e56c9e..a01d07c2f0 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -17,6 +17,7 @@ import * as vscode from 'vscode'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; +import Event, { Emitter } from 'vs/base/common/event'; export abstract class ExtHostAccountManagementShape { $autoOAuthCancelled(handle: number): Thenable { throw ni(); } @@ -514,6 +515,7 @@ export interface ExtHostModelViewShape { $registerProvider(widgetId: string, handler: (webview: sqlops.ModelView) => void): void; $onClosed(handle: number): void; $registerWidget(handle: number, id: string, connection: sqlops.connection.Connection, serverInfo: sqlops.ServerInfo): void; + $handleEvent(handle: number, id: string, eventArgs: any); } export interface MainThreadModelViewShape extends IDisposable { @@ -523,6 +525,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; } export interface ExtHostObjectExplorerShape {