TSK-1395: Update Navbar to work without bootstrap 3 (#1288)

* TSK-1395: update navbar

* TSK-1395: fixed CI error

* TSK-1395: arrange components correctly

* TSK-1395: fit navbar to the other components

* TSK-1395: optimize code and add tests

* TSK-1395: delete comment

* TSK-1395: add missing logout function

* TSK-1395: modify components
This commit is contained in:
Franzi321 2020-10-14 16:39:22 +02:00 committed by GitHub
parent 6a8311a32b
commit 7c83c87f32
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 594 additions and 352 deletions

View File

@ -11,5 +11,6 @@
"[html]": { "[html]": {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.html-language-features" "editor.defaultFormatter": "vscode.html-language-features"
} },
"eslint.enable": false
} }

View File

@ -14,4 +14,4 @@
module.exports = (on, config) => { module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits // `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config // `config` is the resolved Cypress config
} };

View File

@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import './commands' import './commands';
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

View File

@ -2,4 +2,4 @@
"name": "Using fixtures to represent data", "name": "Using fixtures to represent data",
"email": "hello@cypress.io", "email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes" "body": "Fixtures are a great way to mock data for responses to routes"
} }

View File

@ -14,7 +14,7 @@
// *********************************************************** // ***********************************************************
// Import commands.js using ES2015 syntax: // Import commands.js using ES2015 syntax:
import "./commands"; import './commands';
// Alternatively you can use CommonJS syntax: // Alternatively you can use CommonJS syntax:
// require('./commands') // require('./commands')

View File

@ -44,4 +44,3 @@ td {
.table__access-item-groups { .table__access-item-groups {
height: 80px; height: 80px;
} }

View File

@ -10,7 +10,6 @@
position: relative; position: relative;
} }
/* ACTION TOOLBAR */ /* ACTION TOOLBAR */
.classification-details__headline { .classification-details__headline {
padding-top: 0.5rem; padding-top: 0.5rem;
@ -52,7 +51,6 @@
color: $invalid; color: $invalid;
} }
/* DETAILED FIELDS */ /* DETAILED FIELDS */
.classification__detailed-fields { .classification__detailed-fields {
@ -111,4 +109,3 @@
input:invalid.dirty { input:invalid.dirty {
border-color: $invalid; border-color: $invalid;
} }

View File

@ -6,5 +6,5 @@
align-items: stretch; align-items: stretch;
} }
taskana-administration-classification-details { taskana-administration-classification-details {
width: 100% width: 100%;
} }

View File

@ -5,5 +5,5 @@ mat-icon {
margin-left: 3px; margin-left: 3px;
} }
button { button {
color: #555 color: #555;
} }

View File

@ -6,4 +6,3 @@ svg-icon {
fill: #555; fill: #555;
top: -5px !important; top: -5px !important;
} }

View File

@ -2,4 +2,3 @@
width: 100%; width: 100%;
display: flex; display: flex;
} }

View File

@ -1,10 +1,29 @@
<taskana-shared-nav-bar></taskana-shared-nav-bar> <mat-sidenav-container class="sidenav">
<mat-sidenav #sidenav mode="over" class="sidenav__drawer" [autoFocus]="false">
<div (window:resize)="onResize()" class=""> <div class="sidenav__drawer-logout">
<div class="taskana-main"> <button mat-icon-button data-toggle="tooltip" title="Logout" (click)="logout()" aria-expanded="true"
aria-controls="logout">
<mat-icon>exit_to_app</mat-icon>
</button>
</div>
<a class="sidenav__drawer-user-info">
<taskana-shared-user-information></taskana-shared-user-information>
</a>
<taskana-sidenav-list></taskana-sidenav-list>
<div class="sidenav__drawer-version">
<p> Taskana version: {{version}} </p>
</div>
</mat-sidenav>
<mat-sidenav-content>
<taskana-shared-nav-bar></taskana-shared-nav-bar>
<div (window:resize)="onResize()" class="">
<div class="taskana-main">
<router-outlet></router-outlet> <router-outlet></router-outlet>
<taskana-shared-spinner [isRunning]="requestInProgress" isModal=true></taskana-shared-spinner> <taskana-shared-spinner [isRunning]="requestInProgress" isModal=true></taskana-shared-spinner>
<taskana-shared-progress-bar [hidden]="currentProgressValue === 0" currentValue={{currentProgressValue}}></taskana-shared-progress-bar> <taskana-shared-progress-bar [hidden]="currentProgressValue === 0" currentValue={{currentProgressValue}}>
</taskana-shared-progress-bar>
</div>
<taskana-shared-type-ahead></taskana-shared-type-ahead>
</div> </div>
<taskana-shared-type-ahead></taskana-shared-type-ahead> </mat-sidenav-content>
</div> </mat-sidenav-container>

View File

