Merge pull request #2677 from coreos-inc/refactor-cor-tabs-pr

Improve cor-tabs Architecture and API
This commit is contained in:
Alec Merdler 2017-06-02 19:53:59 -07:00 committed by GitHub
commit ed48dfd599
32 changed files with 629 additions and 326 deletions

View file

@ -6,7 +6,7 @@
<div class="cor-loader"></div>
</div>
<div class="co-tab-modal-body" ng-show="!credentials.loading">
<cor-tab-panel remember-cookie="quay.credentialsTab" vertical-tabs="true">
<cor-tab-panel orientation="vertical" cor-cookie-tabs="quay.credentialsTab">
<!-- Tabs -->
<cor-tabs>
<cor-tab tab-active="true" tab-id="cred-secret-{{ ::dialogID }}">

View file

@ -10,7 +10,7 @@
</div>
<!-- Tabs -->
<cor-tab-panel>
<cor-tab-panel cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Description" tab-id="description">
<i class="fa fa-info-circle"></i>

View file

@ -0,0 +1,61 @@
import { CorCookieTabsDirective } from './cor-cookie-tabs.directive';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import { Mock } from 'ts-mocks';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import Spy = jasmine.Spy;
describe("CorCookieTabsDirective", () => {
var directive: CorCookieTabsDirective;
var panelMock: Mock<CorTabPanelComponent>;
var cookieServiceMock: Mock<any>;
var activeTab: BehaviorSubject<string>;
beforeEach(() => {
activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "subscribe").and.returnValue(null);
panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab);
cookieServiceMock = new Mock<any>();
cookieServiceMock.setup(mock => mock.putPermanent).is((cookieName, value) => null);
directive = new CorCookieTabsDirective(panelMock.Object, cookieServiceMock.Object);
directive.cookieName = "quay.credentialsTab";
});
describe("constructor", () => {
it("subscribes to active tab changes", () => {
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
});
it("calls cookie service to put new permanent cookie on active tab changes", () => {
const tabId: string = "description";
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](tabId);
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[0]).toEqual(directive.cookieName);
expect((<Spy>cookieServiceMock.Object.putPermanent).calls.argsFor(0)[1]).toEqual(tabId);
});
});
describe("ngAfterContentInit", () => {
const tabId: string = "description";
beforeEach(() => {
cookieServiceMock.setup(mock => mock.get).is((name) => tabId);
spyOn(activeTab, "next").and.returnValue(null);
});
it("calls cookie service to retrieve initial tab id", () => {
directive.ngAfterContentInit();
expect((<Spy>cookieServiceMock.Object.get).calls.argsFor(0)[0]).toEqual(directive.cookieName);
});
it("emits retrieved tab id as next active tab", () => {
directive.ngAfterContentInit();
expect((<Spy>panelMock.Object.activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
});
});
});

View file

@ -0,0 +1,27 @@
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
/**
* Adds routing capabilities to cor-tab-panel using a browser cookie.
*/
@Directive({
selector: '[corCookieTabs]'
})
export class CorCookieTabsDirective implements AfterContentInit {
@Input('@corCookieTabs') public cookieName: string;
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
@Inject('CookieService') private cookieService: any) {
this.panel.activeTab.subscribe((tab: string) => {
this.cookieService.putPermanent(this.cookieName, tab);
});
}
public ngAfterContentInit(): void {
// Set initial tab
const tabId: string = this.cookieService.get(this.cookieName);
this.panel.activeTab.next(tabId);
}
}

View file

