diff --git a/app/components/certificate/action/add/certificate-create-dialog.component.ts b/app/components/certificate/action/add/certificate-create-dialog.component.ts index 2750fac66c..f2e21b38fd 100644 --- a/app/components/certificate/action/add/certificate-create-dialog.component.ts +++ b/app/components/certificate/action/add/certificate-create-dialog.component.ts @@ -17,6 +17,7 @@ import "./certificate-create-dialog.scss"; export class CertificateCreateDialogComponent { public file: File; public form: FormGroup; + public title = "Add certificate"; constructor( private formBuilder: FormBuilder, diff --git a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.spec.ts b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.spec.ts index eba1fcbbd9..47e911d5c3 100644 --- a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.spec.ts +++ b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.spec.ts @@ -6,6 +6,7 @@ import { List } from "immutable"; import { BehaviorSubject, Observable } from "rxjs"; import { NavigableRecord, PinnableEntity, PinnedEntityType } from "@batch-flask/core"; +import { ContextMenuService } from "@batch-flask/ui"; import { DropdownModule } from "@batch-flask/ui/dropdown"; import { AccountService, PinnedEntityService } from "app/services"; import * as Fixtures from "test/fixture"; @@ -35,6 +36,12 @@ describe("PinnedDropDownComponent", () => { favorites = new BehaviorSubject(List([])); pinServiceSpy = { favorites: favorites.asObservable(), + unPinFavorite: jasmine.createSpy("unPinFavorite").and.callFake((favorite) => { + // remove from fav list + const favArray = favorites.value.toArray(); + favArray.splice(0, 1); + favorites.next(List(favArray)); + }), }; accountServiceSpy = { @@ -52,6 +59,7 @@ describe("PinnedDropDownComponent", () => { providers: [ { provide: AccountService, useValue: accountServiceSpy }, { provide: PinnedEntityService, useValue: pinServiceSpy }, + { provide: ContextMenuService, useValue: null }, ], schemas: [NO_ERRORS_SCHEMA], }); @@ -69,6 +77,8 @@ describe("PinnedDropDownComponent", () => { expect(component.entityType(createPin(PinnedEntityType.Job))).toBe("Batch job"); expect(component.entityType(createPin(PinnedEntityType.Pool))).toBe("Batch pool"); expect(component.entityType(createPin(PinnedEntityType.StorageContainer))).toBe("Storage container"); + expect(component.entityType(createPin(PinnedEntityType.Certificate))).toBe("Batch certificate"); + expect(component.entityType(createPin(PinnedEntityType.JobSchedule))).toBe("Batch job schedule"); expect(component.entityType(createPin(null))).toBe("unknown"); }); @@ -77,6 +87,8 @@ describe("PinnedDropDownComponent", () => { expect(component.entityIcon(createPin(PinnedEntityType.Job))).toBe("fa-tasks"); expect(component.entityIcon(createPin(PinnedEntityType.Pool))).toBe("fa-database"); expect(component.entityIcon(createPin(PinnedEntityType.StorageContainer))).toBe("fa-cloud-upload"); + expect(component.entityIcon(createPin(PinnedEntityType.Certificate))).toBe("fa-certificate"); + expect(component.entityIcon(createPin(PinnedEntityType.JobSchedule))).toBe("fa-calendar"); expect(component.entityIcon(createPin(null))).toBe("fa-question"); }); }); @@ -117,14 +129,15 @@ describe("PinnedDropDownComponent", () => { const items = fixture.debugElement.queryAll(By.css(".dropdown-item")); expect(items.length).toBe(1); expect(items[0].nativeElement.textContent).toContain("my-job-fred"); - expect(items[0].nativeElement.textContent).toContain("Batch job"); - const icon = items[0].query(By.css(".fa.fa-tasks")); - expect(icon.nativeElement).toBeDefined(); + // type is now in icon title + const icon = items[0].query(By.css(".fa.fa-tasks")).nativeElement; + expect(icon).toBeDefined(); + expect(icon.getAttribute("title")).toBe("Batch job"); }); }); - describe("when there are more than one favorites", () => { + describe("when there are more than one favorite", () => { beforeEach(() => { favorites.next(List([ Fixtures.pinnable.create({ @@ -159,11 +172,34 @@ describe("PinnedDropDownComponent", () => { it("pool should show name over id", () => { const items = debugElement.queryAll(By.css(".dropdown-item")); expect(items[0].nativeElement.textContent).toContain("my-job-matt"); - expect(items[0].nativeElement.textContent).toContain("Batch job"); - expect(items[1].nativeElement.textContent).toContain("my-name-is-bob"); expect(items[1].nativeElement.textContent).not.toContain("my-pool-bob"); - expect(items[1].nativeElement.textContent).toContain("Batch pool"); + }); + }); + + describe("when we remove favorites", () => { + beforeEach(() => { + favorites.next(List([ + Fixtures.pinnable.create({ + id: "my-apple", + routerLink: ["/certificates", "my-apple"], + pinnableType: PinnedEntityType.Certificate, + url: "https://myaccount.westus.batch.com/jobs/my-apple", + }), + ])); + + dropDownButton.nativeElement.click(); + fixture.detectChanges(); + }); + + it("should be one favorite", () => { + expect(favorites.value.count()).toBe(1); + }); + + it("should remove favorite", () => { + component.removeFavorite(favorites.value.toArray()[0] as any); + expect(pinServiceSpy.unPinFavorite).toHaveBeenCalledTimes(1); + expect(favorites.value.count()).toBe(0); }); }); }); diff --git a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts index 2071151c20..900ba413dd 100644 --- a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts +++ b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.component.ts @@ -3,8 +3,8 @@ import { NavigationEnd, Router } from "@angular/router"; import { List } from "immutable"; import { Observable, Subscription } from "rxjs"; -import { PinnableEntity, PinnedEntityType } from "@batch-flask/core"; -import { DropdownComponent } from "@batch-flask/ui"; +import { MouseButton, PinnableEntity, PinnedEntityType } from "@batch-flask/core"; +import { ContextMenu, ContextMenuItem, ContextMenuService, DropdownComponent } from "@batch-flask/ui"; import { AccountService, PinnedEntityService } from "app/services"; import "./pinned-dropdown.scss"; @@ -26,7 +26,8 @@ export class PinnedDropDownComponent implements OnInit, OnDestroy { constructor( private router: Router, private changeDetector: ChangeDetectorRef, - public pinnedEntityService: PinnedEntityService, + private contextMenuService: ContextMenuService, + private pinnedEntityService: PinnedEntityService, private accountService: AccountService) { this.favorites = this.pinnedEntityService.favorites; @@ -59,6 +60,16 @@ export class PinnedDropDownComponent implements OnInit, OnDestroy { this._dropdown.close(); } + public removeFavorite(favorite: PinnableEntity) { + this.pinnedEntityService.unPinFavorite(favorite); + } + + public handleMiddleMouseUp(event: MouseEvent, favorite: PinnableEntity) { + if (event.button === MouseButton.middle) { + this.pinnedEntityService.unPinFavorite(favorite); + } + } + public entityType(favorite: PinnableEntity) { switch (favorite.pinnableType) { case PinnedEntityType.Application: @@ -100,4 +111,10 @@ export class PinnedDropDownComponent implements OnInit, OnDestroy { public trackPinnned(index, entity: PinnableEntity) { return `${entity.pinnableType}/${entity.id}`; } + + public onContextMenu(favorite: PinnableEntity) { + this.contextMenuService.openMenu(new ContextMenu([ + new ContextMenuItem("Remove favorite", () => this.pinnedEntityService.unPinFavorite(favorite)), + ])); + } } diff --git a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.html b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.html index c39a2ad3e4..467861475c 100644 --- a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.html +++ b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.html @@ -3,19 +3,20 @@ {{title}}
- - -
-
{{favorite.name || favorite.id}}
-
{{entityType(favorite)}}
-
-
- -
-
+ (click)="gotoFavorite(favorite)" + (mouseup)="handleMiddleMouseUp($event, favorite)" + (contextmenu)="onContextMenu(favorite)"> + + + + {{favorite.name || favorite.id}} + + + +
diff --git a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.scss b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.scss index f8a785dd68..a4b8e1290e 100644 --- a/app/components/layout/pinned-entity-dropdown/pinned-dropdown.scss +++ b/app/components/layout/pinned-entity-dropdown/pinned-dropdown.scss @@ -2,23 +2,76 @@ @import "app/styles/variables"; bl-pinned-dropdown { - $ddl-width: 280px; + [bl-dropdown-content] { + min-width: 300px; + max-width: 350px; + } + + .dropdown-item.favorite { + position: relative; + min-width: 250px; + cursor: pointer; + padding: 0; + display: flex; + align-items: stretch; + justify-content: stretch; - .fa { - font-size: 1.2em; - padding-right: 5px; + &:hover { + i.fa { + color: $primary-color-dark; + } - &.fa-tasks, &.fa-database, &.fa-file-archive-o, &.fa-question, &.fa-cloud-upload, &.fa-certificate, &.fa-calendar { - color: map-get($primary, 300); + &.selected { + color: white; + } } - } - .main { - width: calc(#{$ddl-width} - 65px); - padding-right: 4px; + &.selected { + font-weight: bold; + color: $primary-color-dark; + } + + > span { + flex: 1 1 auto; + text-align: left; + min-width: 20px; + text-overflow: ellipsis; + overflow-x: hidden; + border-right: 1px solid $border-color; + padding: 5px 10px; + + &:hover { + .fa { + color: white; + } + } + + & i { + margin-right: 5px; + + &.fa { + color: $primary-color-dark; + } + } + } + + > i.fa.fa-times { + flex: 0 0 30px; + background-color: white; + color: $primary-color-dark; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + + &:hover { + color: white; + background-color: map-get($danger, 400); + } - .type { - font-size: 10px; + &:active { + background-color: map-get($danger, 600); + } } } } diff --git a/app/components/pool/action/add/pool-create-basic-dialog.component.ts b/app/components/pool/action/add/pool-create-basic-dialog.component.ts index 50a9f60303..4f6014b61f 100644 --- a/app/components/pool/action/add/pool-create-basic-dialog.component.ts +++ b/app/components/pool/action/add/pool-create-basic-dialog.component.ts @@ -26,6 +26,7 @@ export class PoolCreateBasicDialogComponent extends DynamicForm { this.references = x; this.changeDetector.markForCheck(); @@ -59,4 +64,10 @@ export class SidebarBookmarksComponent implements OnDestroy { return reference.id; } } + + public onContextMenu(reference: SidebarRef) { + this.contextMenuService.openMenu(new ContextMenu([ + new ContextMenuItem("Delete in progress form", () => reference.destroy()), + ])); + } } diff --git a/src/@batch-flask/ui/sidebar/sidebar-bookmarks/sidebar-bookmarks.html b/src/@batch-flask/ui/sidebar/sidebar-bookmarks/sidebar-bookmarks.html index 649b081fa0..2a5b65c5eb 100644 --- a/src/@batch-flask/ui/sidebar/sidebar-bookmarks/sidebar-bookmarks.html +++ b/src/@batch-flask/ui/sidebar/sidebar-bookmarks/sidebar-bookmarks.html @@ -7,7 +7,8 @@