@ -0,0 +1,57 @@
@import '../theme/variables';
.sidenav {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin-top: 0px;
}
.sidenav__drawer {
position: absolute;
z-index: 999;
box-shadow: none;
width: 350px;
background-color: $dark-green;
box-shadow: 3px 0px 10px -1px $dark-green;
}
.sidenav__drawer-list-item {
margin-left: 30px;
}
.sidenav__drawer-logout {
text-align: end;
}
.sidenav__drawer-version {
color: $grey;
position: absolute;
bottom: 5px;
font-size: 12px;
margin-left: 16px;
}
.sidenav__drawer-user-info {
margin-top: 30px;
margin-bottom: 50px;
margin-left: -16px;
}
.mat-icon-button {
outline: none;
}
.mat-icon {
color: white;
}
::ng-deep .mat-drawer-inner-container {
overflow: visible !important;
}
::ng-deep .mat-drawer {
overflow-y: visible !important;
}

View File

@ -1,15 +1,17 @@
import { Component, HostListener, OnDestroy, OnInit } from '@angular/core'; import { Component, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { NavigationStart, Router } from '@angular/router'; import { NavigationStart, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { MatSidenav } from '@angular/material';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { SidenavService } from './shared/services/sidenav/sidenav.service';
import { RequestInProgressService } from './shared/services/request-in-progress/request-in-progress.service'; import { RequestInProgressService } from './shared/services/request-in-progress/request-in-progress.service';
import { OrientationService } from './shared/services/orientation/orientation.service'; import { OrientationService } from './shared/services/orientation/orientation.service';
import { SelectedRouteService } from './shared/services/selected-route/selected-route'; import { SelectedRouteService } from './shared/services/selected-route/selected-route';
import { UploadService } from './shared/services/upload/upload.service'; import { UploadService } from './shared/services/upload/upload.service';
import { ErrorModel } from './shared/models/error-model'; import { ErrorModel } from './shared/models/error-model';
import { NotificationService } from './shared/services/notifications/notification.service'; import { TaskanaEngineService } from './shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from 'app/shared/services/window/window.service';
import { environment } from 'environments/environment';
@Component({ @Component({
selector: 'taskana-root', selector: 'taskana-root',
@ -18,7 +20,6 @@ import { NotificationService } from './shared/services/notifications/notificatio
}) })
export class AppComponent implements OnInit, OnDestroy { export class AppComponent implements OnInit, OnDestroy {
workbasketsRoute = true; workbasketsRoute = true;
selectedRoute = ''; selectedRoute = '';
requestInProgress = false; requestInProgress = false;
@ -29,6 +30,7 @@ export class AppComponent implements OnInit, OnDestroy {
routerSubscription: Subscription; routerSubscription: Subscription;
uploadingFileSubscription: Subscription; uploadingFileSubscription: Subscription;
error: ErrorModel; error: ErrorModel;
version: string;
constructor( constructor(
private router: Router, private router: Router,
@ -36,8 +38,10 @@ export class AppComponent implements OnInit, OnDestroy {
private orientationService: OrientationService, private orientationService: OrientationService,
private selectedRouteService: SelectedRouteService, private selectedRouteService: SelectedRouteService,
private formsValidatorService: FormsValidatorService, private formsValidatorService: FormsValidatorService,
private errorService: NotificationService, public uploadService: UploadService,
public uploadService: UploadService private sidenavService: SidenavService,
private taskanaEngineService: TaskanaEngineService,
private window: WindowRefService
) {} ) {}
@HostListener('window:resize', ['$event']) @HostListener('window:resize', ['$event'])
@ -45,6 +49,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.orientationService.onResize(); this.orientationService.onResize();
} }
@ViewChild('sidenav') public sidenav: MatSidenav;
ngOnInit() { ngOnInit() {
this.routerSubscription = this.router.events.subscribe((event) => { this.routerSubscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) { if (event instanceof NavigationStart) {
@ -65,9 +71,23 @@ export class AppComponent implements OnInit, OnDestroy {
} }
this.selectedRoute = value; this.selectedRoute = value;
}); });
this.uploadingFileSubscription = this.uploadService.getCurrentProgressValue().subscribe((value) => { this.uploadingFileSubscription = this.uploadService.getCurrentProgressValue().subscribe((value) => {
this.currentProgressValue = value; this.currentProgressValue = value;
}); });
this.taskanaEngineService.getVersion().subscribe((restVersion) => {
this.version = restVersion.version;
});
}
logout() {
this.taskanaEngineService.logout();
this.window.nativeWindow.location.href = environment.taskanaLogoutUrl;
}
ngAfterViewInit(): void {
this.sidenavService.setSidenav(this.sidenav);
} }
ngOnDestroy() { ngOnDestroy() {

View File

@ -13,6 +13,14 @@ import { TabsModule } from 'ngx-bootstrap/tabs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeModule } from 'angular-tree-component'; import { TreeModule } from 'angular-tree-component';
import { SharedModule } from 'app/shared/shared.module'; import { SharedModule } from 'app/shared/shared.module';
import {
MatButtonModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatIconModule
} from '@angular/material';
/** /**
* Services * Services
@ -31,6 +39,8 @@ import { NoAccessComponent } from 'app/shared/components/no-access/no-access.com
import { FormsValidatorService } from './shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from './shared/services/forms-validator/forms-validator.service';
import { UploadService } from './shared/services/upload/upload.service'; import { UploadService } from './shared/services/upload/upload.service';
import { NotificationService } from './shared/services/notifications/notification.service'; import { NotificationService } from './shared/services/notifications/notification.service';
import { SidenavService } from './shared/services/sidenav/sidenav.service';
import { SidenavListComponent } from 'app/shared/components/sidenav-list/sidenav-list.component';
/** /**
* Components * Components
*/ */
@ -62,11 +72,17 @@ const MODULES = [
ReactiveFormsModule, ReactiveFormsModule,
TreeModule, TreeModule,
SharedModule, SharedModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatButtonModule,
MatIconModule,
NgxsModule.forRoot(STATES, { developmentMode: !environment.production }), NgxsModule.forRoot(STATES, { developmentMode: !environment.production }),
NgxsReduxDevtoolsPluginModule.forRoot({ disabled: environment.production, maxAge: 25 }) NgxsReduxDevtoolsPluginModule.forRoot({ disabled: environment.production, maxAge: 25 })
]; ];
const DECLARATIONS = [AppComponent, NavBarComponent, UserInformationComponent, NoAccessComponent]; const DECLARATIONS = [AppComponent, NavBarComponent, UserInformationComponent, NoAccessComponent, SidenavListComponent];
export function startupServiceFactory(startupService: StartupService): () => Promise<any> { export function startupServiceFactory(startupService: StartupService): () => Promise<any> {
return (): Promise<any> => startupService.load(); return (): Promise<any> => startupService.load();
@ -97,7 +113,8 @@ export function startupServiceFactory(startupService: StartupService): () => Pro
FormsValidatorService, FormsValidatorService,
UploadService, UploadService,
NotificationService, NotificationService,
ClassificationCategoriesService ClassificationCategoriesService,
SidenavService
], ],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View File

@ -1,4 +1,3 @@
.master-detail { .master-detail {
min-width: 100vw; min-width: 100vw;
} }

View File

@ -1,78 +1,11 @@
<nav class="navbar"> <nav class="navbar navbar__inverse">
<div class="navbar no-border-radius navbar-inverse no-gutter col-xs-12"> <div class="navbar__button">
<div class="pull-left col-sm-3 col-md-4"> <button mat-icon-button class="navbar_button-toggle" (click)="toggleSidenav()">
<button type="button" *ngIf="!showNavbar" class="btn btn-default navbar-toggle show pull-left" (click)="toggleNavBar();" <mat-icon>menu</mat-icon>
aria-expanded="true" aria-controls="navbar" data-toggle="tooltip" title="Menu"> </button>
<span class="material-icons md-24 white">menu</span>
</button>
<span>&nbsp;</span>
</div>
<div class="col-xs-6 col-sm-5 col-md-4">
<ul class="nav logo">
<svg-icon class="logo white hidden-xs" src="./assets/icons/logo-copy.svg"></svg-icon>
<h2 class="navbar-brand no-margin"> {{title}}</h2>
</ul>
</div>
<div *ngIf="showDomainSelector()" class="pull-right domain-form">
<div class="dropdown clearfix btn-group">
<label class="control-label hidden-xs">Working on </label>
<button type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
{{selectedDomain? selectedDomain: 'MASTER DOMAIN'}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu" aria-labelledby="dropdownMenu">
<li>
<a *ngFor="let domain of domains" (click)="switchDomain(domain)">
<label>{{domain? domain: 'MASTER DOMAIN'}}</label>
</a>
</li>
</ul>
</div>
</div>
</div> </div>
<div [@toggleRight]="showNavbar" *ngIf="showNavbar" class="navbar-inverse sidenav full-height col-xs-9 col-sm-3" <div class="navbar__logo">
data-html="false" aria-expanded="true"> <svg-icon class="navbar__logo-icon" src="./assets/icons/logo-copy.svg"></svg-icon>
<div class="row"> <h2 class="navbar__title">{{title}}</h2>
<ul class="nav">
<svg-icon class="logo white visible-xs" src="./assets/icons/logo.svg"></svg-icon>
<h2 class="navbar-brand no-margin logo visible-xs"> {{title}}</h2>
<button type="button" class="btn btn-default logout navbar-toggle show pull-right" data-toggle="tooltip" title="Logout"
(click)="logout()" aria-expanded="true" aria-controls="logout">
<span class="material-icons md-20 white ">exit_to_app</span>
</button>
</ul>
</div>
<div class="nav-content">
<taskana-shared-user-information></taskana-shared-user-information>
<div *ngIf="administrationAccess" class="row menu">
<span (click)="toggleNavBar()" routerLink="taskana/administration/workbaskets" aria-controls="administration"
routerLinkActive="active">Administration</span>
<div class="row submenu" [ngClass]="{'selected': selectedRoute.indexOf('workbaskets') !== -1 }">
<span (click)="toggleNavBar()" class="col-xs-6" routerLink="taskana/administration/workbaskets" aria-controls="Workbaskets"
routerLinkActive="active">Workbaskets</span>
</div>
<div class="row submenu" [ngClass]="{'selected': selectedRoute.indexOf('classifications') !== -1}">
<span (click)="toggleNavBar()" class="col-xs-6" routerLink="taskana/administration/classifications" aria-controls="Classifications"
routerLinkActive="active">Classifications</span>
</div>
<div class="row submenu" [ngClass]="{'selected': selectedRoute.indexOf('access-items-management') !== -1}">
<span (click)="toggleNavBar()" class="col-xs-6" routerLink="taskana/administration/access-items-management"
aria-controls="Access items" routerLinkActive="active">Access items</span>
</div>
</div>
<div *ngIf="monitorAccess" class="row menu" [ngClass]="{'selected': selectedRoute.indexOf('monitor') !== -1}">
<span (click)="toggleNavBar()" routerLink="{{monitorUrl}}" aria-controls="Monitor" routerLinkActive="active">Monitor</span>
</div>
<div *ngIf="workplaceAccess" class="row menu" [ngClass]="{'selected': selectedRoute.indexOf('workplace') !== -1 || selectedRoute === ''}">
<span (click)="toggleNavBar()" routerLink="{{workplaceUrl}}" aria-controls="Workplace" routerLinkActive="active">Workplace</span>
</div>
<div *ngIf="historyAccess" class="row menu" [ngClass]="{'selected': selectedRoute.indexOf('history') !== -1}">
<span (click)="toggleNavBar()" routerLink="{{historyUrl}}" aria-controls="history" routerLinkActive="active">History</span>
</div>
</div>
<div class="nav-version">
<p id="taskana-version"> Taskana version: {{version}} </p>
</div>
</div> </div>
<div *ngIf="showNavbar" class="backdrop" (click)="toggleNavBar()"></div> </nav>
</nav>

View File

@ -2,103 +2,7 @@
.navbar.main:before { .navbar.main:before {
@include degraded-bar(right, 100%, 3px); @include degraded-bar(right, 100%, 3px);
} box-sizing: border-box;
.navbar-inverse {
top: 0;
border: none;
background-color: $dark-green;
box-shadow: 0px 1px 5px -1px black;
}
.navbar-toggle {
margin: 3px 0px;
font-size: 20px;
&.logout {
font-size: 20px;
}
}
button.navbar-toggle:hover > span {
color: $aquamarine;
}
ul.nav > p {
white-space: nowrap;
text-overflow: ellipsis;
padding-right: 0px;
}
.navbar-inverse .navbar-toggle,
.navbar-toggle:hover,
.navbar-inverse .navbar-toggle:focus {
border: none;
background-color: transparent;
}
svg-icon.logo {
float: left;
width: 150px;
height: 50px;
padding: 5px;
position: initial;
}
h2.navbar-brand {
vertical-align: middle;
text-decoration: none;
color: white;
padding: 15px 0px 0px 0px;
font-size: 20px;
}
.domain-form {
margin: 13px;
color: white;
font-size: 18px;
> div {
cursor: pointer;
> button {
color: white;
background-color: $dark-green;
border: none;
font-size: 16px;
border-bottom: 1px solid $dark-green;
margin-left: 5px;
}
}
}
.nav-content {
margin-top: 5px;
}
.nav-version {
color: $grey;
position: absolute;
bottom: 5px;
font-size: 12px;
}
/*
* All side bar links styling.
*/
.nav-sidebar > li > a {
padding-right: 20px;
padding-left: 20px;
}
.menu > span:hover,
.submenu > span:hover {
color: white;
cursor: pointer;
}
.sidenav {
position: fixed;
z-index: 999;
box-shadow: none;
height: 100%;
background-color: $dark-green;
box-shadow: 3px 0px 10px -1px $dark-green;
} }
.navbar { .navbar {
@ -106,40 +10,53 @@ h2.navbar-brand {
margin-bottom: 0px; margin-bottom: 0px;
} }
.menu, .navbar__inverse {
.submenu > span { border: none;
margin-top: 15px; background-color: $dark-green;
font-size: 20px; box-shadow: 0px 1px 5px -1px black;
width: 100%; width: 100%;
font-family: inherit;
font-weight: 500;
line-height: 1.1;
} }
.menu, .navbar__buttom {
.submenu > span { flex-grow: 1;
padding-left: 12px; display: flex !important;
color: $grey; order: 1;
margin-top: -10px;
}
.navbar__logo {
display: flex !important;
flex-grow: 6;
position: relative;
left: 40%;
margin-top: -3px;
order: 2;
}
h2.navbar__title {
display: flex !important;
flex-grow: 5;
color: white;
margin-top: 10px;
order: 3;
margin-left: 2%;
font-size: large;
@media only screen and (max-width: 700px) {
display: none !important;
}
}
svg-icon.navbar__logo-icon {
float: left;
width: 150px;
height: 50px;
position: initial;
}
.mat-icon-button {
outline: none; outline: none;
} }
.menu.selected,
.submenu.selected {
background-color: transparent;
& > span {
padding-left: 10px;
border-left: $pallete-green 5px solid;
color: white;
}
}
.menu > .submenu { .mat-icon {
margin-left: 30px; color: white;
}
a {
color: $grey;
&:hover {
color: white;
}
text-decoration: none;
} }

View File

@ -0,0 +1,67 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { NavBarComponent } from './nav-bar.component';
import { SelectedRouteService } from '../../../shared/services/selected-route/selected-route';
import { MatIconModule } from '@angular/material';
import { SidenavService } from '../../../shared/services/sidenav/sidenav.service';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs/internal/observable/of';
const SidenavServiceSpy = jest.fn().mockImplementation(
(): Partial<SidenavService> => ({
toggleSidenav: jest.fn().mockReturnValue(of())
})
);
const SelectedRouteServiceSpy = jest.fn().mockImplementation(
(): Partial<SelectedRouteService> => ({
getSelectedRoute: jest.fn().mockReturnValue(of())
})
);
describe('NavBarComponent', () => {
let component: NavBarComponent;
let fixture: ComponentFixture<NavBarComponent>;
let debugElement: DebugElement;
var route = '';
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NavBarComponent],
imports: [MatIconModule, HttpClientTestingModule, AngularSvgIconModule],
providers: [
{ provide: SidenavService, useClass: SidenavServiceSpy },
{ provide: SelectedRouteService, useClass: SelectedRouteServiceSpy }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(NavBarComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should set title to workbasket if workbasket ist selected', () => {
route = 'workbaskets';
fixture.detectChanges();
component.setTitle(route);
expect(component.title).toBe('Workbaskets');
});
it('should toggle sidenav when button clicked', () => {
fixture.detectChanges();
expect(component.toggle).toBe(false);
const button = debugElement.query(By.css('.navbar_button-toggle')).nativeElement;
expect(button).toBeTruthy();
button.click();
expect(component.toggle).toBe(true);
});
});

View File

@ -1,105 +1,44 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { environment } from 'environments/environment';
import { SelectedRouteService } from 'app/shared/services/selected-route/selected-route'; import { SelectedRouteService } from 'app/shared/services/selected-route/selected-route';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { BusinessAdminGuard } from 'app/shared/guards/business-admin.guard';
import { MonitorGuard } from 'app/shared/guards/monitor.guard';
import { WindowRefService } from 'app/shared/services/window/window.service';
import { UserGuard } from 'app/shared/guards/user.guard';
import { expandRight } from 'app/shared/animations/expand.animation'; import { expandRight } from 'app/shared/animations/expand.animation';
import { TaskanaEngineService } from '../../services/taskana-engine/taskana-engine.service'; import { SidenavService } from '../../services/sidenav/sidenav.service';
@Component({ @Component({
selector: 'taskana-shared-nav-bar', selector: 'taskana-shared-nav-bar',
templateUrl: './nav-bar.component.html', templateUrl: './nav-bar.component.html',
styleUrls: ['./nav-bar.component.scss'], styleUrls: ['./nav-bar.component.scss'],
animations: [expandRight] animations: [expandRight]
}) })
export class NavBarComponent implements OnInit, OnDestroy { export class NavBarComponent implements OnInit {
selectedRoute = ''; selectedRoute = '';
route: string;
title = ''; title = '';
titleAdministration = 'Administration';
titleWorkbaskets = 'Workbaskets'; titleWorkbaskets = 'Workbaskets';
titleClassifications = 'Classifications'; titleClassifications = 'Classifications';
titleAccessItems = 'Access items'; titleAccessItems = 'Access items';
titleMonitor = 'Monitor'; titleMonitor = 'Monitor';
titleWorkplace = 'Workplace'; titleWorkplace = 'Workplace';
titleHistory = 'History'; titleHistory = 'History';
showNavbar = false; toggle: boolean = false;
domains: Array<string> = [];
selectedDomain: string;
version: string;
adminUrl = 'taskana/administration';
monitorUrl = 'taskana/monitor';
workplaceUrl = 'taskana/workplace';
historyUrl = 'taskana/history';
administrationAccess = false;
monitorAccess = false;
workplaceAccess = false;
historyAccess = false;
selectedRouteSubscription: Subscription; selectedRouteSubscription: Subscription;
getDomainsSubscription: Subscription;
constructor( constructor(private selectedRouteService: SelectedRouteService, private sidenavService: SidenavService) {}
private selectedRouteService: SelectedRouteService,
private domainService: DomainService,
private taskanaEngineService: TaskanaEngineService,
private window: WindowRefService
) {}
ngOnInit() { ngOnInit() {
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => { this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
this.selectedRoute = value; this.selectedRoute = value;
this.setTitle(value); this.setTitle(value);
}); });
this.getDomainsSubscription = this.domainService.getDomains().subscribe((domains) => {
this.domains = domains;
});
this.domainService.getSelectedDomain().subscribe((domain) => {
this.selectedDomain = domain;
});
this.taskanaEngineService.getVersion().subscribe((restVersion) => {
this.version = restVersion.version;
});
this.administrationAccess = this.taskanaEngineService.hasRole(BusinessAdminGuard.roles);
this.monitorAccess = this.taskanaEngineService.hasRole(MonitorGuard.roles);
this.workplaceAccess = this.taskanaEngineService.hasRole(UserGuard.roles);
this.taskanaEngineService.isHistoryProviderEnabled().subscribe((value) => {
this.historyAccess = value;
});
} }
switchDomain(domain) { toggleSidenav() {
this.domainService.switchDomain(domain); this.toggle = !this.toggle;
this.sidenavService.toggleSidenav();
} }
toggleNavBar() { setTitle(value: string = 'workbaskets') {
this.showNavbar = !this.showNavbar;
}
logout() {
this.taskanaEngineService.logout().subscribe(() => {});
this.window.nativeWindow.location.href = environment.taskanaLogoutUrl;
}
showDomainSelector(): boolean {
return (
this.selectedRoute.indexOf('administration') !== -1 ||
this.selectedRoute.indexOf('workbaskets') !== -1 ||
this.selectedRoute.indexOf('classifications') !== -1
);
}
private setTitle(value: string = 'workbaskets') {
if (value.indexOf('workbaskets') === 0) { if (value.indexOf('workbaskets') === 0) {
this.title = this.titleWorkbaskets; this.title = this.titleWorkbaskets;
} else if (value.indexOf('classifications') === 0) { } else if (value.indexOf('classifications') === 0) {
@ -114,13 +53,4 @@ export class NavBarComponent implements OnInit, OnDestroy {
this.title = this.titleHistory; this.title = this.titleHistory;
} }
} }
ngOnDestroy(): void {
if (this.selectedRouteSubscription) {
this.selectedRouteSubscription.unsubscribe();
}
if (this.getDomainsSubscription) {
this.getDomainsSubscription.unsubscribe();
}
}
} }

View File

@ -0,0 +1,16 @@
<mat-nav-list>
<a mat-list-item class="list-item list-item__admin" [routerLink]=[workbasketsUrl] [routerLinkActive]="['active']"
*ngIf="administrationAccess" (click)="toggleSidenav()">Administration</a>
<a mat-list-item class="list-item list-item__admin-workbaskets" [routerLink]=[workbasketsUrl]
[routerLinkActive]="['active']" *ngIf="administrationAccess" (click)="toggleSidenav()">Workbaskets</a>
<a mat-list-item class="list-item list-item__admin-classifications" [routerLink]=[classificationUrl]
[routerLinkActive]="['active']" *ngIf="administrationAccess" (click)="toggleSidenav()">Classifications</a>
<a mat-list-item class="list-item list-item__admin-acces-items" [routerLink]=[accessUrl]
[routerLinkActive]="['active']" (click)="toggleSidenav()" *ngIf="administrationAccess">Access Items</a>
<a mat-list-item class="list-item list-item__monitor" [routerLink]=[monitorUrl] [routerLinkActive]="['active']"
*ngIf="monitorAccess" (click)="toggleSidenav()">Monitor</a>
<a mat-list-item class="list-item list-item__workplace" [routerLink]=[workplaceUrl] [routerLinkActive]="['active']"
*ngIf="workplaceAccess" (click)="toggleSidenav()">Workplace</a>
<a mat-list-item class="list-item list-item__history" [routerLink]=[historyUrl] [routerLinkActive]="['active']"
*ngIf="historyAccess" (click)="toggleSidenav()">History</a>
</mat-nav-list>

View File

@ -0,0 +1,35 @@
@import '../../../../theme/variables';
.list-item {
color: $grey;
}
.list-item {
color: $grey;
}
.list-item {
color: $grey;
}
.list-item__admin-workbaskets {
color: $grey;
margin-left: 30px;
}
.list-item__admin-classifications {
color: $grey;
margin-left: 30px;
}
.list-item__admin-acces-items {
color: $grey;
margin-left: 30px;
}
.active {
color: white !important;
}
::ng-deep .mat-drawer-container {
background-color: white;
}

View File

@ -0,0 +1,101 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { SidenavListComponent } from './sidenav-list.component';
import { SidenavService } from '../../../shared/services/sidenav/sidenav.service';
import {
MatButtonModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatIconModule
} from '@angular/material';
import { BrowserModule, By } from '@angular/platform-browser';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { TaskanaEngineService } from '../../services/taskana-engine/taskana-engine.service';
import { TaskanaEngineServiceMock } from '../../services/taskana-engine/taskana-engine.mock.service';
import { of } from 'rxjs/internal/observable/of';
const SidenavServiceSpy = jest.fn().mockImplementation(
(): Partial<SidenavService> => ({
toggleSidenav: jest.fn().mockReturnValue(of())
})
);
const TaskanaEngingeServiceSpy = jest.fn().mockImplementation(
(): Partial<TaskanaEngineServiceMock> => ({
hasRole: jest.fn().mockReturnValue(of()),
isHistoryProviderEnabled: jest.fn().mockReturnValue(of())
})
);
describe('SidenavListComponent', () => {
let component: SidenavListComponent;
let fixture: ComponentFixture<SidenavListComponent>;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [SidenavListComponent],
imports: [
MatButtonModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatIconModule,
BrowserModule,
RouterModule,
RouterTestingModule,
HttpClientTestingModule
],
providers: [
{ provide: SidenavService, useClass: SidenavServiceSpy },
{ provide: TaskanaEngineService, useClass: TaskanaEngingeServiceSpy }
]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SidenavListComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should show all links if user has all permissions', () => {
component.administrationAccess = true;
component.monitorAccess = true;
component.workplaceAccess = true;
component.historyAccess = true;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.list-item'));
expect(menuList.length).toBe(7);
fixture.detectChanges();
});
it('should show all links if user has only monitor access', () => {
component.administrationAccess = false;
component.monitorAccess = true;
component.workplaceAccess = false;
component.historyAccess = false;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.list-item'));
expect(menuList.length).toBe(1);
});
it('should toggle sidenav when link clicked', () => {
component.toggle = true;
fixture.detectChanges();
const button = debugElement.query(By.css('.list-item__admin-workbaskets')).nativeElement;
expect(button).toBeTruthy();
button.click();
expect(component.toggle).toBe(false);
});
});