@ -0,0 +1,73 @@
import { CorNavTabsDirective } from './cor-nav-tabs.directive';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import { Mock } from 'ts-mocks';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import Spy = jasmine.Spy;
describe("CorNavTabsDirective", () => {
var directive: CorNavTabsDirective;
var panelMock: Mock<CorTabPanelComponent>;
var $locationMock: Mock<ng.ILocationService>;
var $rootScopeMock: Mock<ng.IRootScopeService>;
var activeTab: BehaviorSubject<string>;
const tabId: string = "description";
beforeEach(() => {
activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "next").and.returnValue(null);
panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab);
$locationMock = new Mock<ng.ILocationService>();
$locationMock.setup(mock => mock.search).is(() => <any>{tab: tabId});
$rootScopeMock = new Mock<ng.IRootScopeService>();
$rootScopeMock.setup(mock => mock.$on);
directive = new CorNavTabsDirective(panelMock.Object, $locationMock.Object, $rootScopeMock.Object);
});
describe("constructor", () => {
it("subscribes to $routeUpdate event on the root scope", () => {
expect((<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[0]).toEqual("$routeUpdate");
});
it("calls location service to retrieve tab id from URL query parameters on route update", () => {
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
});
it("emits retrieved tab id as next active tab on route update", () => {
(<Spy>$rootScopeMock.Object.$on).calls.argsFor(0)[1]();
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
});
});
describe("ngAfterContentInit", () => {
const path: string = "quay.io/repository/devtable/simple";
beforeEach(() => {
$locationMock.setup(mock => mock.path).is(() => <any>path);
});
it("calls location service to retrieve the current URL path and sets panel's base path", () => {
directive.ngAfterContentInit();
expect(panelMock.Object.basePath).toEqual(path);
});
it("calls location service to retrieve tab id from URL query parameters", () => {
directive.ngAfterContentInit();
expect(<Spy>$locationMock.Object.search).toHaveBeenCalled();
});
it("emits retrieved tab id as next active tab", () => {
directive.ngAfterContentInit();
expect((<Spy>activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
});
});
});

View file

@ -0,0 +1,29 @@
import { Directive, Inject, Host, AfterContentInit, Input } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
/**
* Adds routing capabilities to cor-tab-panel, either using URL query parameters, or browser cookie.
*/
@Directive({
selector: '[corNavTabs]'
})
export class CorNavTabsDirective implements AfterContentInit {
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent,
@Inject('$location') private $location: ng.ILocationService,
@Inject('$rootScope') private $rootScope: ng.IRootScopeService) {
this.$rootScope.$on('$routeUpdate', () => {
const tabId: string = this.$location.search()['tab'];
this.panel.activeTab.next(tabId);
});
}
public ngAfterContentInit(): void {
this.panel.basePath = this.$location.path();
// Set initial tab
const tabId: string = this.$location.search()['tab'];
this.panel.activeTab.next(tabId);
}
}

View file

@ -6,10 +6,12 @@ import { Component } from 'ng-metadata/core';
*/
@Component({
selector: 'cor-tab-content',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-content.component.html',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.html',
legacy: {
transclude: true,
replace: true,
}
})
export class CorTabContentComponent {}
export class CorTabContentComponent {
}

View file

@ -1,83 +0,0 @@
import { CorTabComponent } from './cor-tab.component';
import { CorTabPanelComponent } from './cor-tab-panel.component';
/**
* Defines an interface for reading and writing the current tab state.
*/
export interface CorTabCurrentHandler {
getInitialTabId(): string
notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean)
dispose(): void
}
export function CorTabCurrentHandlerFactory(options?: any): CorTabCurrentHandler {
switch (options.type) {
case "cookie":
return new CookieCurrentTabHandler(options.cookieService, options.cookieName);
default:
return new LocationCurrentTabHandler(options.panel, options.$location, options.$rootScope);
}
}
/**
* Reads and writes the tab from the `tab` query parameter in the location.
*/
export class LocationCurrentTabHandler implements CorTabCurrentHandler {
private cancelWatchHandle: Function;
constructor (private panel: CorTabPanelComponent,
private $location: ng.ILocationService,
private $rootScope: ng.IRootScopeService) {
}
private checkLocation(): void {
var specifiedTabId = this.$location.search()['tab'];
var specifiedTab = this.panel.findTab(specifiedTabId);
this.panel.setActiveTab(specifiedTab);
}
public getInitialTabId(): string {
if (!this.cancelWatchHandle) {
this.cancelWatchHandle = this.$rootScope.$on('$routeUpdate', () => this.checkLocation());
}
return this.$location.search()['tab'];
}
public notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean) {
var newSearch = $.extend(this.$location.search(), {});
if (isDefaultTab) {
delete newSearch['tab'];
} else {
newSearch['tab'] = tab.tabId;
}
this.$location.search(newSearch);
}
public dispose(): void {
this.cancelWatchHandle();
}
}
/**
* Reads and writes the tab from a cookie,.
*/
export class CookieCurrentTabHandler implements CorTabCurrentHandler {
constructor (private CookieService: any, private cookieName: string) {}
public getInitialTabId(): string {
return this.CookieService.get(this.cookieName);
}
public notifyTabChanged(tab: CorTabComponent, isDefaultTab: boolean) {
if (isDefaultTab) {
this.CookieService.clear(this.cookieName);
} else {
this.CookieService.putPermanent(this.cookieName, tab.tabId);
}
}
public dispose(): void {}
}

