diff --git a/samples/sqlservices/src/controllers/mainController.ts b/samples/sqlservices/src/controllers/mainController.ts index c41cf58c01..1460d0abf2 100644 --- a/samples/sqlservices/src/controllers/mainController.ts +++ b/samples/sqlservices/src/controllers/mainController.ts @@ -11,6 +11,7 @@ import * as vscode from 'vscode'; import SplitPropertiesPanel from './splitPropertiesPanel'; import * as fs from 'fs'; import * as path from 'path'; +import {TreeNode, TreeDataProvider} from './treeDataProvider'; /** * The main controller class that initializes the extension @@ -67,6 +68,67 @@ export default class MainController implements vscode.Disposable { return Promise.resolve(true); } + private async getTab3Content(view: sqlops.ModelView): Promise { + let treeData = { + label: '1', + children: [ + { + label: '11', + id: '11', + children: [ + { + label: '111', + id: '111', + checked: false + }, + { + label: '112', + id: '112', + children: [ + { + label: '1121', + id: '1121', + checked: true + }, + { + label: '1122', + id: '1122', + checked: false + } + ] + } + ] + }, + { + label: '12', + id: '12', + checked: true + } + ], + id: '1' + }; + let root = TreeNode.createTree(treeData); + + let treeDataProvider = new TreeDataProvider(root); + + let tree: sqlops.TreeComponent = view.modelBuilder.tree().withProperties({ + 'withCheckbox': true + }).component(); + tree.registerDataProvider(treeDataProvider); + let formModel = view.modelBuilder.formContainer() + .withFormItems([{ + component: tree, + title: 'Tree' + }], { + horizontal: false, + componentWidth: 800, + componentHeight: 800 + }).component(); + let formWrapper = view.modelBuilder.loadingComponent().withItem(formModel).component(); + formWrapper.loading = false; + + await view.initializeModel(formWrapper); + } private async getTabContent(view: sqlops.ModelView, customButton1: sqlops.window.modelviewdialog.Button, customButton2: sqlops.window.modelviewdialog.Button, componentWidth: number | string): Promise { let inputBox = view.modelBuilder.inputBox() .withProperties({ @@ -271,8 +333,9 @@ export default class MainController implements vscode.Disposable { let tab1 = sqlops.window.modelviewdialog.createTab('Test tab 1'); let tab2 = sqlops.window.modelviewdialog.createTab('Test tab 2'); + let tab3 = sqlops.window.modelviewdialog.createTab('Test tab 3'); tab2.content = 'sqlservices'; - dialog.content = [tab1, tab2]; + dialog.content = [tab1, tab2, tab3]; dialog.okButton.onClick(() => console.log('ok clicked!')); dialog.cancelButton.onClick(() => console.log('cancel clicked!')); dialog.okButton.label = 'ok'; @@ -285,6 +348,10 @@ export default class MainController implements vscode.Disposable { tab1.registerContent(async (view) => { await this.getTabContent(view, customButton1, customButton2, 400); }); + + tab3.registerContent(async (view) => { + await this.getTab3Content(view); + }); sqlops.window.modelviewdialog.openDialog(dialog); } diff --git a/samples/sqlservices/src/controllers/treeDataProvider.ts b/samples/sqlservices/src/controllers/treeDataProvider.ts new file mode 100644 index 0000000000..cf84d4608d --- /dev/null +++ b/samples/sqlservices/src/controllers/treeDataProvider.ts @@ -0,0 +1,299 @@ +/*--------------------------------------------------------------------------------------------- + * 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 vscode from 'vscode'; +import * as sqlops from 'sqlops'; +import * as path from 'path'; + +export enum TreeCheckboxState { + Intermediate = 0, + Checked = 1, + Unchecked = 2 +} + +export interface TreeComponentDataModel { + label?: string; + children?: TreeComponentDataModel[]; + id?: string; + checked?: boolean; +} + +export class TreeNode implements sqlops.TreeComponentItem { + private _onNodeChange = new vscode.EventEmitter(); + private _onTreeChange = new vscode.EventEmitter(); + private _data: TreeComponentDataModel; + private _parent?: TreeNode; + private _root: TreeNode; + private _isAlwaysLeaf: boolean; + private _nodeMap: Map; + private _children: TreeNode[]; + + public readonly onNodeChange: vscode.Event = this._onNodeChange.event; + public readonly onTreeChange: vscode.Event = this._onTreeChange.event; + + + /** + * Creates new instance of tree node + * @param data the underlining data that's bind to the tree node, any change in the tree will affect the same node in data + * @param root the root node of the tree. If passed null, the current node will be the root + */ + constructor(data: TreeComponentDataModel, root: TreeNode) { + if (!data) { + throw new Error(`Invalid tree node data`); + } + if (root === undefined) { + root = this; + root._nodeMap = new Map(); + } + + this._root = root; + if (this.findNode(data.id)) { + throw new Error(`tree node with id: '${data.id}' already exists`); + } + this._data = data; + } + + /** + * id for TreeNode + */ + public get id(): string { + return this.data.id; + } + + public set id(value: string) { + this.data.id = value; + } + + /** + * Label to display to the user, describing this node + */ + public set label(value: string) { + this.data.label = value; + } + + public get label(): string { + return this.data.label; + } + + /** + * Is this a leaf node (in which case no children can be generated) or is it expandable? + */ + public get isAlwaysLeaf(): boolean { + return this._isAlwaysLeaf; + } + + /** + * Parent of this node + */ + public get parent(): TreeNode { + return this._parent; + } + + public get root(): TreeNode { + return this._root; + } + + /** + * Path identifying this node + */ + public get nodePath(): string { + return `${this.parent ? this.parent.nodePath + '-' : ''}${this.id}`; + } + + public get data(): TreeComponentDataModel { + if (this._data === undefined) { + this._data = { + label: undefined + }; + } + return this._data; + } + + public changeNodeCheckedState(value: boolean, fromParent?: boolean): void { + if (value !== this.checked) { + if (value !== undefined && this.children) { + this.children.forEach(child => { + child.changeNodeCheckedState(value, true); + }); + } + + this.checked = value; + if (!fromParent && this.parent) { + this.parent.refreshState(); + } + + this.onValueChanged(); + } + } + + public set checked(value: boolean) { + this.data.checked = value; + } + + public refreshState(): void { + if (this.hasChildren) { + if (this.children.every(c => c.checked)) { + this.changeNodeCheckedState(true); + } else if (this.children.every(c => c.checked !== undefined && !c.checked)) { + this.changeNodeCheckedState(false); + } else { + this.changeNodeCheckedState(undefined); + } + } + } + + public get hasChildren(): boolean { + return this.children !== undefined && this.children.length > 0; + } + + public get checked(): boolean { + return this.data.checked; + } + + private onValueChanged(): void { + this._onNodeChange.fire(); + if (this.root) { + this.root._onTreeChange.fire(this); + } + } + + public get checkboxState(): TreeCheckboxState { + if (this.checked === undefined) { + return TreeCheckboxState.Intermediate; + } else { + return this.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked; + } + } + + public findNode(id: string): TreeNode { + if (this.id === id) { + return this; + } else if (this.root) { + return this.root._nodeMap.has(id) ? this.root._nodeMap.get(id) : undefined; + } else { + let node: TreeNode; + if (this.children) { + this.children.forEach(child => { + node = child.findNode(id); + if (node) { + return; + } + }); + } + + return node; + } + } + + /** + * Children of this node + */ + public get children(): TreeNode[] { + return this._children; + } + + public addChildNode(node: TreeNode): void { + + if (node) { + if (!node.root) { + node._root = this.root; + } + if (!node.parent) { + node._parent = this; + } + if (node.root) { + node.root._nodeMap.set(node.id, node); + } + this._children.push(node); + } + } + + public static createNode(nodeData: TreeComponentDataModel, parent?: TreeNode, root?: TreeNode): TreeNode { + let rootNode = root || (parent !== undefined ? parent.root : undefined); + let treeNode = new TreeNode(nodeData, rootNode); + + treeNode._parent = parent; + return treeNode; + } + + public static createTree(nodeData: TreeComponentDataModel, parent?: TreeNode, root?: TreeNode): TreeNode { + if (nodeData) { + let treeNode = TreeNode.createNode(nodeData, parent, root); + + if (nodeData.children && nodeData.children.length > 0) { + treeNode._isAlwaysLeaf = false; + treeNode._children = []; + nodeData.children.forEach(childNode => { + if (childNode) { + let childTreeNode = TreeNode.createTree(childNode, treeNode, root || treeNode.root); + treeNode.addChildNode(childTreeNode); + } + }); + treeNode.refreshState(); + } else { + treeNode._isAlwaysLeaf = true; + } + return treeNode; + } else { + return undefined; + } + } +} + +export class TreeDataProvider implements sqlops.TreeComponentDataProvider { + private _onDidChangeTreeData = new vscode.EventEmitter(); + constructor(private _root: TreeNode) { + if(this._root) { + this._root.onTreeChange(node => { + this._onDidChangeTreeData.fire(node); + }); + } + } + onDidChangeTreeData?: vscode.Event = this._onDidChangeTreeData.event ; + + /** + * Get [TreeItem](#TreeItem) representation of the `element` + * + * @param element The element for which [TreeItem](#TreeItem) representation is asked for. + * @return [TreeItem](#TreeItem) representation of the element + */ + getTreeItem(element: TreeNode): sqlops.TreeComponentItem | Thenable { + let item: sqlops.TreeComponentItem = {}; + item.label = element.label; + item.checked = element.checked; + item.iconPath = vscode.Uri.file(path.join(__dirname, '..', 'media', 'monitor.svg')); + return item; + } + + /** + * Get the children of `element` or root if no element is passed. + * + * @param element The element from which the provider gets children. Can be `undefined`. + * @return Children of `element` or root if no element is passed. + */ + getChildren(element?: TreeNode): vscode.ProviderResult { + if (element) { + return Promise.resolve(element.children); + } else { + return Promise.resolve(this._root.children); + } + } + + getParent(element?: TreeNode): vscode.ProviderResult { + if (element) { + return Promise.resolve(element.parent); + } else { + return Promise.resolve(this._root); + } + } + + onNodeCheckedChanged(element: TreeNode, checked: boolean): void { + if (element) { + element.changeNodeCheckedState(checked); + } + } +} diff --git a/src/sql/parts/dashboard/dashboard.module.ts b/src/sql/parts/dashboard/dashboard.module.ts index f2511e4d5d..91ae290b5b 100644 --- a/src/sql/parts/dashboard/dashboard.module.ts +++ b/src/sql/parts/dashboard/dashboard.module.ts @@ -57,7 +57,7 @@ import { OperatorsViewComponent } from 'sql/parts/jobManagement/views/operatorsV import { ProxiesViewComponent } from 'sql/parts/jobManagement/views/proxiesView.component'; import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component'; -import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editabledropdown.component'; +import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component'; let baseComponents = [DashboardHomeContainer, DashboardComponent, DashboardWidgetWrapper, DashboardWebviewContainer, diff --git a/src/sql/parts/modelComponents/componentBase.ts b/src/sql/parts/modelComponents/componentBase.ts index c99fa4ecfc..15b0fb2bcd 100644 --- a/src/sql/parts/modelComponents/componentBase.ts +++ b/src/sql/parts/modelComponents/componentBase.ts @@ -71,6 +71,12 @@ export abstract class ComponentBase extends Disposable implements IComponent, On abstract setLayout(layout: any): void; + public setDataProvider(handle: number, componentId: string, context: any): void { + } + + public refreshDataProvider(item: any): void { + } + public setProperties(properties: { [key: string]: any; }): void { if (!properties) { this.properties = {}; diff --git a/src/sql/parts/modelComponents/components.contribution.ts b/src/sql/parts/modelComponents/components.contribution.ts index a8d26c86a4..3851bb5563 100644 --- a/src/sql/parts/modelComponents/components.contribution.ts +++ b/src/sql/parts/modelComponents/components.contribution.ts @@ -14,6 +14,7 @@ import DeclarativeTableComponent from './declarativeTable.component'; import ListBoxComponent from './listbox.component'; import ButtonComponent from './button.component'; import CheckBoxComponent from './checkbox.component'; +import TreeComponent from './tree/tree.component'; import RadioButtonComponent from './radioButton.component'; import WebViewComponent from './webview.component'; import TableComponent from './table.component'; @@ -73,6 +74,9 @@ registerComponentType(TABLE_COMPONENT, ModelComponentTypes.Table, TableComponent export const LOADING_COMPONENT = 'loading-component'; registerComponentType(LOADING_COMPONENT, ModelComponentTypes.LoadingComponent, LoadingComponent); +export const TREE_COMPONENT = 'tree-component'; +registerComponentType(TREE_COMPONENT, ModelComponentTypes.TreeComponent, TreeComponent); + export const FILEBROWSERTREE_COMPONENT = 'filebrowsertree-component'; registerComponentType(FILEBROWSERTREE_COMPONENT, ModelComponentTypes.FileBrowserTree, FileBrowserTreeComponent); diff --git a/src/sql/parts/modelComponents/declarativeTable.component.ts b/src/sql/parts/modelComponents/declarativeTable.component.ts index dc5df42f1d..8eb1c6f8d4 100644 --- a/src/sql/parts/modelComponents/declarativeTable.component.ts +++ b/src/sql/parts/modelComponents/declarativeTable.component.ts @@ -17,7 +17,7 @@ import { IContextViewService } from 'vs/platform/contextview/browser/contextView import { Event, Emitter } from 'vs/base/common/event'; import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component'; -import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editabledropdown.component'; +import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component'; import { ISelectData } from 'vs/base/browser/ui/selectBox/selectBox'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component'; import * as nls from 'vs/nls'; diff --git a/src/sql/parts/modelComponents/interfaces.ts b/src/sql/parts/modelComponents/interfaces.ts index aa34231f1e..fdbbc11ef1 100644 --- a/src/sql/parts/modelComponents/interfaces.ts +++ b/src/sql/parts/modelComponents/interfaces.ts @@ -26,6 +26,8 @@ export interface IComponent { enabled: boolean; readonly valid?: boolean; validate(): Thenable; + setDataProvider(handle: number, componentId: string, context: any): void; + refreshDataProvider(item: any): void; } export const COMPONENT_CONFIG = new InjectionToken('component_config'); diff --git a/src/sql/parts/modelComponents/tree/tree.component.ts b/src/sql/parts/modelComponents/tree/tree.component.ts new file mode 100644 index 0000000000..1ec6f786ac --- /dev/null +++ b/src/sql/parts/modelComponents/tree/tree.component.ts @@ -0,0 +1,148 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import 'vs/css!sql/parts/modelComponents/tree/treeComponent'; +import 'vs/css!sql/media/icons/common-icons'; + +import { + Component, Input, Inject, ChangeDetectorRef, forwardRef, + ViewChild, ElementRef, OnDestroy, AfterViewInit +} from '@angular/core'; + +import * as sqlops from 'sqlops'; + +import { ComponentBase } from 'sql/parts/modelComponents/componentBase'; +import { IComponent, IComponentDescriptor, IModelStore, ComponentEventType } from 'sql/parts/modelComponents/interfaces'; +import { Tree } from 'vs/base/parts/tree/browser/treeImpl'; +import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; +import { TreeComponentRenderer } from 'sql/parts/modelComponents/tree/treeComponentRenderer'; +import { TreeComponentDataSource } from 'sql/parts/modelComponents/tree/treeDataSource'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { attachListStyler } from 'vs/platform/theme/common/styler'; +import { DefaultFilter, DefaultAccessibilityProvider, DefaultController } from 'vs/base/parts/tree/browser/treeDefaults'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITreeComponentItem, IModelViewTreeViewDataProvider } from 'sql/workbench/common/views'; +import { TreeViewDataProvider } from './treeViewDataProvider'; + +class Root implements ITreeComponentItem { + label = 'root'; + handle = '0'; + parentHandle = null; + collapsibleState = 0; + children = void 0; + options = undefined; +} + +@Component({ + selector: 'modelview-tree', + template: ` +
+ ` +}) +export default class TreeComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit { + @Input() descriptor: IComponentDescriptor; + @Input() modelStore: IModelStore; + private _tree: Tree; + private _treeRenderer: TreeComponentRenderer; + private _dataProvider: TreeViewDataProvider; + + @ViewChild('input', { read: ElementRef }) private _inputContainer: ElementRef; + constructor( + @Inject(forwardRef(() => CommonServiceInterface)) private _commonService: CommonServiceInterface, + @Inject(forwardRef(() => ChangeDetectorRef)) changeRef: ChangeDetectorRef, + @Inject(IWorkbenchThemeService) private themeService: IWorkbenchThemeService, + @Inject(IContextViewService) private contextViewService: IContextViewService, + @Inject(IInstantiationService) private _instantiationService: IInstantiationService) { + super(changeRef); + } + + ngOnInit(): void { + this.baseInit(); + + } + + ngAfterViewInit(): void { + if (this._inputContainer) { + this.createTreeControl(); + } + } + + ngOnDestroy(): void { + this.baseDestroy(); + } + + public setDataProvider(handle: number, componentId: string, context: any): any { + this._dataProvider = new TreeViewDataProvider(handle, componentId, context); + this.createTreeControl(); + } + + public refreshDataProvider(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }): void { + if (this._dataProvider) { + this._dataProvider.refresh(itemsToRefreshByHandle); + } + if (this._tree) { + for (const item of Object.values(itemsToRefreshByHandle)) { + this._tree.refresh(item); + } + } + } + + private createTreeControl(): void { + if (!this._tree && this._dataProvider) { + const dataSource = this._instantiationService.createInstance(TreeComponentDataSource, this._dataProvider); + const renderer = this._instantiationService.createInstance(TreeComponentRenderer, this.themeService, { withCheckbox: this.withCheckbox }); + this._treeRenderer = renderer; + const controller = new DefaultController(); + const filter = new DefaultFilter(); + const sorter = undefined; + const dnd = undefined; + const accessibilityProvider = new DefaultAccessibilityProvider(); + + this._tree = new Tree(this._inputContainer.nativeElement, + { dataSource, renderer, controller, dnd, filter, sorter, accessibilityProvider }, + { + indentPixels: 10, + twistiePixels: 20, + ariaLabel: 'Tree Node' + }); + this._tree.setInput(new Root()); + this._tree.domFocus(); + this._register(this._tree); + this._register(attachListStyler(this._tree, this.themeService)); + this._tree.refresh(); + this.layout(); + } + } + + /// IComponent implementation + + public layout(): void { + this._changeRef.detectChanges(); + this.createTreeControl(); + if (this._tree) { + this._tree.layout(this.convertSizeToNumber(this.width), this.convertSizeToNumber(this.height)); + this._tree.refresh(); + } + } + + 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._treeRenderer.options.withCheckbox = this.withCheckbox; + } + + public get withCheckbox(): boolean { + return this.getPropertyOrDefault((props) => props.withCheckbox, false); + } + + public set withCheckbox(newValue: boolean) { + this.setPropertyFromUI((properties, value) => { properties.withCheckbox = value; }, newValue); + } +} diff --git a/src/sql/parts/modelComponents/tree/treeComponent.css b/src/sql/parts/modelComponents/tree/treeComponent.css new file mode 100644 index 0000000000..65b99ce345 --- /dev/null +++ b/src/sql/parts/modelComponents/tree/treeComponent.css @@ -0,0 +1,8 @@ +.tree-component-node-tile { + display: flex; +} + +.tree-component-node-tile .model-view-tree-node-item-icon{ + width: 15px; + height: 15px; +} diff --git a/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts new file mode 100644 index 0000000000..067e82435f --- /dev/null +++ b/src/sql/parts/modelComponents/tree/treeComponentRenderer.ts @@ -0,0 +1,168 @@ +/*--------------------------------------------------------------------------------------------- + * 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!sql/media/icons/common-icons'; +import * as dom from 'vs/base/browser/dom'; +import { localize } from 'vs/nls'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ITree, IRenderer } from 'vs/base/parts/tree/browser/tree'; +import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; +import { LIGHT } from 'vs/platform/theme/common/themeService'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { Event, Emitter } from 'vs/base/common/event'; +import { ITreeComponentItem } from 'sql/workbench/common/views'; +import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/workbenchThemeService'; + +export enum TreeCheckboxState { + Intermediate = 0, + Checked = 1, + Unchecked = 2 +} + +export class TreeDataTemplate extends Disposable { + root: HTMLElement; + label: HTMLSpanElement; + icon: HTMLElement; + private _checkbox: HTMLInputElement; + model: ITreeComponentItem; + private _onChange = new Emitter(); + + public readonly onChange: Event = this._onChange.event; + + public set checkbox(input: HTMLInputElement) { + this._checkbox = input; + this.handleOnChange(this._checkbox, () => { + this._onChange.fire(this._checkbox.checked); + if (this.model && this.model.onCheckedChanged) { + this.model.onCheckedChanged(this._checkbox.checked); + } + }); + } + + public get checkboxState(): TreeCheckboxState { + if (this._checkbox.indeterminate) { + return TreeCheckboxState.Intermediate; + } else { + return this.checkbox.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked; + } + } + + public set checkboxState(value: TreeCheckboxState) { + if (this.checkboxState !== value) { + switch (value) { + case TreeCheckboxState.Checked: + this._checkbox.indeterminate = false; + this._checkbox.checked = true; + break; + case TreeCheckboxState.Unchecked: + this._checkbox.indeterminate = false; + this._checkbox.checked = false; + break; + case TreeCheckboxState.Intermediate: + this._checkbox.indeterminate = true; + break; + default: + break; + } + } + } + + public get checkbox(): HTMLInputElement { + return this._checkbox; + } + + protected handleOnChange(domNode: HTMLElement, listener: (e: Event) => void): void { + this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener)); + } +} + +/** + * Renders the tree items. + * Uses the dom template to render connection groups and connections. + */ +export class TreeComponentRenderer extends Disposable implements IRenderer { + + public static DEFAULT_TEMPLATE = 'DEFAULT_TEMPLATE'; + public static DEFAULT_HEIGHT = 20; + + + constructor( + private themeService: IWorkbenchThemeService, + public options?: { withCheckbox: boolean } + ) { + super(); + } + + /** + * Returns the element's height in the tree, in pixels. + */ + public getHeight(tree: ITree, element: any): number { + return TreeComponentRenderer.DEFAULT_HEIGHT; + } + + /** + * Returns a template ID for a given element. + */ + public getTemplateId(tree: ITree, element: any): string { + + return TreeComponentRenderer.DEFAULT_TEMPLATE; + } + + /** + * Render template in a dom element based on template id + */ + public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { + + if (templateId === TreeComponentRenderer.DEFAULT_TEMPLATE) { + const nodeTemplate: TreeDataTemplate = new TreeDataTemplate(); + nodeTemplate.root = dom.append(container, dom.$('.tree-component-node-tile')); + nodeTemplate.icon = dom.append(nodeTemplate.root, dom.$('div.model-view-tree-node-item-icon')); + if (this.options && this.options.withCheckbox) { + let checkboxWrapper = dom.append(nodeTemplate.root, dom.$('div.checkboxWrapper')); + nodeTemplate.checkbox = dom.append(checkboxWrapper, dom.$('input.checkbox', { type: 'checkbox' })); + } + + nodeTemplate.label = dom.append(nodeTemplate.root, dom.$('div.model-view-tree-node-item-label')); + return nodeTemplate; + } + } + + /** + * Render a element, given an object bag returned by the template + */ + public renderElement(tree: ITree, element: ITreeComponentItem, templateId: string, templateData: TreeDataTemplate): void { + const icon = this.themeService.getTheme().type === LIGHT ? element.icon : element.iconDark; + templateData.icon.style.backgroundImage = icon ? `url('${icon}')` : ''; + dom.toggleClass(templateData.icon, 'model-view-tree-node-item-icon', !!icon); + if (element && !templateData.model) { + templateData.model = element; + } + if (templateId === TreeComponentRenderer.DEFAULT_TEMPLATE) { + this.renderNode(element, templateData); + } + } + + private renderNode(treeNode: ITreeComponentItem, templateData: TreeDataTemplate): void { + let label = treeNode.label; + templateData.label.textContent = label; + templateData.root.title = label; + templateData.checkboxState = this.getCheckboxState(treeNode); + } + + private getCheckboxState(treeNode: ITreeComponentItem): TreeCheckboxState { + if (treeNode.checked === undefined) { + return TreeCheckboxState.Intermediate; + } else { + return treeNode.checked ? TreeCheckboxState.Checked : TreeCheckboxState.Unchecked; + } + } + + public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { + this.dispose(); + // no op + } +} + diff --git a/src/sql/parts/modelComponents/tree/treeDataSource.ts b/src/sql/parts/modelComponents/tree/treeDataSource.ts new file mode 100644 index 0000000000..f338616b41 --- /dev/null +++ b/src/sql/parts/modelComponents/tree/treeDataSource.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 { ITree, IDataSource } from 'vs/base/parts/tree/browser/tree'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { IModelViewTreeViewDataProvider, ITreeComponentItem } from 'sql/workbench/common/views'; + +/** + * Implements the DataSource(that returns a parent/children of an element) for the recent connection tree + */ +export class TreeComponentDataSource implements IDataSource { + + /** + * + */ + constructor( + private _dataProvider: IModelViewTreeViewDataProvider) { + + } + + /** + * Returns the unique identifier of the given element. + * No more than one element may use a given identifier. + */ + public getId(tree: ITree, node: ITreeComponentItem): string { + return node.handle; + } + + /** + * Returns a boolean value indicating whether the element has children. + */ + public hasChildren(tree: ITree, node: ITreeComponentItem): boolean { + return this._dataProvider !== undefined; + } + + /** + * Returns the element's children as an array in a promise. + */ + public getChildren(tree: ITree, node: ITreeComponentItem): TPromise { + if (this._dataProvider) { + if (node && node.handle === '0') { + return this._dataProvider.getChildren(undefined); + } else { + return this._dataProvider.getChildren(node); + } + } + return TPromise.as([]); + } + + public getParent(tree: ITree, node: any): TPromise { + return TPromise.as(null); + } +} \ No newline at end of file diff --git a/src/sql/parts/modelComponents/tree/treeViewDataProvider.ts b/src/sql/parts/modelComponents/tree/treeViewDataProvider.ts new file mode 100644 index 0000000000..fd14e2ef61 --- /dev/null +++ b/src/sql/parts/modelComponents/tree/treeViewDataProvider.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { ExtHostModelViewTreeViewsShape, SqlExtHostContext } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; +import { IModelViewTreeViewDataProvider, ITreeComponentItem } from 'sql/workbench/common/views'; +import { INotificationService } from 'vs/platform/notification/common/notification'; +import * as vsTreeView from 'vs/workbench/api/electron-browser/mainThreadTreeViews'; + + +export class TreeViewDataProvider extends vsTreeView.TreeViewDataProvider implements IModelViewTreeViewDataProvider { + constructor(handle: number, treeViewId: string, + context: IExtHostContext, + notificationService?: INotificationService + ) { + super(`${handle}-${treeViewId}`, context.getProxy(SqlExtHostContext.ExtHostModelViewTreeViews), notificationService); + } + + onNodeCheckedChanged(treeViewId: string, treeItemHandle?: string, checked?: boolean) { + (this._proxy).$onNodeCheckedChanged(treeViewId, treeItemHandle, checked); + } + + protected postGetChildren(elements: ITreeComponentItem[]): ITreeComponentItem[] { + const result = []; + if (elements) { + for (const element of elements) { + element.onCheckedChanged = (checked: boolean) => { + this.onNodeCheckedChanged(this.treeViewId, element.handle, checked); + }; + this.itemsMap.set(element.handle, element); + result.push(element); + } + } + return result; + } +} diff --git a/src/sql/parts/modelComponents/viewBase.ts b/src/sql/parts/modelComponents/viewBase.ts index 196498910c..ffb6dd0358 100644 --- a/src/sql/parts/modelComponents/viewBase.ts +++ b/src/sql/parts/modelComponents/viewBase.ts @@ -10,7 +10,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import nls = require('vs/nls'); import * as sqlops from 'sqlops'; -import { IModelStore, IComponentDescriptor, IComponent, IComponentEventArgs } from './interfaces'; +import { IModelStore, IComponentDescriptor, IComponent } from './interfaces'; import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IModelView, IModelViewEventArgs } from 'sql/services/model/modelViewService'; import { Extensions, IComponentRegistry } from 'sql/platform/dashboard/common/modelComponentRegistry'; @@ -97,6 +97,10 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { this.queueAction(componentId, (component) => component.setProperties(properties)); } + refreshDataProvider(componentId: string, item: any): void { + this.queueAction(componentId, (component) => component.refreshDataProvider(item)); + } + private queueAction(componentId: string, action: (component: IComponent) => T): void { this.modelStore.eventuallyRunOnComponent(componentId, action).catch(err => { // TODO add error handling @@ -122,4 +126,8 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { public validate(componentId: string): Thenable { return new Promise(resolve => this.modelStore.eventuallyRunOnComponent(componentId, component => resolve(component.validate()))); } + + public setDataProvider(handle: number, componentId: string, context: any): any { + return this.queueAction(componentId, (component) => component.setDataProvider(handle, componentId, context)); + } } \ No newline at end of file diff --git a/src/sql/platform/dialog/dialog.module.ts b/src/sql/platform/dialog/dialog.module.ts index bf6a88fec3..329525ade1 100644 --- a/src/sql/platform/dialog/dialog.module.ts +++ b/src/sql/platform/dialog/dialog.module.ts @@ -22,7 +22,7 @@ import { IBootstrapParams, ISelector, providerIterator } from 'sql/services/boot import { CommonServiceInterface } from 'sql/services/common/commonServiceInterface.service'; import { Checkbox } from 'sql/base/browser/ui/checkbox/checkbox.component'; import { SelectBox } from 'sql/base/browser/ui/selectBox/selectBox.component'; -import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editabledropdown.component'; +import { EditableDropDown } from 'sql/base/browser/ui/editableDropdown/editableDropdown.component'; import { InputBox } from 'sql/base/browser/ui/inputBox/inputBox.component'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; diff --git a/src/sql/services/model/modelViewService.ts b/src/sql/services/model/modelViewService.ts index 3e7b83c78f..89ea327e28 100644 --- a/src/sql/services/model/modelViewService.ts +++ b/src/sql/services/model/modelViewService.ts @@ -25,6 +25,8 @@ 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; + setDataProvider(handle: number, componentId: string, context: any): void; + refreshDataProvider(componentId: string, item: any): void; registerEvent(componentId: string); onEvent: Event; validate(componentId: string): Thenable; diff --git a/src/sql/sqlops.proposed.d.ts b/src/sql/sqlops.proposed.d.ts index b47e48743a..17c29d1fb6 100644 --- a/src/sql/sqlops.proposed.d.ts +++ b/src/sql/sqlops.proposed.d.ts @@ -27,6 +27,7 @@ declare module 'sqlops' { text(): ComponentBuilder; button(): ComponentBuilder; dropDown(): ComponentBuilder; + tree(): ComponentBuilder>; listBox(): ComponentBuilder; table(): ComponentBuilder; declarativeTable(): ComponentBuilder; @@ -39,6 +40,17 @@ declare module 'sqlops' { fileBrowserTree(): ComponentBuilder; } + export interface TreeComponentDataProvider extends vscode.TreeDataProvider { + getTreeItem(element: T): TreeComponentItem | Thenable; + + onNodeCheckedChanged?(element: T, checked: boolean): void; + } + + + export class TreeComponentItem extends vscode.TreeItem { + checked?: boolean; + } + export interface ComponentBuilder { component(): T; withProperties(properties: U): ComponentBuilder; @@ -370,7 +382,7 @@ declare module 'sqlops' { } export interface TableColumn { - value: string + value: string; } export interface TableComponentProperties extends ComponentProperties { @@ -389,6 +401,10 @@ declare module 'sqlops' { label?: string; } + export interface TreeProperties { + withCheckbox?: boolean; + } + export enum DeclarativeDataType { string = 'string', category = 'category', @@ -514,6 +530,10 @@ declare module 'sqlops' { onDidChange: vscode.Event; } + export interface TreeComponent extends Component, TreeProperties { + registerDataProvider(dataProvider: TreeComponentDataProvider): any; + } + export interface WebViewComponent extends Component { html: string; message: any; @@ -534,7 +554,7 @@ declare module 'sqlops' { languageMode: string; } - export interface ButtonComponent extends Component, ButtonProperties { + export interface ButtonComponent extends Component, ButtonProperties { label: string; iconPath: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri }; onDidClick: vscode.Event; diff --git a/src/sql/workbench/api/common/sqlExtHostTypes.ts b/src/sql/workbench/api/common/sqlExtHostTypes.ts index 4b25964431..f2bd26b2d2 100644 --- a/src/sql/workbench/api/common/sqlExtHostTypes.ts +++ b/src/sql/workbench/api/common/sqlExtHostTypes.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import { TreeItem } from 'vs/workbench/api/node/extHostTypes'; + // SQL added extension host types export enum ServiceOptionType { string = 'string', @@ -146,6 +148,7 @@ export enum ModelComponentTypes { Group, Toolbar, LoadingComponent, + TreeComponent, FileBrowserTree, Editor } @@ -281,6 +284,11 @@ export enum CardType { VerticalButton = 'VerticalButton', Details = 'Details' } + +export class TreeComponentItem extends TreeItem { + checked?: boolean; +} + export class SqlThemeIcon { static readonly Folder = new SqlThemeIcon('Folder'); diff --git a/src/sql/workbench/api/node/extHostModelView.ts b/src/sql/workbench/api/node/extHostModelView.ts index 81ea4d104f..f082211bf3 100644 --- a/src/sql/workbench/api/node/extHostModelView.ts +++ b/src/sql/workbench/api/node/extHostModelView.ts @@ -13,14 +13,18 @@ import * as nls from 'vs/nls'; 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, IComponentEventArgs, ComponentEventType} from 'sql/workbench/api/common/sqlExtHostTypes'; +import { SqlMainContext, ExtHostModelViewShape, MainThreadModelViewShape, ExtHostModelViewTreeViewsShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, ComponentEventType } from 'sql/workbench/api/common/sqlExtHostTypes'; class ModelBuilderImpl implements sqlops.ModelBuilder { private nextComponentId: number; private readonly _componentBuilders = new Map>(); - constructor(private readonly _proxy: MainThreadModelViewShape, private readonly _handle: number) { + constructor( + private readonly _proxy: MainThreadModelViewShape, + private readonly _handle: number, + private readonly _mainContext: IMainContext, + private readonly _extHostModelViewTree: ExtHostModelViewTreeViewsShape) { this.nextComponentId = 0; } @@ -66,6 +70,13 @@ class ModelBuilderImpl implements sqlops.ModelBuilder { return builder; } + tree(): sqlops.ComponentBuilder> { + let id = this.getNextComponentId(); + let builder: ComponentBuilderImpl> = this.getComponentBuilder(new TreeComponentWrapper(this._extHostModelViewTree, this._proxy, this._handle, id), id); + this._componentBuilders.set(id, builder); + return builder; + } + inputBox(): sqlops.ComponentBuilder { let id = this.getNextComponentId(); let builder: ComponentBuilderImpl = this.getComponentBuilder(new InputBoxWrapper(this._proxy, this._handle, id), id); @@ -500,6 +511,11 @@ class ComponentWrapper implements sqlops.Component { } } + + protected setDataProvider(): Thenable { + return this._proxy.$setDataProvider(this._handle, this._id); + } + protected async setProperty(key: string, value: any): Promise { if (!this.properties[key] || this.properties[key] !== value) { // Only notify the front end if a value has been updated @@ -1038,6 +1054,28 @@ class FileBrowserTreeComponentWrapper extends ComponentWrapper implements sqlops } } +class TreeComponentWrapper extends ComponentWrapper implements sqlops.TreeComponent { + + constructor( + private _extHostModelViewTree: ExtHostModelViewTreeViewsShape, + proxy: MainThreadModelViewShape, handle: number, id: string) { + super(proxy, handle, ModelComponentTypes.TreeComponent, id); + this.properties = {}; + } + + public registerDataProvider(dataProvider: sqlops.TreeComponentDataProvider): vscode.TreeView { + this.setDataProvider(); + return this._extHostModelViewTree.$createTreeView(this._handle, this.id, { treeDataProvider: dataProvider }); + } + + public get withCheckbox(): boolean { + return this.properties['withCheckbox']; + } + public set withCheckbox(v: boolean) { + this.setProperty('withCheckbox', v); + } +} + class ModelViewImpl implements sqlops.ModelView { public onClosedEmitter = new Emitter(); @@ -1051,9 +1089,11 @@ class ModelViewImpl implements sqlops.ModelView { private readonly _proxy: MainThreadModelViewShape, private readonly _handle: number, private readonly _connection: sqlops.connection.Connection, - private readonly _serverInfo: sqlops.ServerInfo + private readonly _serverInfo: sqlops.ServerInfo, + private readonly mainContext: IMainContext, + private readonly _extHostModelViewTree: ExtHostModelViewTreeViewsShape ) { - this._modelBuilder = new ModelBuilderImpl(this._proxy, this._handle); + this._modelBuilder = new ModelBuilderImpl(this._proxy, this._handle, this.mainContext, this._extHostModelViewTree); } public get onClosed(): vscode.Event { @@ -1106,9 +1146,10 @@ export class ExtHostModelView implements ExtHostModelViewShape { private readonly _handlers = new Map void>(); constructor( - mainContext: IMainContext + private _mainContext: IMainContext, + private _extHostModelViewTree: ExtHostModelViewTreeViewsShape ) { - this._proxy = mainContext.getProxy(SqlMainContext.MainThreadModelView); + this._proxy = _mainContext.getProxy(SqlMainContext.MainThreadModelView); } $onClosed(handle: number): void { @@ -1123,7 +1164,7 @@ export class ExtHostModelView implements ExtHostModelViewShape { } $registerWidget(handle: number, id: string, connection: sqlops.connection.Connection, serverInfo: sqlops.ServerInfo): void { - let view = new ModelViewImpl(this._proxy, handle, connection, serverInfo); + let view = new ModelViewImpl(this._proxy, handle, connection, serverInfo, this._mainContext, this._extHostModelViewTree); this._modelViews.set(handle, view); this._handlers.get(id)(view); } diff --git a/src/sql/workbench/api/node/extHostModelViewTree.ts b/src/sql/workbench/api/node/extHostModelViewTree.ts new file mode 100644 index 0000000000..f1820f2be1 --- /dev/null +++ b/src/sql/workbench/api/node/extHostModelViewTree.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { localize } from 'vs/nls'; +import * as vscode from 'vscode'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { SqlMainContext, ExtHostModelViewTreeViewsShape, MainThreadModelViewShape } from 'sql/workbench/api/node/sqlExtHost.protocol'; +import { ITreeComponentItem } from 'sql/workbench/common/views'; +import { CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; +import { asWinJsPromise } from 'vs/base/common/async'; +import { IMainContext } from 'vs/workbench/api/node/extHost.protocol'; +import * as sqlops from 'sqlops'; +import * as vsTreeExt from 'vs/workbench/api/node/extHostTreeViews'; + +export class ExtHostModelViewTreeViews implements ExtHostModelViewTreeViewsShape { + private _proxy: MainThreadModelViewShape; + + private treeViews: Map> = new Map>(); + + + constructor( + private _mainContext: IMainContext + ) { + this._proxy = this._mainContext.getProxy(SqlMainContext.MainThreadModelView); + } + + $createTreeView(handle: number, componentId: string, options: { treeDataProvider: sqlops.TreeComponentDataProvider }): vscode.TreeView { + if (!options || !options.treeDataProvider) { + throw new Error('Options with treeDataProvider is mandatory'); + } + + const treeView = this.createExtHostTreeViewer(handle, componentId, options.treeDataProvider); + return { + reveal: (element: T, options?: { select?: boolean }): Thenable => { + return treeView.reveal(element, options); + }, + dispose: () => { + this.treeViews.delete(componentId); + treeView.dispose(); + } + }; + } + + $getChildren(treeViewId: string, treeItemHandle?: string): TPromise { + const treeView = this.treeViews.get(treeViewId); + if (!treeView) { + + return TPromise.wrapError(new Error(localize('treeView.notRegistered', 'No tree view with id \'{0}\' registered.', treeViewId))); + } + return treeView.getChildren(treeItemHandle); + } + + $onNodeCheckedChanged(treeViewId: string, treeItemHandle?: string, checked?: boolean): void { + const treeView = this.treeViews.get(treeViewId); + if (treeView) { + treeView.onNodeCheckedChanged(treeItemHandle, checked); + } + } + + private createExtHostTreeViewer(handle: number, id: string, dataProvider: sqlops.TreeComponentDataProvider): ExtHostTreeView { + const treeView = new ExtHostTreeView(handle, id, dataProvider, this._proxy, undefined); + this.treeViews.set(`${handle}-${id}`, treeView); + return treeView; + } +} + +export class ExtHostTreeView extends vsTreeExt.ExtHostTreeView { + + constructor(private handle: number, private componentId: string, private componentDataProvider: sqlops.TreeComponentDataProvider, private modelViewProxy: MainThreadModelViewShape, commands: CommandsConverter) { + super(componentId, componentDataProvider, undefined, commands); + } + + onNodeCheckedChanged(parentHandle?: vsTreeExt.TreeItemHandle, checked?: boolean): void { + const parentElement = parentHandle ? this.getExtensionElement(parentHandle) : void 0; + if (parentHandle && !parentElement) { + console.error(`No tree item with id \'${parentHandle}\' found.`); + } + + this.componentDataProvider.onNodeCheckedChanged(parentElement, checked); + } + + reveal(element: T, options?: { select?: boolean }): TPromise { + if (typeof this.componentDataProvider.getParent !== 'function') { + return TPromise.wrapError(new Error(`Required registered TreeDataProvider to implement 'getParent' method to access 'reveal' method`)); + } + let i: void; + return this.resolveUnknownParentChain(element) + .then(parentChain => this.resolveTreeNode(element, parentChain[parentChain.length - 1]) + .then(treeNode => i)); + } + + protected refresh(elements: T[]): void { + const hasRoot = elements.some(element => !element); + if (hasRoot) { + this.clearAll(); // clear cache + this.modelViewProxy.$refreshDataProvider(this.handle, this.componentId); + } else { + const handlesToRefresh = this.getHandlesToRefresh(elements); + if (handlesToRefresh.length) { + this.refreshHandles(handlesToRefresh); + } + } + } + + protected refreshHandles(itemHandles: vsTreeExt.TreeItemHandle[]): TPromise { + const itemsToRefresh: { [treeItemHandle: string]: ITreeComponentItem } = {}; + return TPromise.join(itemHandles.map(treeItemHandle => + this.refreshNode(treeItemHandle) + .then(node => { + if (node) { + itemsToRefresh[treeItemHandle] = node.item; + } + }))) + .then(() => Object.keys(itemsToRefresh).length ? this.modelViewProxy.$refreshDataProvider(this.handle, this.componentId, itemsToRefresh) : null); + } + + protected refreshNode(treeItemHandle: vsTreeExt.TreeItemHandle): TPromise { + const extElement = this.getExtensionElement(treeItemHandle); + const existing = this.nodes.get(extElement); + //this.clearChildren(extElement); // clear children cache + return asWinJsPromise(() => this.componentDataProvider.getTreeItem(extElement)) + .then(extTreeItem => { + if (extTreeItem) { + const newNode = this.createTreeNode(extElement, extTreeItem, existing.parent); + this.updateNodeCache(extElement, newNode, existing, existing.parent); + return newNode; + } + return null; + }); + } + + protected createTreeItem(element: T, extensionTreeItem: sqlops.TreeComponentItem, parent?: vsTreeExt.TreeNode): ITreeComponentItem { + let item = super.createTreeItem(element, extensionTreeItem, parent); + item = Object.assign({}, item, { checked: extensionTreeItem.checked }); + return item; + } +} \ No newline at end of file diff --git a/src/sql/workbench/api/node/mainThreadModelView.ts b/src/sql/workbench/api/node/mainThreadModelView.ts index de5f00ba31..42191ebfe1 100644 --- a/src/sql/workbench/api/node/mainThreadModelView.ts +++ b/src/sql/workbench/api/node/mainThreadModelView.ts @@ -9,10 +9,9 @@ import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostC import { IExtHostContext } from 'vs/workbench/api/node/extHost.protocol'; import { Disposable } from 'vs/base/common/lifecycle'; -import * as sqlops from 'sqlops'; import { IModelViewService } from 'sql/services/modelComponents/modelViewService'; -import { IItemConfig, ModelComponentTypes, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; +import { IItemConfig, IComponentShape } from 'sql/workbench/api/common/sqlExtHostTypes'; import { IModelView } from 'sql/services/model/modelViewService'; @@ -22,15 +21,14 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi private static _handlePool = 0; private readonly _proxy: ExtHostModelViewShape; private readonly _dialogs = new Map(); - private knownWidgets = new Array(); constructor( - context: IExtHostContext, + private _context: IExtHostContext, @IModelViewService viewService: IModelViewService ) { super(); - this._proxy = context.getProxy(SqlExtHostContext.ExtHostModelView); + this._proxy = _context.getProxy(SqlExtHostContext.ExtHostModelView); viewService.onRegisteredModelView(view => { if (this.knownWidgets.includes(view.id)) { let handle = MainThreadModelView._handlePool++; @@ -79,6 +77,14 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi }); } + $setDataProvider(handle: number, componentId: string): Thenable { + return this.execModelViewAction(handle, (modelView) => modelView.setDataProvider(handle, componentId, this._context)); + } + + $refreshDataProvider(handle: number, componentId: string, item?: any): Thenable { + return this.execModelViewAction(handle, (modelView) => modelView.refreshDataProvider(componentId, item)); + } + $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.api.impl.ts b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts index 7cb961e3bb..9fc12fd2a4 100644 --- a/src/sql/workbench/api/node/sqlExtHost.api.impl.ts +++ b/src/sql/workbench/api/node/sqlExtHost.api.impl.ts @@ -33,6 +33,7 @@ import { ExtHostDashboard } from 'sql/workbench/api/node/extHostDashboard'; import { ExtHostObjectExplorer } from 'sql/workbench/api/node/extHostObjectExplorer'; import { ExtHostLogService } from 'vs/workbench/api/node/extHostLogService'; import { ExtHostModelViewDialog } from 'sql/workbench/api/node/extHostModelViewDialog'; +import { ExtHostModelViewTreeViews } from 'sql/workbench/api/node/extHostModelViewTree'; import { ExtHostQueryEditor } from 'sql/workbench/api/node/extHostQueryEditor'; import { ExtHostBackgroundTaskManagement } from './extHostBackgroundTaskManagement'; @@ -66,7 +67,8 @@ export function createApiFactory( const extHostTasks = rpcProtocol.set(SqlExtHostContext.ExtHostTasks, new ExtHostTasks(rpcProtocol, logService)); const extHostBackgroundTaskManagement = rpcProtocol.set(SqlExtHostContext.ExtHostBackgroundTaskManagement, new ExtHostBackgroundTaskManagement(rpcProtocol)); const extHostWebviewWidgets = rpcProtocol.set(SqlExtHostContext.ExtHostDashboardWebviews, new ExtHostDashboardWebviews(rpcProtocol)); - const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol)); + const extHostModelViewTree = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewTreeViews, new ExtHostModelViewTreeViews(rpcProtocol)); + const extHostModelView = rpcProtocol.set(SqlExtHostContext.ExtHostModelView, new ExtHostModelView(rpcProtocol, extHostModelViewTree)); const extHostDashboard = rpcProtocol.set(SqlExtHostContext.ExtHostDashboard, new ExtHostDashboard(rpcProtocol)); const extHostModelViewDialog = rpcProtocol.set(SqlExtHostContext.ExtHostModelViewDialog, new ExtHostModelViewDialog(rpcProtocol, extHostModelView, extHostBackgroundTaskManagement)); const extHostQueryEditor = rpcProtocol.set(SqlExtHostContext.ExtHostQueryEditor, new ExtHostQueryEditor(rpcProtocol)); @@ -415,7 +417,8 @@ export function createApiFactory( ui: ui, StatusIndicator: sqlExtHostTypes.StatusIndicator, CardType: sqlExtHostTypes.CardType, - SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon + SqlThemeIcon: sqlExtHostTypes.SqlThemeIcon, + TreeComponentItem: sqlExtHostTypes.TreeComponentItem }; } }; diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 75dbc37051..05aa78f264 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -16,6 +16,7 @@ import { IDisposable } from 'vs/base/common/lifecycle'; import * as sqlops from 'sqlops'; import * as vscode from 'vscode'; +import { ITreeComponentItem } from 'sql/workbench/common/views'; import { ITaskHandlerDescription } from 'sql/platform/tasks/common/tasks'; import { IItemConfig, ModelComponentTypes, IComponentShape, IModelViewDialogDetails, IModelViewTabDetails, IModelViewButtonDetails, @@ -531,6 +532,7 @@ export const SqlExtHostContext = { ExtHostBackgroundTaskManagement: createExtId('ExtHostBackgroundTaskManagement'), ExtHostDashboardWebviews: createExtId('ExtHostDashboardWebviews'), ExtHostModelView: createExtId('ExtHostModelView'), + ExtHostModelViewTreeViews: createExtId('ExtHostModelViewTreeViews'), ExtHostDashboard: createExtId('ExtHostDashboard'), ExtHostModelViewDialog: createExtId('ExtHostModelViewDialog'), ExtHostQueryEditor: createExtId('ExtHostQueryEditor') @@ -590,6 +592,12 @@ export interface ExtHostModelViewShape { $runCustomValidations(handle: number, id: string): Thenable; } +export interface ExtHostModelViewTreeViewsShape { + $getChildren(treeViewId: string, treeItemHandle?: string): TPromise; + $createTreeView(handle: number, componentId: string, options: { treeDataProvider: vscode.TreeDataProvider }): vscode.TreeView; + $onNodeCheckedChanged(treeViewId: string, treeItemHandle?: string, checked?: boolean): void; +} + export interface ExtHostBackgroundTaskManagementShape { $onTaskRegistered(operationId: string): void; $onTaskCanceled(operationId: string): void; @@ -611,6 +619,8 @@ export interface MainThreadModelViewShape extends IDisposable { $setProperties(handle: number, componentId: string, properties: { [key: string]: any }): Thenable; $registerEvent(handle: number, componentId: string): Thenable; $validate(handle: number, componentId: string): Thenable; + $setDataProvider(handle: number, componentId: string): Thenable; + $refreshDataProvider(handle: number, componentId: string, item?: any): Thenable; } export interface ExtHostObjectExplorerShape { diff --git a/src/sql/workbench/common/views.ts b/src/sql/workbench/common/views.ts new file mode 100644 index 0000000000..d3d8621946 --- /dev/null +++ b/src/sql/workbench/common/views.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { TPromise } from 'vs/base/common/winjs.base'; +import { Event } from 'vs/base/common/event'; +import { ITreeViewDataProvider, ITreeItem } from 'vs/workbench/common/views'; + +export interface ITreeComponentItem extends ITreeItem { + checked?: boolean; + onCheckedChanged?: (checked: boolean) => void; + children?: ITreeComponentItem[]; +} + +export interface IModelViewTreeViewDataProvider extends ITreeViewDataProvider { + refresh(itemsToRefreshByHandle: { [treeItemHandle: string]: ITreeComponentItem }); +} + +export interface IModelViewTreeViewDataProvider { + onDidChange: Event; + + onDispose: Event; + + getChildren(element?: ITreeComponentItem): TPromise; +} \ No newline at end of file diff --git a/src/sqltest/workbench/api/extHostModelView.test.ts b/src/sqltest/workbench/api/extHostModelView.test.ts index ab8f2b2df6..d339ccedef 100644 --- a/src/sqltest/workbench/api/extHostModelView.test.ts +++ b/src/sqltest/workbench/api/extHostModelView.test.ts @@ -47,7 +47,7 @@ suite('ExtHostModelView Validation Tests', () => { mockProxy.setup(x => x.$setProperties(It.isAny(), It.isAny(), It.isAny())).returns(() => Promise.resolve()); // Register a model view of an input box and drop down box inside a form container inside a flex container - extHostModelView = new ExtHostModelView(mainContext); + extHostModelView = new ExtHostModelView(mainContext, undefined); extHostModelView.$registerProvider(widgetId, async view => { modelView = view; inputBox = view.modelBuilder.inputBox() diff --git a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts index 5676a70953..a2bd828a12 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadTreeViews.ts @@ -60,9 +60,11 @@ export class MainThreadTreeViews extends Disposable implements MainThreadTreeVie } } -type TreeItemHandle = string; +// {{SQL CARBON EDIT}} +export type TreeItemHandle = string; -class TreeViewDataProvider implements ITreeViewDataProvider { +// {{SQL CARBON EDIT}} +export class TreeViewDataProvider implements ITreeViewDataProvider { private readonly _onDidChange: Emitter = new Emitter(); readonly onDidChange: Event = this._onDidChange.event; @@ -70,11 +72,13 @@ class TreeViewDataProvider implements ITreeViewDataProvider { private readonly _onDispose: Emitter = new Emitter(); readonly onDispose: Event = this._onDispose.event; - private itemsMap: Map = new Map(); + // {{SQL CARBON EDIT}} + protected itemsMap: Map = new Map(); - constructor(private treeViewId: string, - private _proxy: ExtHostTreeViewsShape, - private notificationService: INotificationService + // {{SQL CARBON EDIT}} + constructor(protected treeViewId: string, + protected _proxy: ExtHostTreeViewsShape, + protected notificationService: INotificationService ) { } @@ -121,7 +125,8 @@ class TreeViewDataProvider implements ITreeViewDataProvider { } } - private postGetChildren(elements: ITreeItem[]): ITreeItem[] { + // {{SQL CARBON EDIT}} + protected postGetChildren(elements: ITreeItem[]): ITreeItem[] { const result = []; if (elements) { for (const element of elements) { diff --git a/src/vs/workbench/api/node/extHostTreeViews.ts b/src/vs/workbench/api/node/extHostTreeViews.ts index 41fafaa578..69ec46ab3b 100644 --- a/src/vs/workbench/api/node/extHostTreeViews.ts +++ b/src/vs/workbench/api/node/extHostTreeViews.ts @@ -18,7 +18,8 @@ import { asWinJsPromise } from 'vs/base/common/async'; import { TreeItemCollapsibleState, ThemeIcon } from 'vs/workbench/api/node/extHostTypes'; import { isUndefinedOrNull } from 'vs/base/common/types'; -type TreeItemHandle = string; +// {{SQL CARBON EDIT}} +export type TreeItemHandle = string; export class ExtHostTreeViews implements ExtHostTreeViewsShape { @@ -79,24 +80,30 @@ export class ExtHostTreeViews implements ExtHostTreeViewsShape { } } -interface TreeNode { +// {{SQL CARBON EDIT}} +export interface TreeNode { item: ITreeItem; parent: TreeNode; children: TreeNode[]; } -class ExtHostTreeView extends Disposable { +// {{SQL CARBON EDIT}} +export class ExtHostTreeView extends Disposable { private static LABEL_HANDLE_PREFIX = '0'; private static ID_HANDLE_PREFIX = '1'; private roots: TreeNode[] = null; private elements: Map = new Map(); - private nodes: Map = new Map(); + // {{SQL CARBON EDIT}} + protected nodes: Map = new Map(); constructor(private viewId: string, private dataProvider: vscode.TreeDataProvider, private proxy: MainThreadTreeViewsShape, private commands: CommandsConverter) { super(); - this.proxy.$registerTreeViewDataProvider(viewId); + // {{SQL CARBON EDIT}} + if (this.proxy) { + this.proxy.$registerTreeViewDataProvider(viewId); + } if (this.dataProvider.onDidChangeTreeData) { this._register(debounceEvent(this.dataProvider.onDidChangeTreeData, (last, current) => last ? [...last, current] : [current], 200)(elements => this.refresh(elements))); } @@ -127,7 +134,8 @@ class ExtHostTreeView extends Disposable { .then(treeNode => this.proxy.$reveal(this.viewId, treeNode.item, parentChain.map(p => p.item), options))); } - private resolveUnknownParentChain(element: T): TPromise { + // {{SQL CARBON EDIT}} + protected resolveUnknownParentChain(element: T): TPromise { return this.resolveParent(element) .then((parent) => { if (!parent) { @@ -150,7 +158,8 @@ class ExtHostTreeView extends Disposable { return asWinJsPromise(() => this.dataProvider.getParent(element)); } - private resolveTreeNode(element: T, parent?: TreeNode): TPromise { + // {{SQL CARBON EDIT}} + protected resolveTreeNode(element: T, parent?: TreeNode): TPromise { return asWinJsPromise(() => this.dataProvider.getTreeItem(element)) .then(extTreeItem => this.createHandle(element, extTreeItem, parent, true)) .then(handle => this.getChildren(parent ? parent.item.handle : null) @@ -194,7 +203,8 @@ class ExtHostTreeView extends Disposable { .then(nodes => nodes.filter(n => !!n)); } - private refresh(elements: T[]): void { + // {{SQL CARBON EDIT}} + protected refresh(elements: T[]): void { const hasRoot = elements.some(element => !element); if (hasRoot) { this.clearAll(); // clear cache @@ -207,7 +217,8 @@ class ExtHostTreeView extends Disposable { } } - private getHandlesToRefresh(elements: T[]): TreeItemHandle[] { + // {{SQL CARBON EDIT}} + protected getHandlesToRefresh(elements: T[]): TreeItemHandle[] { const elementsToUpdate = new Set(); for (const element of elements) { let elementNode = this.nodes.get(element); @@ -237,7 +248,8 @@ class ExtHostTreeView extends Disposable { return handlesToUpdate; } - private refreshHandles(itemHandles: TreeItemHandle[]): TPromise { + // {{SQL CARBON EDIT}} + protected refreshHandles(itemHandles: TreeItemHandle[]): TPromise { const itemsToRefresh: { [treeItemHandle: string]: ITreeItem } = {}; return TPromise.join(itemHandles.map(treeItemHandle => this.refreshNode(treeItemHandle) @@ -249,7 +261,8 @@ class ExtHostTreeView extends Disposable { .then(() => Object.keys(itemsToRefresh).length ? this.proxy.$refresh(this.viewId, itemsToRefresh) : null); } - private refreshNode(treeItemHandle: TreeItemHandle): TPromise { + // {{SQL CARBON EDIT}} + protected refreshNode(treeItemHandle: TreeItemHandle): TPromise { const extElement = this.getExtensionElement(treeItemHandle); const existing = this.nodes.get(extElement); this.clearChildren(extElement); // clear children cache @@ -274,7 +287,8 @@ class ExtHostTreeView extends Disposable { return node; } - private createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode): TreeNode { + // {{SQL CARBON EDIT}} + protected createTreeNode(element: T, extensionTreeItem: vscode.TreeItem, parent: TreeNode): TreeNode { return { item: this.createTreeItem(element, extensionTreeItem, parent), parent, @@ -282,7 +296,8 @@ class ExtHostTreeView extends Disposable { }; } - private createTreeItem(element: T, extensionTreeItem: vscode.TreeItem, parent?: TreeNode): ITreeItem { + // {{SQL CARBON EDIT}} + protected createTreeItem(element: T, extensionTreeItem: vscode.TreeItem, parent?: TreeNode): ITreeItem { const handle = this.createHandle(element, extensionTreeItem, parent); const icon = this.getLightIconPath(extensionTreeItem); @@ -354,7 +369,8 @@ class ExtHostTreeView extends Disposable { this.nodes.set(element, node); } - private updateNodeCache(element: T, newNode: TreeNode, existing: TreeNode, parentNode: TreeNode): void { + // {{SQL CARBON EDIT}} + protected updateNodeCache(element: T, newNode: TreeNode, existing: TreeNode, parentNode: TreeNode): void { // Remove from the cache this.elements.delete(newNode.item.handle); this.nodes.delete(element); @@ -418,7 +434,8 @@ class ExtHostTreeView extends Disposable { this.elements.delete(node.item.handle); } - private clearAll(): void { + // {{SQL CARBON EDIT}} + protected clearAll(): void { this.roots = null; this.elements.clear(); this.nodes.clear();