View File

@ -0,0 +1,45 @@
import { Component, OnInit } from '@angular/core';
import { BusinessAdminGuard } from 'app/shared/guards/business-admin.guard';
import { MonitorGuard } from 'app/shared/guards/monitor.guard';
import { UserGuard } from 'app/shared/guards/user.guard';
import { TaskanaEngineService } from '../../services/taskana-engine/taskana-engine.service';
import { SidenavService } from '../../services/sidenav/sidenav.service';
@Component({
selector: 'taskana-sidenav-list',
templateUrl: './sidenav-list.component.html',
styleUrls: ['./sidenav-list.component.scss']
})
export class SidenavListComponent implements OnInit {
toggle: boolean = false;
monitorUrl = 'taskana/monitor';
workplaceUrl = 'taskana/workplace';
historyUrl = 'taskana/history';
accessUrl = 'taskana/administration/access-items-management';
classificationUrl = 'taskana/administration/classifications';
workbasketsUrl = 'taskana/administration/workbaskets';
administrationAccess = false;
monitorAccess = false;
workplaceAccess = false;
historyAccess = false;
admin_url_list: any[];
constructor(private taskanaEngineService: TaskanaEngineService, private sidenavService: SidenavService) {}
ngOnInit() {
this.administrationAccess = this.taskanaEngineService.hasRole(BusinessAdminGuard.roles);
this.monitorAccess = this.taskanaEngineService.hasRole(MonitorGuard.roles);
this.workplaceAccess = this.taskanaEngineService.hasRole(UserGuard.roles);
this.taskanaEngineService.isHistoryProviderEnabled().subscribe((value) => {
this.historyAccess = value;
});
}
toggleSidenav() {
this.toggle = !this.toggle;
this.sidenavService.toggleSidenav();
}
}