View file

@ -1,31 +0,0 @@
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from './cor-tab-panel.component';
/**
* A component that creates a single tab pane under a cor-tabs component.
*/
@Component({
selector: 'cor-tab-pane',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-pane.component.html',
legacy: {
transclude: true,
}
})
export class CorTabPaneComponent implements OnInit {
@Input('@') public id: string;
// Whether this is the active tab.
private isActiveTab: boolean = false;
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
}
public ngOnInit(): void {
this.parent.addTabPane(this);
}
public changeState(isActive: boolean): void {
this.isActiveTab = isActive;
}
}

View file

@ -0,0 +1,40 @@
import { CorTabPaneComponent } from './cor-tab-pane.component';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import { Mock } from 'ts-mocks';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import Spy = jasmine.Spy;
describe("CorTabPaneComponent", () => {
var component: CorTabPaneComponent;
var panelMock: Mock<CorTabPanelComponent>;
var activeTab: BehaviorSubject<string>;
beforeEach(() => {
activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "subscribe").and.returnValue(null);
panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab);
component = new CorTabPaneComponent(panelMock.Object);
});
describe("ngOnInit", () => {
beforeEach(() => {
panelMock.setup(mock => mock.addTabPane);
});
it("adds self as tab pane to panel", () => {
component.ngOnInit();
expect((<Spy>panelMock.Object.addTabPane).calls.argsFor(0)[0]).toBe(component);
});
it("subscribes to active tab changes", () => {
component.ngOnInit();
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
});
});
});

View file

@ -0,0 +1,32 @@
import { Component, Input, Inject, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
/**
* A component that creates a single tab pane under a cor-tabs component.
*/
@Component({
selector: 'cor-tab-pane',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.html',
legacy: {
transclude: true,
}
})
export class CorTabPaneComponent implements OnInit {
@Input('@') public id: string;
private isActiveTab: boolean = false;
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
}
public ngOnInit(): void {
this.panel.addTabPane(this);
this.panel.activeTab.subscribe((tabId: string) => {
this.isActiveTab = (this.id === tabId);
});
}
}

View file

