Support icons in panel (#895)

* support icons in panel

* formatting

* address Smitha comments

* address comments
This commit is contained in:
Abbie Petchtes
2018-03-13 13:48:17 -07:00
committed by GitHub
parent 383d74ceb4
commit f3c7b2416b
10 changed files with 96 additions and 19 deletions

View File

@@ -35,6 +35,7 @@ panel {
.tabbedPanel .tabList .tab { .tabbedPanel .tabList .tab {
cursor: pointer; cursor: pointer;
margin: auto;
} }
.tabbedPanel .tabList .tab .tabLabel { .tabbedPanel .tabList .tab .tabLabel {
@@ -43,6 +44,16 @@ panel {
padding-bottom: 4px; padding-bottom: 4px;
} }
.tabbedPanel .tabList .tab .tabLabel.icon {
background-repeat: no-repeat;
background-position: center 10px;
background-size: 25px;
line-height: 15px;
padding-top: 40px;
display: inline-block;
text-transform: none;
}
.tabbedPanel .tabList .tab-header { .tabbedPanel .tabList .tab-header {
padding-left: 10px; padding-left: 10px;
padding-right: 10px; padding-right: 10px;

View File

@@ -24,6 +24,7 @@ export interface IPanelOptions {
*/ */
showTabsWhenOne?: boolean; showTabsWhenOne?: boolean;
layout?: NavigationBarLayout; layout?: NavigationBarLayout;
showIcon?: boolean;
} }
export enum NavigationBarLayout { export enum NavigationBarLayout {
@@ -33,7 +34,8 @@ export enum NavigationBarLayout {
const defaultOptions: IPanelOptions = { const defaultOptions: IPanelOptions = {
showTabsWhenOne: true, showTabsWhenOne: true,
layout: NavigationBarLayout.horizontal layout: NavigationBarLayout.horizontal,
showIcon: false
}; };
const verticalLayout = 'vertical'; const verticalLayout = 'vertical';
@@ -49,7 +51,7 @@ let idPool = 0;
<div *ngIf="!options.showTabsWhenOne ? _tabs.length !== 1 : true" class="composite title" #titleContainer> <div *ngIf="!options.showTabsWhenOne ? _tabs.length !== 1 : true" class="composite title" #titleContainer>
<div class="tabList" #tabList> <div class="tabList" #tabList>
<div *ngFor="let tab of _tabs"> <div *ngFor="let tab of _tabs">
<tab-header [tab]="tab" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'> </tab-header> <tab-header [tab]="tab" [showIcon]="options.showIcon" (onSelectTab)='selectTab($event)' (onCloseTab)='closeTab($event)'> </tab-header>
</div> </div>
</div> </div>
<div class="title-actions"> <div class="title-actions">

View File

@@ -23,6 +23,7 @@ export class TabComponent implements OnDestroy {
@Input() public title: string; @Input() public title: string;
@Input() public canClose: boolean; @Input() public canClose: boolean;
@Input() public actions: Array<Action>; @Input() public actions: Array<Action>;
@Input() public iconClass: string;
public _active = false; public _active = false;
@Input() public identifier: string; @Input() public identifier: string;
@Input() private visibilityType: 'if' | 'visibility' = 'if'; @Input() private visibilityType: 'if' | 'visibility' = 'if';

View File

@@ -21,7 +21,7 @@ import { CloseTabAction } from './tabActions';
template: ` template: `
<div #actionHeader class="tab-header" style="display: flex; flex: 0 0; flex-direction: row;" [class.active]="tab.active" tabindex="0" (keyup)="onKey($event)"> <div #actionHeader class="tab-header" style="display: flex; flex: 0 0; flex-direction: row;" [class.active]="tab.active" tabindex="0" (keyup)="onKey($event)">
<span class="tab" (click)="selectTab(tab)"> <span class="tab" (click)="selectTab(tab)">
<a class="tabLabel" [class.active]="tab.active"> <a class="tabLabel" [class.active]="tab.active" #tabLabel>
{{tab.title}} {{tab.title}}
</a> </a>
</span> </span>
@@ -31,6 +31,7 @@ import { CloseTabAction } from './tabActions';
}) })
export class TabHeaderComponent extends Disposable implements AfterContentInit, OnDestroy { export class TabHeaderComponent extends Disposable implements AfterContentInit, OnDestroy {
@Input() public tab: TabComponent; @Input() public tab: TabComponent;
@Input() public showIcon: boolean;
@Output() public onSelectTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>(); @Output() public onSelectTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>();
@Output() public onCloseTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>(); @Output() public onCloseTab: EventEmitter<TabComponent> = new EventEmitter<TabComponent>();
@@ -38,11 +39,13 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit,
@ViewChild('actionHeader', { read: ElementRef }) private _actionHeaderRef: ElementRef; @ViewChild('actionHeader', { read: ElementRef }) private _actionHeaderRef: ElementRef;
@ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef; @ViewChild('actionbar', { read: ElementRef }) private _actionbarRef: ElementRef;
@ViewChild('tabLabel', { read: ElementRef }) private _tabLabelRef: ElementRef;
constructor() { constructor() {
super(); super();
} }
ngAfterContentInit(): void { ngAfterContentInit(): void {
if (this.tab.canClose || this.tab.actions) {
this._actionbar = new ActionBar(this._actionbarRef.nativeElement); this._actionbar = new ActionBar(this._actionbarRef.nativeElement);
if (this.tab.actions) { if (this.tab.actions) {
this._actionbar.push(this.tab.actions, { icon: true, label: false }); this._actionbar.push(this.tab.actions, { icon: true, label: false });
@@ -53,6 +56,15 @@ export class TabHeaderComponent extends Disposable implements AfterContentInit,
} }
} }
let tabLabelcontainer = this._tabLabelRef.nativeElement as HTMLElement;
if (this.showIcon && this.tab.iconClass) {
tabLabelcontainer.className = 'tabLabel icon';
tabLabelcontainer.classList.add(this.tab.iconClass);
} else {
tabLabelcontainer.className = 'tabLabel';
}
}
ngOnDestroy() { ngOnDestroy() {
if (this._actionbar) { if (this._actionbar) {
this._actionbar.dispose(); this._actionbar.dispose();

View File

@@ -9,7 +9,7 @@ import * as types from 'vs/base/common/types';
import { registerTab } from 'sql/platform/dashboard/common/dashboardRegistry'; import { registerTab } from 'sql/platform/dashboard/common/dashboardRegistry';
import { generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; import { generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { NAV_SECTION, validateNavSectionContribution } from 'sql/parts/dashboard/containers/dashboardNavSection.contribution'; import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/parts/dashboard/containers/dashboardNavSection.contribution';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution'; import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution'; import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution';
@@ -130,7 +130,7 @@ ExtensionsRegistry.registerExtensionPoint<IDashboardTabContrib | IDashboardTabCo
result = validateGridContainerContribution(extension, containerValue); result = validateGridContainerContribution(extension, containerValue);
break; break;
case NAV_SECTION: case NAV_SECTION:
result = validateNavSectionContribution(extension, containerValue); result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
break; break;
} }

View File

@@ -40,6 +40,7 @@ export interface TabConfig extends IDashboardTab {
editable: boolean; editable: boolean;
canClose: boolean; canClose: boolean;
actions?: Array<Action>; actions?: Array<Action>;
iconClass?: string;
} }
export type IUserFriendlyIcon = string | { light: string; dark: string; }; export type IUserFriendlyIcon = string | { light: string; dark: string; };
@@ -47,6 +48,7 @@ export type IUserFriendlyIcon = string | { light: string; dark: string; };
export interface NavSectionConfig { export interface NavSectionConfig {
id: string; id: string;
title: string; title: string;
iconClass?: string;
icon?: IUserFriendlyIcon; icon?: IUserFriendlyIcon;
container: object; container: object;
} }

View File

@@ -10,7 +10,7 @@ import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri'; import URI from 'vs/base/common/uri';
import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; import { registerContainer, generateContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { NAV_SECTION, validateNavSectionContribution } from 'sql/parts/dashboard/containers/dashboardNavSection.contribution'; import { NAV_SECTION, validateNavSectionContributionAndRegisterIcon } from 'sql/parts/dashboard/containers/dashboardNavSection.contribution';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution'; import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution'; import { GRID_CONTAINER, validateGridContainerContribution } from 'sql/parts/dashboard/containers/dashboardGridContainer.contribution';
import { WEBVIEW_CONTAINER } from 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution'; import { WEBVIEW_CONTAINER } from 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution';
@@ -91,7 +91,7 @@ ExtensionsRegistry.registerExtensionPoint<IDashboardContainerContrib | IDashboar
result = validateGridContainerContribution(extension, containerValue); result = validateGridContainerContribution(extension, containerValue);
break; break;
case NAV_SECTION: case NAV_SECTION:
result = validateNavSectionContribution(extension, containerValue); result = validateNavSectionContributionAndRegisterIcon(extension, containerValue);
break; break;
} }

View File

@@ -5,7 +5,8 @@
*--------------------------------------------------------------------------------------------*/ *--------------------------------------------------------------------------------------------*/
--> -->
<panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel" (onTabChange)="handleTabChange($event)"> <panel [options]="panelOpt" style="flex: 1 1 auto;" class="dashboard-panel" (onTabChange)="handleTabChange($event)">
<tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize" [identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions"> <tab [visibilityType]="'visibility'" *ngFor="let tab of tabs" [title]="tab.title" class="fullsize"
[identifier]="tab.id" [canClose]="tab.canClose" [actions]="tab.actions" [iconClass]="tab.iconClass">
<dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab"> <dashboard-webview-container *ngIf="getContentType(tab) === 'webview-container'" [tab]="tab">
</dashboard-webview-container> </dashboard-webview-container>
<dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab"> <dashboard-widget-container *ngIf="getContentType(tab) === 'widgets-container'" [tab]="tab">

View File

@@ -64,6 +64,13 @@ export class DashboardNavSection extends DashboardTab implements OnDestroy, OnCh
let navSectionContainers: NavSectionConfig[] = []; let navSectionContainers: NavSectionConfig[] = [];
if (this.tab.container) { if (this.tab.container) {
navSectionContainers = Object.values(this.tab.container)[0]; navSectionContainers = Object.values(this.tab.container)[0];
let hasIcon = true;
navSectionContainers.forEach(navSection => {
if (!navSection.iconClass) {
hasIcon = false;
}
});
this.panelOpt.showIcon = hasIcon;
this.loadNewTabs(navSectionContainers); this.loadNewTabs(navSectionContainers);
} }
} }
@@ -101,13 +108,13 @@ export class DashboardNavSection extends DashboardTab implements OnDestroy, OnCh
configs = cb.apply(this, [configs]); configs = cb.apply(this, [configs]);
}); });
if (key === WIDGETS_CONTAINER) { if (key === WIDGETS_CONTAINER) {
return { id: v.id, title: v.title, container: { 'widgets-container': configs } }; return { id: v.id, title: v.title, container: { 'widgets-container': configs }, iconClass: v.iconClass };
} else { } else {
return { id: v.id, title: v.title, container: { 'grid-container': configs } }; return { id: v.id, title: v.title, container: { 'grid-container': configs }, iconClass: v.iconClass };
} }
} }
return { id: v.id, title: v.title, container: containerResult.container }; return { id: v.id, title: v.title, container: containerResult.container, iconClass: v.iconClass };
}).map(v => { }).map(v => {
let config = v as TabConfig; let config = v as TabConfig;
config.context = this.tab.context; config.context = this.tab.context;

View File

@@ -5,8 +5,12 @@
import { IExtensionPointUser } from 'vs/platform/extensions/common/extensionsRegistry'; import { IExtensionPointUser } from 'vs/platform/extensions/common/extensionsRegistry';
import { IJSONSchema } from 'vs/base/common/jsonSchema'; import { IJSONSchema } from 'vs/base/common/jsonSchema';
import * as nls from 'vs/nls'; import * as nls from 'vs/nls';
import { join } from 'path';
import { createCSSRule } from 'vs/base/browser/dom';
import URI from 'vs/base/common/uri';
import { IdGenerator } from 'vs/base/common/idGenerator';
import { NavSectionConfig } from 'sql/parts/dashboard/common/dashboardWidget'; import { NavSectionConfig, IUserFriendlyIcon } from 'sql/parts/dashboard/common/dashboardWidget';
import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry'; import { registerContainerType, generateNavSectionContainerTypeSchemaProperties } from 'sql/platform/dashboard/common/dashboardContainerRegistry';
import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution'; import { WIDGETS_CONTAINER, validateWidgetContainerContribution } from 'sql/parts/dashboard/containers/dashboardWidgetContainer.contribution';
import { WEBVIEW_CONTAINER } from 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution'; import { WEBVIEW_CONTAINER } from 'sql/parts/dashboard/containers/dashboardWebviewContainer.contribution';
@@ -60,7 +64,39 @@ let NavSectionSchema: IJSONSchema = {
registerContainerType(NAV_SECTION, NavSectionSchema); registerContainerType(NAV_SECTION, NavSectionSchema);
export function validateNavSectionContribution(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean { function isValidIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): boolean {
if (typeof icon === 'undefined') {
return false;
}
if (typeof icon === 'string') {
return true;
} else if (typeof icon.dark === 'string' && typeof icon.light === 'string') {
return true;
}
extension.collector.error(nls.localize('opticon', "property `icon` can be omitted or must be either a string or a literal like `{dark, light}`"));
return false;
}
const ids = new IdGenerator('contrib-dashboardNavSection-icon-');
function createCSSRuleForIcon(icon: IUserFriendlyIcon, extension: IExtensionPointUser<any>): string {
let iconClass: string;
if (icon) {
iconClass = ids.nextId();
if (typeof icon === 'string') {
const path = join(extension.description.extensionFolderPath, icon);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(path).toString()}")`);
} else {
const light = join(extension.description.extensionFolderPath, icon.light);
const dark = join(extension.description.extensionFolderPath, icon.dark);
createCSSRule(`.icon.${iconClass}`, `background-image: url("${URI.file(light).toString()}")`);
createCSSRule(`.vs-dark .icon.${iconClass}, .hc-black .icon.${iconClass}`, `background-image: url("${URI.file(dark).toString()}")`);
}
}
return iconClass;
}
export function validateNavSectionContributionAndRegisterIcon(extension: IExtensionPointUser<any>, navSectionConfigs: NavSectionConfig[]): boolean {
let result = true; let result = true;
navSectionConfigs.forEach(section => { navSectionConfigs.forEach(section => {
if (!section.title) { if (!section.title) {
@@ -78,6 +114,10 @@ export function validateNavSectionContribution(extension: IExtensionPointUser<an
extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.')); extension.collector.error(nls.localize('navSection.moreThanOneDashboardContainersError', 'Exactly 1 dashboard container must be defined per space.'));
} }
if (isValidIcon(section.icon, extension)) {
section.iconClass = createCSSRuleForIcon(section.icon, extension);
}
let containerKey = Object.keys(section.container)[0]; let containerKey = Object.keys(section.container)[0];
let containerValue = Object.values(section.container)[0]; let containerValue = Object.values(section.container)[0];
@@ -93,6 +133,7 @@ export function validateNavSectionContribution(extension: IExtensionPointUser<an
extension.collector.error(nls.localize('navSection.invalidContainer_error', 'NAV_SECTION within NAV_SECTION is an invalid container for extension.')); extension.collector.error(nls.localize('navSection.invalidContainer_error', 'NAV_SECTION within NAV_SECTION is an invalid container for extension.'));
break; break;
} }
}); });
return result; return result;
} }