View File

@ -1,16 +1,14 @@
<div class="row"> <div class="icon">
<div class="icon"> <div class="icon__wrap">
<div class="icon-wrap col-xs-offset-5"> <svg-icon class="blue big" src="./assets/icons/user.svg"></svg-icon>
<svg-icon class="blue big" src="./assets/icons/user.svg"></svg-icon>
</div>
</div>
<div class="user-info white">
<span>Logged as: {{userInformation?.userId}}</span>
<button type="button" class="btn btn-default white pull-right transparent" (click)="toggleRoles();" aria-expanded="true" aria-controls="roles">
<span>Roles</span>
</button>
</div>
<div class="white pull-right roles col-xs-12" [@toggleDown]="showRoles">
<span><i>{{roles}}</i></span>
</div> </div>
</div> </div>
<div class="user-info">
<span>Logged as: {{userInformation?.userId}}</span>
<button mat-button class="user-info__button" (click)="toggleRoles();" aria-expanded="true" aria-controls="roles">
<span>Roles</span>
</button>
</div>
<div class="roles" [@toggleDown]="showRoles">
<span><i>{{roles}}</i></span>
</div>

View File

@ -7,6 +7,7 @@
.user-info { .user-info {
margin-top: 85px; margin-top: 85px;
color: white;
> span { > span {
margin: 0 15px 0 15px; margin: 0 15px 0 15px;
font-size: 20px; font-size: 20px;
@ -18,13 +19,15 @@
.icon { .icon {
width: 100%; width: 100%;
position: relative; position: absolute;
& > .icon-wrap {
& > .icon__wrap {
border-radius: 50%; border-radius: 50%;
border: solid 2px #22a39f; border: solid 2px #22a39f;
width: 72px; width: 72px;
height: 72px; height: 72px;
overflow: hidden; left: 35%;
//overflow: hidden;
background-color: #175263; background-color: #175263;
position: absolute; position: absolute;
-webkit-box-shadow: 0px 3px 3px #416b6a; -webkit-box-shadow: 0px 3px 3px #416b6a;
@ -36,7 +39,7 @@
overflow: hidden; overflow: hidden;
background-color: white; background-color: white;
position: absolute; position: absolute;
margin: 2px; margin: 7px -0px 0px 2px;
} }
} }
} }
@ -53,6 +56,11 @@
box-shadow: 0px 3px 3px #416b6a; box-shadow: 0px 3px 3px #416b6a;
} }
.user-info_roles {
margin-left: 16px;
color: white;
}
button.transparent { button.transparent {
border: none; border: none;
background-color: transparent; background-color: transparent;
@ -68,3 +76,14 @@ button.transparent {
color: white; color: white;
} }
} }
.mat-button {
border: none;
outline: none;
left: 28%;
}
.roles {
margin-left: 16px;
color: white;
}