@ -1,128 +0,0 @@
import { Component, Input, Inject, OnDestroy } from 'ng-metadata/core';
import { CorTabComponent } from './cor-tab.component';
import { CorTabPaneComponent } from './cor-tab-pane.component';
import { CorTabCurrentHandler, LocationCurrentTabHandler, CookieCurrentTabHandler, CorTabCurrentHandlerFactory } from './cor-tab-handlers'
/**
* A component that contains a cor-tabs and handles all of its logic.
*/
@Component({
selector: 'cor-tab-panel',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-panel.component.html',
legacy: {
transclude: true,
}
})
export class CorTabPanelComponent implements OnDestroy {
// If supplied, the currently selected tab will be remembered via the named cookie and not
// the page URL.
@Input('@') public rememberCookie: string;
// If 'true', the tabs will be displayed vertically, as opposed to horizontally.
@Input('<') public verticalTabs: boolean;
// The tabs under this tabs component.
private tabs: CorTabComponent[] = [];
// The tab panes under the tabs component, indexed by the tab id.
private tabPanes: {[id: string]: CorTabPaneComponent} = {};
// The currently active tab, if any.
private activeTab: CorTabComponent = null;
// Whether the initial tab was set.
private initialTabSet: boolean = false;
// The handler to use to read/write the current tab.
private currentTabHandler: CorTabCurrentHandler = null;
constructor(@Inject('$location') private $location: ng.ILocationService,
@Inject('$rootScope') private $rootScope: ng.IRootScopeService,
@Inject('CookieService') private CookieService: any,
@Inject('CorTabCurrentHandlerFactory') private CorTabCurrentHandlerFactory: (Object) => CorTabCurrentHandler) {
}
public ngOnDestroy(): void {
if (this.currentTabHandler) {
this.currentTabHandler.dispose();
}
}
/**
* isVertical returns true if the tabs in this panel are displayed vertically.
*/
public isVertical(): boolean {
return this.verticalTabs;
}
public tabClicked(tab: CorTabComponent): void {
this.setActiveTab(tab);
}
public findTab(tabId: string): CorTabComponent {
if (!this.tabs.length) {
return null;
}
var tab = this.tabs.find(function(current) {
return current.tabId == tabId;
}) || this.tabs[0];
if (!this.tabPanes[tab.tabId]) {
return null;
}
return tab;
}
public setActiveTab(tab: CorTabComponent): void {
if (this.activeTab == tab) {
return;
}
if (this.activeTab != null) {
this.activeTab.changeState(false);
this.tabPanes[this.activeTab.tabId].changeState(false);
}
this.activeTab = tab;
this.activeTab.changeState(true);
this.tabPanes[this.activeTab.tabId].changeState(true);
this.currentTabHandler.notifyTabChanged(tab, this.tabs[0] == tab);
}
public addTab(tab: CorTabComponent): void {
this.tabs.push(tab);
this.checkInitialTab();
}
public addTabPane(tabPane: CorTabPaneComponent): void {
this.tabPanes[tabPane.id] = tabPane;
this.checkInitialTab();
}
private checkInitialTab(): void {
if (this.tabs.length < 1 || this.initialTabSet) {
return;
}
this.currentTabHandler = this.CorTabCurrentHandlerFactory({
type: this.rememberCookie ? 'cookie' : 'location',
cookieService: this.CookieService,
cookeName: this.rememberCookie,
panel: this,
$location: this.$location,
$rootScope: this.$rootScope,
});
var tabId = this.currentTabHandler.getInitialTabId();
var tab = this.findTab(tabId);
if (!tab) {
return;
}
this.initialTabSet = true;
this.setActiveTab(tab);
}
}

View file

