diff --git a/extensions/machine-learning/src/test/views/utils.ts b/extensions/machine-learning/src/test/views/utils.ts index 3264e86a89..25a1432c6d 100644 --- a/extensions/machine-learning/src/test/views/utils.ts +++ b/extensions/machine-learning/src/test/views/utils.ts @@ -51,7 +51,8 @@ export function createViewContext(): ViewTestContext { removeItem: () => true, insertItem: () => { }, items: [], - setLayout: () => { } + setLayout: () => { }, + setItemLayout: () => { } }; let form: azdata.FormContainer = Object.assign({}, componentBase, container, { }); diff --git a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts index da073e91ba..58dafc17ee 100644 --- a/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts +++ b/extensions/notebook/src/test/managePackages/managePackagesDialog.test.ts @@ -139,7 +139,8 @@ describe('Manage Package Dialog', () => { removeItem: () => true, insertItem: () => { }, items: components, - setLayout: () => { } + setLayout: () => { }, + setItemLayout: () => { } }; let form: azdata.FormContainer = Object.assign({}, componentBase, container, { }); diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 65f1672915..067cfa3b04 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -385,6 +385,10 @@ declare module 'azdata' { alwaysShowTabs?: boolean; } + export interface Container extends Component { + setItemLayout(component: Component, layout: TItemLayout): void; + } + export interface TaskInfo { targetLocation?: string; } diff --git a/src/sql/base/browser/ui/panel/panel.component.ts b/src/sql/base/browser/ui/panel/panel.component.ts index e7e465b0dd..3e5ea52fa7 100644 --- a/src/sql/base/browser/ui/panel/panel.component.ts +++ b/src/sql/base/browser/ui/panel/panel.component.ts @@ -249,6 +249,31 @@ export class PanelComponent extends Disposable implements IThemable { this.selectTab(nextTabIndex); } + /** + * Updates the specified tab with new config values + * @param tabId The id of the tab to update + * @param config The values to update the tab with + */ + public updateTab(tabId: string, config: { title?: string, iconClass?: string }): void { + // First find the tab and update it with the new values. Then manually refresh the + // tab header since it won't detect changes made to the corresponding tab by itself. + let tabHeader: TabHeaderComponent; + const tabHeaders = this._tabHeaders.toArray(); + const tab = this._tabs.find((item, i) => { + if (item.identifier === tabId) { + tabHeader = tabHeaders?.length > i ? tabHeaders[i] : undefined; + return true; + } + return false; + }); + + if (tab) { + tab.title = config.title; + tab.iconClass = config.iconClass; + tabHeader?.refresh(); + } + } + private findAndRemoveTabFromMRU(tab: TabComponent): void { let mruIndex = firstIndex(this._mru, i => i === tab); diff --git a/src/sql/base/browser/ui/panel/tabHeader.component.ts b/src/sql/base/browser/ui/panel/tabHeader.component.ts index 76fa6e2572..bafd7feb6b 100644 --- a/src/sql/base/browser/ui/panel/tabHeader.component.ts +++ b/src/sql/base/browser/ui/panel/tabHeader.component.ts @@ -5,7 +5,7 @@ import 'vs/css!./media/tabHeader'; -import { Component, AfterContentInit, OnDestroy, Input, Output, ElementRef, ViewChild, EventEmitter } from '@angular/core'; +import { Component, AfterContentInit, OnDestroy, Input, Output, ElementRef, ViewChild, EventEmitter, ChangeDetectorRef, forwardRef, Inject } from '@angular/core'; import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; @@ -21,8 +21,8 @@ import { CloseTabAction } from 'sql/base/browser/ui/panel/tabActions'; template: ` @@ -40,10 +40,10 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit, @ViewChild('actionHeader', { read: ElementRef }) private _actionHeaderRef!: ElementRef; @ViewChild('actionbar', { read: ElementRef }) private _actionbarRef!: ElementRef; - @ViewChild('tabLabel', { read: ElementRef }) private _tabLabelRef!: ElementRef; - @ViewChild('tabIcon', { read: ElementRef }) private _tabIconRef!: ElementRef; - constructor() { + constructor( + @Inject(forwardRef(() => ChangeDetectorRef)) private _cd: ChangeDetectorRef + ) { super(); } @@ -51,6 +51,10 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit, return this._actionHeaderRef.nativeElement; } + public refresh(): void { + this._cd.detectChanges(); + } + ngAfterContentInit(): void { if (this.tab.canClose || this.tab.actions) { this._actionbar = new ActionBar(this._actionbarRef.nativeElement); @@ -62,15 +66,6 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit, this._actionbar.push(closeAction, { icon: true, label: false }); } } - - const tabLabelContainer = this._tabLabelRef.nativeElement as HTMLElement; - if (this.showIcon && this.tab.iconClass) { - const tabIconContainer = this._tabIconRef.nativeElement as HTMLElement; - tabIconContainer.className = 'tabIcon codicon icon'; - tabIconContainer.classList.add(this.tab.iconClass); - } - - tabLabelContainer.textContent = this.tab.title; } ngOnDestroy() { diff --git a/src/sql/platform/dashboard/browser/interfaces.ts b/src/sql/platform/dashboard/browser/interfaces.ts index ffa897709c..a2924ca95a 100644 --- a/src/sql/platform/dashboard/browser/interfaces.ts +++ b/src/sql/platform/dashboard/browser/interfaces.ts @@ -86,6 +86,7 @@ export interface IComponent extends IDisposable { addToContainer?: (componentDescriptor: IComponentDescriptor, config: any, index?: number) => void; removeFromContainer?: (componentDescriptor: IComponentDescriptor) => void; setLayout?: (layout: any) => void; + setItemLayout?: (componentDescriptor: IComponentDescriptor, config: any) => void; getHtml: () => any; setProperties?: (properties: { [key: string]: any; }) => void; enabled: boolean; diff --git a/src/sql/platform/model/browser/modelViewService.ts b/src/sql/platform/model/browser/modelViewService.ts index d25d5d658c..d88e7ca910 100644 --- a/src/sql/platform/model/browser/modelViewService.ts +++ b/src/sql/platform/model/browser/modelViewService.ts @@ -36,6 +36,7 @@ export interface IModelView extends IView { addToContainer(containerId: string, item: IItemConfig, index?: number): void; removeFromContainer(containerId: string, item: IItemConfig): void; setLayout(componentId: string, layout: any): void; + setItemLayout(componentId: string, item: IItemConfig): void; setProperties(componentId: string, properties: { [key: string]: any }): void; setDataProvider(handle: number, componentId: string, context: any): void; refreshDataProvider(componentId: string, item: any): void; diff --git a/src/sql/workbench/api/browser/mainThreadModelView.ts b/src/sql/workbench/api/browser/mainThreadModelView.ts index 57454f3b35..d28288ef8d 100644 --- a/src/sql/workbench/api/browser/mainThreadModelView.ts +++ b/src/sql/workbench/api/browser/mainThreadModelView.ts @@ -66,6 +66,10 @@ export class MainThreadModelView extends Disposable implements MainThreadModelVi return this.execModelViewAction(handle, (modelView) => modelView.setLayout(componentId, layout)); } + $setItemLayout(handle: number, containerId: string, item: IItemConfig): Thenable { + return this.execModelViewAction(handle, (modelView) => modelView.setItemLayout(containerId, item)); + } + private onEvent(handle: number, componentId: string, eventArgs: any) { this._proxy.$handleEvent(handle, componentId, eventArgs); } diff --git a/src/sql/workbench/api/common/extHostModelView.ts b/src/sql/workbench/api/common/extHostModelView.ts index 8f84f624b4..35e6f2bf0a 100644 --- a/src/sql/workbench/api/common/extHostModelView.ts +++ b/src/sql/workbench/api/common/extHostModelView.ts @@ -19,6 +19,7 @@ import { IItemConfig, ModelComponentTypes, IComponentShape, IComponentEventArgs, import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { firstIndex } from 'vs/base/common/arrays'; import { ILogService } from 'vs/platform/log/common/log'; +import { onUnexpectedError } from 'vs/base/common/errors'; class ModelBuilderImpl implements azdata.ModelBuilder { private nextComponentId: number; @@ -730,6 +731,15 @@ class ComponentWrapper implements azdata.Component { return this._proxy.$setLayout(this._handle, this.id, layout); } + public setItemLayout(item: azdata.Component, itemLayout: any): boolean { + const itemConfig = this.itemConfigs.find(c => c.component.id === item.id); + if (itemConfig) { + itemConfig.config = itemLayout; + this._proxy.$setItemLayout(this._handle, this.id, itemConfig.toIItemConfig()).then(undefined, onUnexpectedError); + } + return false; + } + public updateProperties(properties: { [key: string]: any }): Thenable { this.properties = assign(this.properties, properties); return this.notifyPropertyChanged(); @@ -1740,11 +1750,19 @@ class TabbedPanelComponentWrapper extends ComponentWrapper implements azdata.Tab this.properties = {}; this._emitterMap.set(ComponentEventType.onDidChange, new Emitter()); } + updateTabs(tabs: (azdata.Tab | azdata.TabGroup)[]): void { - this.clearItems(); const itemConfigs = createFromTabs(tabs); - itemConfigs.forEach(itemConfig => { - this.addItem(itemConfig.component, itemConfig.config); + // Go through all of the tabs and either update their layout if they already exist + // or add them if they don't. + // We do not currently support reordering or removing tabs. + itemConfigs.forEach(newItemConfig => { + const existingTab = this.itemConfigs.find(itemConfig => newItemConfig.config.id === itemConfig.config.id); + if (existingTab) { + this.setItemLayout(existingTab.component, newItemConfig.config); + } else { + this.addItem(newItemConfig.component, newItemConfig.config); + } }); } diff --git a/src/sql/workbench/api/common/sqlExtHost.protocol.ts b/src/sql/workbench/api/common/sqlExtHost.protocol.ts index dcbf427877..f5b03c8a12 100644 --- a/src/sql/workbench/api/common/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/common/sqlExtHost.protocol.ts @@ -732,6 +732,7 @@ export interface MainThreadModelViewShape extends IDisposable { $addToContainer(handle: number, containerId: string, item: IItemConfig, index?: number): Thenable; $removeFromContainer(handle: number, containerId: string, item: IItemConfig): Thenable; $setLayout(handle: number, componentId: string, layout: any): Thenable; + $setItemLayout(handle: number, componentId: string, item: IItemConfig): Thenable; $setProperties(handle: number, componentId: string, properties: { [key: string]: any }): Thenable; $registerEvent(handle: number, componentId: string): Thenable; $validate(handle: number, componentId: string): Thenable; diff --git a/src/sql/workbench/browser/modelComponents/componentBase.ts b/src/sql/workbench/browser/modelComponents/componentBase.ts index 62b6d639ee..693a8df9e7 100644 --- a/src/sql/workbench/browser/modelComponents/componentBase.ts +++ b/src/sql/workbench/browser/modelComponents/componentBase.ts @@ -352,6 +352,24 @@ export abstract class ContainerBase extends ComponentBase { abstract setLayout(layout: any): void; + public setItemLayout(componentDescriptor: IComponentDescriptor, config: any): void { + if (!componentDescriptor) { + return; + } + const item = this.items.find(item => item.descriptor.id === componentDescriptor.id && item.descriptor.type === componentDescriptor.type); + if (item) { + item.config = config; + this.onItemLayoutUpdated(item); + this._changeRef.detectChanges(); + } else { + throw new Error(`Unable to set item layout - unknown item ${componentDescriptor.id}`); + } + return; + } + protected onItemsUpdated(): void { } + + protected onItemLayoutUpdated(item: ItemDescriptor): void { + } } diff --git a/src/sql/workbench/browser/modelComponents/tabbedPanel.component.ts b/src/sql/workbench/browser/modelComponents/tabbedPanel.component.ts index f147307ff5..eacbecdbfe 100644 --- a/src/sql/workbench/browser/modelComponents/tabbedPanel.component.ts +++ b/src/sql/workbench/browser/modelComponents/tabbedPanel.component.ts @@ -5,7 +5,7 @@ import { AfterViewInit, ChangeDetectorRef, Component, ElementRef, forwardRef, Inject, Input, OnDestroy, ViewChild } from '@angular/core'; import { NavigationBarLayout, PanelComponent } from 'sql/base/browser/ui/panel/panel.component'; import { TabType } from 'sql/base/browser/ui/panel/tab.component'; -import { ContainerBase } from 'sql/workbench/browser/modelComponents/componentBase'; +import { ContainerBase, ItemDescriptor } from 'sql/workbench/browser/modelComponents/componentBase'; import { ComponentEventType, IComponent, IComponentDescriptor, IModelStore } from 'sql/platform/dashboard/browser/interfaces'; import 'vs/css!./media/tabbedPanel'; import { IUserFriendlyIcon, createIconCssClass } from 'sql/workbench/browser/modelComponents/iconUtils'; @@ -121,4 +121,8 @@ export default class TabbedPanelComponent extends ContainerBase imple this._panel.selectTab(firstTabIndex); } } + + onItemLayoutUpdated(item: ItemDescriptor): void { + this._panel.updateTab(item.config.id, { title: item.config.title, iconClass: item.config.icon ? createIconCssClass(item.config.icon) : undefined }); + } } diff --git a/src/sql/workbench/browser/modelComponents/viewBase.ts b/src/sql/workbench/browser/modelComponents/viewBase.ts index 9e5b6d07d0..33f770f187 100644 --- a/src/sql/workbench/browser/modelComponents/viewBase.ts +++ b/src/sql/workbench/browser/modelComponents/viewBase.ts @@ -105,6 +105,13 @@ export abstract class ViewBase extends AngularDisposable implements IModelView { this.queueAction(componentId, (component) => component.setLayout(layout)); } + setItemLayout(containerId: string, itemConfig: IItemConfig): void { + let childDescriptor = this.modelStore.getComponentDescriptor(itemConfig.componentShape.id); + this.queueAction(containerId, (component) => { + component.setItemLayout(childDescriptor, itemConfig.config); + }); + } + setProperties(componentId: string, properties: { [key: string]: any; }): void { if (!properties) { return;