View File

@ -0,0 +1,53 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { UserInformationComponent } from './user-information.component';
import { BrowserModule, By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TaskanaEngineService } from '../../services/taskana-engine/taskana-engine.service';
import { TaskanaEngineServiceMock } from '../../services/taskana-engine/taskana-engine.mock.service';
import { of } from 'rxjs/internal/observable/of';
import { expandDown } from '../../animations/expand.animation';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { fromEventPattern } from 'rxjs';
const TaskanaEngingeServiceSpy = jest.fn().mockImplementation(
(): Partial<TaskanaEngineServiceMock> => ({
hasRole: jest.fn().mockReturnValue(of()),
isHistoryProviderEnabled: jest.fn().mockReturnValue(of())
})
);
describe('UserInformationComponent', () => {
let component: UserInformationComponent;
let fixture: ComponentFixture<UserInformationComponent>;
let debugElement: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [UserInformationComponent],
imports: [BrowserModule, AngularSvgIconModule, HttpClientTestingModule, BrowserAnimationsModule],
providers: [{ provide: TaskanaEngineService, useClass: TaskanaEngingeServiceSpy }]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(UserInformationComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should toggle roles when roles clicked', () => {
fixture.detectChanges();
expect(component.showRoles).toBe(false);
const button = debugElement.query(By.css('.user-info__button')).nativeElement;
expect(button).toBeTruthy();
button.click();
expect(component.showRoles).toBe(true);
});
});