@ -0,0 +1,111 @@
import { CorTabPanelComponent } from './cor-tab-panel.component';
import { CorTabComponent } from '../cor-tab/cor-tab.component';
import { SimpleChanges } from 'ng-metadata/core';
import Spy = jasmine.Spy;
describe("CorTabPanelComponent", () => {
var component: CorTabPanelComponent;
beforeEach(() => {
component = new CorTabPanelComponent();
});
describe("ngOnInit", () => {
beforeEach(() => {
spyOn(component.activeTab, "subscribe").and.returnValue(null);
spyOn(component.tabChange, "emit").and.returnValue(null);
});
it("subscribes to active tab changes", () => {
component.ngOnInit();
expect(<Spy>component.activeTab.subscribe).toHaveBeenCalled();
});
it("emits output event for tab change when ", () => {
component.ngOnInit();
const tabId: string = "description";
(<Spy>component.activeTab.subscribe).calls.argsFor(0)[0](tabId);
expect((<Spy>component.tabChange.emit).calls.argsFor(0)[0]).toEqual(tabId);
});
});
describe("ngOnChanges", () => {
var changes: SimpleChanges;
var tabs: CorTabComponent[] = [];
beforeEach(() => {
// Add tabs to panel
tabs.push(new CorTabComponent(component));
tabs.forEach((tab) => component.addTab(tab));
changes = {
'selectedIndex': {
currentValue: 0,
previousValue: null,
isFirstChange: () => false
},
};
spyOn(component.activeTab, "next").and.returnValue(null);
});
it("emits next active tab if 'selectedIndex' input changes and is valid", () => {
component.ngOnChanges(changes);
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tabs[changes['selectedIndex'].currentValue].tabId);
});
it("does nothing if 'selectedIndex' input changed to invalid value", () => {
changes['selectedIndex'].currentValue = 100;
component.ngOnChanges(changes);
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
});
});
describe("addTab", () => {
beforeEach(() => {
spyOn(component.activeTab, "next").and.returnValue(null);
});
it("emits next active tab if it is not set", () => {
const tab: CorTabComponent = new CorTabComponent(component);
component.addTab(tab);
expect((<Spy>component.activeTab.next).calls.argsFor(0)[0]).toEqual(tab.tabId);
});
it("does not emit next active tab if it is already set", () => {
spyOn(component.activeTab, "getValue").and.returnValue("description");
const tab: CorTabComponent = new CorTabComponent(component);
component.addTab(tab);
expect(<Spy>component.activeTab.next).not.toHaveBeenCalled();
});
});
describe("addTabPane", () => {
});
describe("isVertical", () => {
it("returns true if orientation is 'vertical'", () => {
component.orientation = 'vertical';
const isVertical: boolean = component.isVertical();
expect(isVertical).toBe(true);
});
it("returns false if orientation is not 'vertical'", () => {
const isVertical: boolean = component.isVertical();
expect(isVertical).toBe(false);
});
});
});

View file

@ -0,0 +1,60 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges, OnInit } from 'ng-metadata/core';
import { CorTabComponent } from '../cor-tab/cor-tab.component';
import { CorTabPaneComponent } from '../cor-tab-pane/cor-tab-pane.component';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
/**
* A component that contains a cor-tabs and handles all of its logic.
*/
@Component({
selector: 'cor-tab-panel',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.html',
legacy: {
transclude: true
}
})
export class CorTabPanelComponent implements OnInit, OnChanges {
@Input('@') public orientation: 'horizontal' | 'vertical' = 'horizontal';
@Output() public tabChange: EventEmitter<string> = new EventEmitter();
public basePath: string;
public activeTab: BehaviorSubject<string> = new BehaviorSubject(null);
private tabs: CorTabComponent[] = [];
private tabPanes: {[id: string]: CorTabPaneComponent} = {};
public ngOnInit(): void {
this.activeTab.subscribe((tabId: string) => {
this.tabChange.emit(tabId);
});
}
public ngOnChanges(changes: SimpleChanges): void {
switch (Object.keys(changes)[0]) {
case 'selectedIndex':
if (this.tabs.length > changes['selectedIndex'].currentValue) {
this.activeTab.next(this.tabs[changes['selectedIndex'].currentValue].tabId);
}
break;
}
}
public addTab(tab: CorTabComponent): void {
this.tabs.push(tab);
if (!this.activeTab.getValue()) {
this.activeTab.next(this.tabs[0].tabId);
}
}
public addTabPane(tabPane: CorTabPaneComponent): void {
this.tabPanes[tabPane.id] = tabPane;
}
public isVertical(): boolean {
return this.orientation == 'vertical';
}
}

View file

@ -1,11 +0,0 @@
<li class="cor-tab-itself" ng-class="{'active': $ctrl.isActive, 'co-top-tab': !$ctrl.parent.isVertical()}">
<a ng-click="$ctrl.tabClicked()">
<span class="cor-tab-icon"
data-title="{{ ::($ctrl.parent.isVertical() ? $ctrl.tabTitle : '') }}"
data-placement="right"
data-container="body"
style="display: inline-block"
bs-tooltip><span ng-transclude/></span><span class="horizontal-label">{{ ::$ctrl.tabTitle }}</span>
</span>
</a>
</li>

View file

