diff --git a/static/directives/credentials-dialog.html b/static/directives/credentials-dialog.html
index c491741bb..2707c0971 100644
--- a/static/directives/credentials-dialog.html
+++ b/static/directives/credentials-dialog.html
@@ -6,7 +6,7 @@
-
+
diff --git a/static/js/directives/ui/app-public-view/app-public-view.component.html b/static/js/directives/ui/app-public-view/app-public-view.component.html
index 753ac6293..dbb881347 100644
--- a/static/js/directives/ui/app-public-view/app-public-view.component.html
+++ b/static/js/directives/ui/app-public-view/app-public-view.component.html
@@ -10,7 +10,7 @@
-
+
diff --git a/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.spec.ts b/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.spec.ts
new file mode 100644
index 000000000..b7e237998
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.spec.ts
@@ -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;
+ var cookieServiceMock: Mock;
+ var activeTab: BehaviorSubject;
+
+ beforeEach(() => {
+ activeTab = new BehaviorSubject(null);
+ spyOn(activeTab, "subscribe").and.returnValue(null);
+ panelMock = new Mock();
+ panelMock.setup(mock => mock.activeTab).is(activeTab);
+ cookieServiceMock = new Mock();
+ 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((panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
+ });
+
+ it("calls cookie service to put new permanent cookie on active tab changes", () => {
+ const tabId: string = "description";
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](tabId);
+
+ expect((cookieServiceMock.Object.putPermanent).calls.argsFor(0)[0]).toEqual(directive.cookieName);
+ expect((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((cookieServiceMock.Object.get).calls.argsFor(0)[0]).toEqual(directive.cookieName);
+ });
+
+ it("emits retrieved tab id as next active tab", () => {
+ directive.ngAfterContentInit();
+
+ expect((panelMock.Object.activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
+ });
+ });
+});
diff --git a/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.ts b/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.ts
new file mode 100644
index 000000000..2cd429d48
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-cookie-tabs/cor-cookie-tabs.directive.ts
@@ -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);
+ }
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.spec.ts b/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.spec.ts
new file mode 100644
index 000000000..5676facdd
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.spec.ts
@@ -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;
+ var $locationMock: Mock;
+ var $rootScopeMock: Mock;
+ var activeTab: BehaviorSubject;
+ const tabId: string = "description";
+
+ beforeEach(() => {
+ activeTab = new BehaviorSubject(null);
+ spyOn(activeTab, "next").and.returnValue(null);
+ panelMock = new Mock();
+ panelMock.setup(mock => mock.activeTab).is(activeTab);
+ $locationMock = new Mock();
+ $locationMock.setup(mock => mock.search).is(() => {tab: tabId});
+ $rootScopeMock = new Mock();
+ $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(($rootScopeMock.Object.$on).calls.argsFor(0)[0]).toEqual("$routeUpdate");
+ });
+
+ it("calls location service to retrieve tab id from URL query parameters on route update", () => {
+ ($rootScopeMock.Object.$on).calls.argsFor(0)[1]();
+
+ expect($locationMock.Object.search).toHaveBeenCalled();
+ });
+
+ it("emits retrieved tab id as next active tab on route update", () => {
+ ($rootScopeMock.Object.$on).calls.argsFor(0)[1]();
+
+ expect((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(() => 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($locationMock.Object.search).toHaveBeenCalled();
+ });
+
+ it("emits retrieved tab id as next active tab", () => {
+ directive.ngAfterContentInit();
+
+ expect((activeTab.next).calls.argsFor(0)[0]).toEqual(tabId);
+ });
+ });
+});
diff --git a/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.ts b/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.ts
new file mode 100644
index 000000000..2701f7058
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-nav-tabs/cor-nav-tabs.directive.ts
@@ -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);
+ }
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-content.component.html b/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.html
similarity index 100%
rename from static/js/directives/ui/cor-tabs/cor-tab-content.component.html
rename to static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.html
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-content.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.ts
similarity index 80%
rename from static/js/directives/ui/cor-tabs/cor-tab-content.component.ts
rename to static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.ts
index a79d7db5d..5877a441a 100644
--- a/static/js/directives/ui/cor-tabs/cor-tab-content.component.ts
+++ b/static/js/directives/ui/cor-tabs/cor-tab-content/cor-tab-content.component.ts
@@ -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 {
+
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-handlers.ts b/static/js/directives/ui/cor-tabs/cor-tab-handlers.ts
deleted file mode 100644
index a9eba4945..000000000
--- a/static/js/directives/ui/cor-tabs/cor-tab-handlers.ts
+++ /dev/null
@@ -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 {}
-}
\ No newline at end of file
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-pane.component.ts
deleted file mode 100644
index 03fcc04c9..000000000
--- a/static/js/directives/ui/cor-tabs/cor-tab-pane.component.ts
+++ /dev/null
@@ -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;
- }
-}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane.component.html b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.html
similarity index 100%
rename from static/js/directives/ui/cor-tabs/cor-tab-pane.component.html
rename to static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.html
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts
new file mode 100644
index 000000000..46441848a
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.spec.ts
@@ -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;
+ var activeTab: BehaviorSubject;
+
+ beforeEach(() => {
+ activeTab = new BehaviorSubject(null);
+ spyOn(activeTab, "subscribe").and.returnValue(null);
+ panelMock = new Mock();
+ 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((panelMock.Object.addTabPane).calls.argsFor(0)[0]).toBe(component);
+ });
+
+ it("subscribes to active tab changes", () => {
+ component.ngOnInit();
+
+ expect((panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts
new file mode 100644
index 000000000..95867aa3b
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab-pane/cor-tab-pane.component.ts
@@ -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);
+ });
+ }
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-panel.component.ts
deleted file mode 100644
index dc24c39b0..000000000
--- a/static/js/directives/ui/cor-tabs/cor-tab-panel.component.ts
+++ /dev/null
@@ -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);
- }
-}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel.component.html b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.html
similarity index 100%
rename from static/js/directives/ui/cor-tabs/cor-tab-panel.component.html
rename to static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.html
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts
new file mode 100644
index 000000000..c26634472
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.spec.ts
@@ -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(component.activeTab.subscribe).toHaveBeenCalled();
+ });
+
+ it("emits output event for tab change when ", () => {
+ component.ngOnInit();
+ const tabId: string = "description";
+ (component.activeTab.subscribe).calls.argsFor(0)[0](tabId);
+
+ expect((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((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(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((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(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);
+ });
+ });
+});
diff --git a/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts
new file mode 100644
index 000000000..3d3321054
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab-panel/cor-tab-panel.component.ts
@@ -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 = new EventEmitter();
+
+ public basePath: string;
+ public activeTab: BehaviorSubject = 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';
+ }
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab.component.html b/static/js/directives/ui/cor-tabs/cor-tab.component.html
deleted file mode 100644
index 56b97fccf..000000000
--- a/static/js/directives/ui/cor-tabs/cor-tab.component.html
+++ /dev/null
@@ -1,11 +0,0 @@
-
-
- {{ ::$ctrl.tabTitle }}
-
-
-
diff --git a/static/js/directives/ui/cor-tabs/cor-tab.component.ts b/static/js/directives/ui/cor-tabs/cor-tab.component.ts
deleted file mode 100644
index 7697d757b..000000000
--- a/static/js/directives/ui/cor-tabs/cor-tab.component.ts
+++ /dev/null
@@ -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 = new EventEmitter();
- @Output() public tabShown: EventEmitter = new EventEmitter();
- @Output() public tabHidden: EventEmitter = 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);
- }
-}
diff --git a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.html b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.html
new file mode 100644
index 000000000..c10fbf0ef
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.html
@@ -0,0 +1,13 @@
+
+
+
+ {{ ::$ctrl.tabTitle }}
+
+
+
diff --git a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts
new file mode 100644
index 000000000..ead20c980
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.spec.ts
@@ -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;
+ var activeTab: BehaviorSubject;
+
+ beforeEach(() => {
+ activeTab = new BehaviorSubject(null);
+ spyOn(activeTab, "subscribe").and.returnValue(null);
+ panelMock = new Mock();
+ 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((panelMock.Object.activeTab.subscribe)).toHaveBeenCalled();
+ });
+
+ it("emits output event for tab init if it is new active tab", () => {
+ component.ngOnInit();
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
+
+ expect(component.tabInit.emit).toHaveBeenCalled();
+ });
+
+ it("emits output event for tab show if it is new active tab", () => {
+ component.ngOnInit();
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
+
+ expect(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
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](component.tabId);
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
+
+ expect(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();
+ (panelMock.Object.activeTab.subscribe).calls.argsFor(0)[0](newTabId);
+
+ expect(component.tabHide.emit).not.toHaveBeenCalled();
+ });
+
+ it("adds self as tab to panel", () => {
+ component.ngOnInit();
+
+ expect((panelMock.Object.addTab).calls.argsFor(0)[0]).toBe(component);
+ });
+ });
+});
diff --git a/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts
new file mode 100644
index 000000000..2bb832194
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tab/cor-tab.component.ts
@@ -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 = new EventEmitter();
+ @Output() public tabShow: EventEmitter = new EventEmitter();
+ @Output() public tabHide: EventEmitter = 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);
+ }
+ }
+}
diff --git a/static/js/directives/ui/cor-tabs/cor-tabs.component.ts b/static/js/directives/ui/cor-tabs/cor-tabs.component.ts
index 7c7c60a61..85b709d09 100644
--- a/static/js/directives/ui/cor-tabs/cor-tabs.component.ts
+++ b/static/js/directives/ui/cor-tabs/cor-tabs.component.ts
@@ -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 {
diff --git a/static/js/directives/ui/cor-tabs/cor-tabs.module.ts b/static/js/directives/ui/cor-tabs/cor-tabs.module.ts
new file mode 100644
index 000000000..02570a1b0
--- /dev/null
+++ b/static/js/directives/ui/cor-tabs/cor-tabs.module.ts
@@ -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 {
+
+}
diff --git a/static/js/quay.module.ts b/static/js/quay.module.ts
index cce2b51da..c274af627 100644
--- a/static/js/quay.module.ts
+++ b/static/js/quay.module.ts
@@ -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)},
],
})
diff --git a/static/partials/image-view.html b/static/partials/image-view.html
index 231a8ce0e..770965e9e 100644
--- a/static/partials/image-view.html
+++ b/static/partials/image-view.html
@@ -15,7 +15,7 @@
-
+
diff --git a/static/partials/manage-application.html b/static/partials/manage-application.html
index 85c986c4a..52264bd45 100644
--- a/static/partials/manage-application.html
+++ b/static/partials/manage-application.html
@@ -20,7 +20,7 @@
-
+
diff --git a/static/partials/org-view.html b/static/partials/org-view.html
index 8cfef7e10..8dad36c5e 100644
--- a/static/partials/org-view.html
+++ b/static/partials/org-view.html
@@ -20,7 +20,9 @@
-
+
diff --git a/static/partials/repo-view.html b/static/partials/repo-view.html
index af949ed0d..dbbb6e28a 100644
--- a/static/partials/repo-view.html
+++ b/static/partials/repo-view.html
@@ -27,7 +27,7 @@
-
+
@@ -111,7 +111,7 @@
is-enabled="settingsShown">
-
-
+
+
diff --git a/static/partials/super-user.html b/static/partials/super-user.html
index 7ade80b23..d8f7d2154 100644
--- a/static/partials/super-user.html
+++ b/static/partials/super-user.html
@@ -22,7 +22,7 @@
Quay Enterprise Management
-
+
diff --git a/static/partials/user-view.html b/static/partials/user-view.html
index 4fc2e620c..e9ea15f99 100644
--- a/static/partials/user-view.html
+++ b/static/partials/user-view.html
@@ -20,7 +20,9 @@
-
+
diff --git a/test/data/test.db b/test/data/test.db
index 6514b4379..2dde19399 100644
Binary files a/test/data/test.db and b/test/data/test.db differ