View File

@ -0,0 +1,15 @@
import { TestBed, inject } from '@angular/core/testing';
import { SidenavService } from './sidenav.service';
describe('SidenavService', () => {
beforeEach(() => {
TestBed.configureTestingModule({
providers: [SidenavService]
});
});
it('should be created', inject([SidenavService], (service: SidenavService) => {
expect(service).toBeTruthy();
}));
});

View File

@ -0,0 +1,19 @@
import { Injectable } from '@angular/core';
import { MatSidenav } from '@angular/material';
@Injectable({
providedIn: 'root'
})
export class SidenavService {
private sidenav: MatSidenav;
state: boolean = false;
public setSidenav(sidenav: MatSidenav) {
this.sidenav = sidenav;
}
public toggleSidenav(): void {
this.sidenav.toggle();
this.state = this.sidenav.opened;
}
}

View File

@ -10,8 +10,9 @@
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
</head> </head>
<body> <body>
<taskana-root></taskana-root> <taskana-root></taskana-root>
</body> </body>
</html> </html>

View File

@ -5,13 +5,8 @@
"baseUrl": "", "baseUrl": "",
"types": [] "types": []
}, },
"files": [ "files": ["main.ts", "polyfills.ts"],
"main.ts", "include": ["src/**/*.d.ts"],
"polyfills.ts"
],
"include": [
"src/**/*.d.ts"
],
"angularCompilerOptions": { "angularCompilerOptions": {
"enableIvy": false "enableIvy": false
} }

View File

@ -2,19 +2,10 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"outDir": "./out-tsc/spec", "outDir": "./out-tsc/spec",
"types": [ "types": ["jest", "node"],
"jest",
"node"
],
"esModuleInterop": true, "esModuleInterop": true,
"emitDecoratorMetadata": true "emitDecoratorMetadata": true
}, },
"files": [ "files": ["src/test.ts", "src/polyfills.ts"],
"src/test.ts", "include": ["src/**/*.spec.ts", "src/**/*.d.ts"]
"src/polyfills.ts"
],
"include": [
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
} }