@ -1,46 +0,0 @@
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from './cor-tab-panel.component';
/**
* A component that creates a single tab under a cor-tabs component.
*/
@Component({
selector: 'cor-tab',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab.component.html',
legacy: {
transclude: true,
}
})
export class CorTabComponent implements OnInit {
@Input('@') public tabId: string;
@Input('@') public tabTitle: string;
@Output() public tabInit: EventEmitter<any> = new EventEmitter();
@Output() public tabShown: EventEmitter<any> = new EventEmitter();
@Output() public tabHidden: EventEmitter<any> = new EventEmitter();
// Whether this is the active tab.
private isActive: boolean = false;
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
}
public ngOnInit(): void {
this.parent.addTab(this);
}
public changeState(isActive: boolean): void {
this.isActive = isActive;
if (isActive) {
this.tabInit.emit({});
this.tabShown.emit({});
} else {
this.tabHidden.emit({});
}
}
private tabClicked(): void {
this.parent.tabClicked(this);
}
}

View file

@ -0,0 +1,13 @@
<li class="cor-tab-itself" ng-class="{'active': $ctrl.isActive, 'co-top-tab': !$ctrl.parent.isVertical()}">
<a href="{{ $ctrl.panel.basePath ? $ctrl.panel.basePath + '?tab=' + $ctrl.tabId : '' }}"
ng-click="$ctrl.tabClicked($event)">
<span class="cor-tab-icon"
data-title="{{ ::($ctrl.panel.isVertical() ? $ctrl.tabTitle : '') }}"
data-placement="right"
data-container="body"
style="display: inline-block"
bs-tooltip>
<span ng-transclude /><span class="horizontal-label">{{ ::$ctrl.tabTitle }}</span>
</span>
</a>
</li>

View file

@ -0,0 +1,76 @@
import { CorTabComponent } from './cor-tab.component';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
import { Mock } from 'ts-mocks';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import Spy = jasmine.Spy;
describe("CorTabComponent", () => {
var component: CorTabComponent;
var panelMock: Mock<CorTabPanelComponent>;
var activeTab: BehaviorSubject<string>;
beforeEach(() => {
activeTab = new BehaviorSubject<string>(null);
spyOn(activeTab, "subscribe").and.returnValue(null);
panelMock = new Mock<CorTabPanelComponent>();
panelMock.setup(mock => mock.activeTab).is(activeTab);
component = new CorTabComponent(panelMock.Object);
});
describe("ngOnInit", () => {
beforeEach(() => {
panelMock.setup(mock => mock.addTab);
spyOn(component.tabInit, "emit").and.returnValue(null);
spyOn(component.tabShow, "emit").and.returnValue(null);
spyOn(component.tabHide, "emit").and.returnValue(null);
component.tabId = "description";
});
it("subscribes to active tab changes", () => {
component.ngOnInit();
expect((<Spy>panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
});
it("emits output event for tab init if it is new active tab", () => {
component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
expect(<Spy>component.tabInit.emit).toHaveBeenCalled();
});
it("emits output event for tab show if it is new active tab", () => {
component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
expect(<Spy>component.tabShow.emit).toHaveBeenCalled();
});
it("emits output event for tab hide if active tab changes to different tab", () => {
const newTabId: string = component.tabId.split('').reverse().join('');
component.ngOnInit();
// Call twice, first time to set 'isActive' to true
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
expect(<Spy>component.tabHide.emit).toHaveBeenCalled();
});
it("does not emit output event for tab hide if was not previously active tab", () => {
const newTabId: string = component.tabId.split('').reverse().join('');
component.ngOnInit();
(<Spy>panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
expect(<Spy>component.tabHide.emit).not.toHaveBeenCalled();
});
it("adds self as tab to panel", () => {
component.ngOnInit();
expect((<Spy>panelMock.Object.addTab).calls.argsFor(0)[0]).toBe(component);
});
});
});

View file

@ -0,0 +1,51 @@
import { Component, Input, Output, Inject, EventEmitter, Host, OnInit } from 'ng-metadata/core';
import { CorTabPanelComponent } from '../cor-tab-panel/cor-tab-panel.component';
/**
* A component that creates a single tab under a cor-tabs component.
*/
@Component({
selector: 'cor-tab',
templateUrl: '/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.html',
legacy: {
transclude: true,
}
})
export class CorTabComponent implements OnInit {
@Input('@') public tabId: string;
@Input('@') public tabTitle: string;
@Output() public tabInit: EventEmitter<any> = new EventEmitter();
@Output() public tabShow: EventEmitter<any> = new EventEmitter();
@Output() public tabHide: EventEmitter<any> = new EventEmitter();
private isActive: boolean = false;
constructor(@Host() @Inject(CorTabPanelComponent) private panel: CorTabPanelComponent) {
}
public ngOnInit(): void {
this.panel.activeTab.subscribe((tabId: string) => {
if (!this.isActive && this.tabId === tabId) {
this.isActive = true;
this.tabInit.emit({});
this.tabShow.emit({});
} else if (this.isActive && this.tabId !== tabId) {
this.isActive = false;
this.tabHide.emit({});
}
});
this.panel.addTab(this);
}
private tabClicked(event: MouseEvent): void {
if (!this.panel.basePath) {
event.preventDefault();
this.panel.activeTab.next(this.tabId);
}
}
}

View file

@ -1,6 +1,5 @@
import { Component, Input, Output, Inject, EventEmitter, Host } from 'ng-metadata/core';
import { CorTabComponent } from './cor-tab.component';
import { CorTabPanelComponent } from './cor-tab-panel.component';
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
/**
@ -14,10 +13,11 @@ import { CorTabPanelComponent } from './cor-tab-panel.component';
}
})
export class CorTabsComponent {
// If true, the tabs are in a closed state. Only applies in the mobile view.
private isClosed: boolean = true;
constructor(@Host() @Inject(CorTabPanelComponent) private parent: CorTabPanelComponent) {
}
private toggleClosed(e): void {

View file

@ -0,0 +1,33 @@
import { NgModule } from 'ng-metadata/core'
import { CorTabsComponent } from './cor-tabs.component';
import { CorTabComponent } from './cor-tab/cor-tab.component';
import { CorNavTabsDirective } from './cor-nav-tabs/cor-nav-tabs.directive';
import { CorTabContentComponent } from './cor-tab-content/cor-tab-content.component';
import { CorTabPaneComponent } from './cor-tab-pane/cor-tab-pane.component';
import { CorTabPanelComponent } from './cor-tab-panel/cor-tab-panel.component';
import { CorCookieTabsDirective } from './cor-cookie-tabs/cor-cookie-tabs.directive';
/**
* Module containing everything needed for cor-tabs.
*/
@NgModule({
imports: [
],
declarations: [
CorNavTabsDirective,
CorTabComponent,
CorTabContentComponent,
CorTabPaneComponent,
CorTabPanelComponent,
CorTabsComponent,
CorCookieTabsDirective,
],
providers: [
]
})
export class CorTabsModule {
}

View file

@ -11,12 +11,6 @@ import { AppPublicViewComponent } from './directives/ui/app-public-view/app-publ
import { VisibilityIndicatorComponent } from './directives/ui/visibility-indicator/visibility-indicator.component';
import { CorTableComponent } from './directives/ui/cor-table/cor-table.component';
import { CorTableColumn } from './directives/ui/cor-table/cor-table-col.component';
import { CorTabPanelComponent } from './directives/ui/cor-tabs/cor-tab-panel.component';
import { CorTabContentComponent } from './directives/ui/cor-tabs/cor-tab-content.component';
import { CorTabsComponent } from './directives/ui/cor-tabs/cor-tabs.component';
import { CorTabComponent } from './directives/ui/cor-tabs/cor-tab.component';
import { CorTabPaneComponent } from './directives/ui/cor-tabs/cor-tab-pane.component';
import { CorTabCurrentHandlerFactory } from './directives/ui/cor-tabs/cor-tab-handlers';
import { ChannelIconComponent } from './directives/ui/channel-icon/channel-icon.component';
import { TagSigningDisplayComponent } from './directives/ui/tag-signing-display/tag-signing-display.component';
import { RepositorySigningConfigComponent } from './directives/ui/repository-signing-config/repository-signing-config.component';
@ -37,6 +31,7 @@ import { MarkdownEditorComponent } from './directives/ui/markdown/markdown-edito
import { BrowserPlatform, browserPlatform } from './constants/platform.constant';
import { ManageTriggerComponent } from './directives/ui/manage-trigger/manage-trigger.component';
import { ClipboardCopyDirective } from './directives/ui/clipboard-copy/clipboard-copy.directive';
import { CorTabsModule } from './directives/ui/cor-tabs/cor-tabs.module';
import { Converter, ConverterOptions } from 'showdown';
import * as Clipboard from 'clipboard';
@ -48,6 +43,7 @@ import * as Clipboard from 'clipboard';
imports: [
QuayRoutesModule,
QuayConfigModule,
CorTabsModule,
],
declarations: [
RegexMatchViewComponent,
@ -71,11 +67,6 @@ import * as Clipboard from 'clipboard';
MarkdownEditorComponent,
SearchBoxComponent,
TypeaheadDirective,
CorTabPanelComponent,
CorTabContentComponent,
CorTabsComponent,
CorTabComponent,
CorTabPaneComponent,
ManageTriggerComponent,
ClipboardCopyDirective,
],
@ -88,7 +79,6 @@ import * as Clipboard from 'clipboard';
{provide: 'fileReaderFactory', useValue: () => new FileReader()},
{provide: 'markdownConverterFactory', useValue: (options?: ConverterOptions) => new Converter(options)},
{provide: 'BrowserPlatform', useValue: browserPlatform},
{provide: 'CorTabCurrentHandlerFactory', useValue: CorTabCurrentHandlerFactory},
{provide: 'clipboardFactory', useValue: (trigger, options) => new Clipboard(trigger, options)},
],
})

View file

@ -15,7 +15,7 @@
</span>
</div>
<cor-tab-panel vertical-tabs="true">
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Layers" tab-id="layers">
<i class="fa ci-layers"></i>

View file

@ -20,7 +20,7 @@
</div>
</div>
<cor-tab-panel vertical-tabs="true">
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Settings" tab-id="settings">
<i class="fa fa-gear"></i>

View file

@ -20,7 +20,9 @@
<div class="repo-list-view padded" namespaces="[organization]">&nbsp;</div>
</div>
<cor-tab-panel ng-if="!user.anonymous && isMember" vertical-tabs="true">
<cor-tab-panel ng-if="!user.anonymous && isMember"
orientation="vertical"
cor-nav-tabs>
<cor-tabs quay-show="isMember">
<cor-tab tab-active="true" tab-title="Repositories" tab-id="repos">
<i class="fa fa-hdd-o"></i>

View file

@ -27,7 +27,7 @@
</span>
</div>
<cor-tab-panel vertical-tabs="true">
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Information" tab-id="info"
tab-init="showInfo()">
@ -111,7 +111,7 @@
is-enabled="settingsShown"></div>
</cor-tab-pane>
</cor-tab-content>
</cor-tabs>
</cor-tab-panel>
</div>
</div>
</div>

View file

@ -22,7 +22,7 @@
<span class="cor-title-content">Quay Enterprise Management</span>
</div>
<cor-tab-panel vertical-tabs="true">
<cor-tab-panel orientation="vertical" cor-nav-tabs>
<cor-tabs>
<cor-tab tab-title="Manage Users"
tab-id="users" tab-init="loadUsers()">

View file

@ -20,7 +20,9 @@
<div class="repo-list-view padded" namespaces="[context.viewuser]">&nbsp;</div>
</div>
<cor-tab-panel ng-if="context.viewuser.is_me" vertical-tabs="true">
<cor-tab-panel ng-if="context.viewuser.is_me"
orientation="vertical"
cor-nav-tabs>
<cor-tabs quay-show="context.viewuser.is_me">
<cor-tab tab-active="true" tab-title="Repositories" tab-id="repos">
<i class="fa fa-hdd-o"></i>

Binary file not shown.