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]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "vscode.html-language-features"
}
},
"eslint.enable": false
}

View File

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

View File

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

View File

@ -2,4 +2,4 @@
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"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";
import './commands';
// Alternatively you can use CommonJS syntax:
// require('./commands')

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,29 @@
<taskana-shared-nav-bar></taskana-shared-nav-bar>
<div (window:resize)="onResize()" class="">
<div class="taskana-main">
<mat-sidenav-container class="sidenav">
<mat-sidenav #sidenav mode="over" class="sidenav__drawer" [autoFocus]="false">
<div class="sidenav__drawer-logout">
<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>
<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>
<taskana-shared-type-ahead></taskana-shared-type-ahead>
</div>
</mat-sidenav-content>
</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 { Subscription } from 'rxjs';
import { MatSidenav } from '@angular/material';
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 { OrientationService } from './shared/services/orientation/orientation.service';
import { SelectedRouteService } from './shared/services/selected-route/selected-route';
import { UploadService } from './shared/services/upload/upload.service';
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({
selector: 'taskana-root',
@ -18,7 +20,6 @@ import { NotificationService } from './shared/services/notifications/notificatio
})
export class AppComponent implements OnInit, OnDestroy {
workbasketsRoute = true;
selectedRoute = '';
requestInProgress = false;
@ -29,6 +30,7 @@ export class AppComponent implements OnInit, OnDestroy {
routerSubscription: Subscription;
uploadingFileSubscription: Subscription;
error: ErrorModel;
version: string;
constructor(
private router: Router,
@ -36,8 +38,10 @@ export class AppComponent implements OnInit, OnDestroy {
private orientationService: OrientationService,
private selectedRouteService: SelectedRouteService,
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'])
@ -45,6 +49,8 @@ export class AppComponent implements OnInit, OnDestroy {
this.orientationService.onResize();
}
@ViewChild('sidenav') public sidenav: MatSidenav;
ngOnInit() {
this.routerSubscription = this.router.events.subscribe((event) => {
if (event instanceof NavigationStart) {
@ -65,9 +71,23 @@ export class AppComponent implements OnInit, OnDestroy {
}
this.selectedRoute = value;
});
this.uploadingFileSubscription = this.uploadService.getCurrentProgressValue().subscribe((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() {

View File

@ -13,6 +13,14 @@ import { TabsModule } from 'ngx-bootstrap/tabs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { TreeModule } from 'angular-tree-component';
import { SharedModule } from 'app/shared/shared.module';
import {
MatButtonModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatIconModule
} from '@angular/material';
/**
* 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 { UploadService } from './shared/services/upload/upload.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
*/
@ -62,11 +72,17 @@ const MODULES = [
ReactiveFormsModule,
TreeModule,
SharedModule,
MatSidenavModule,
MatCheckboxModule,
MatGridListModule,
MatListModule,
MatButtonModule,
MatIconModule,
NgxsModule.forRoot(STATES, { developmentMode: !environment.production }),
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> {
return (): Promise<any> => startupService.load();
@ -97,7 +113,8 @@ export function startupServiceFactory(startupService: StartupService): () => Pro
FormsValidatorService,
UploadService,
NotificationService,
ClassificationCategoriesService
ClassificationCategoriesService,
SidenavService
],
bootstrap: [AppComponent]
})

View File

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

View File

@ -1,78 +1,11 @@
<nav class="navbar">
<div class="navbar no-border-radius navbar-inverse no-gutter col-xs-12">
<div class="pull-left col-sm-3 col-md-4">
<button type="button" *ngIf="!showNavbar" class="btn btn-default navbar-toggle show pull-left" (click)="toggleNavBar();"
aria-expanded="true" aria-controls="navbar" data-toggle="tooltip" title="Menu">
<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>
<nav class="navbar navbar__inverse">
<div class="navbar__button">
<button mat-icon-button class="navbar_button-toggle" (click)="toggleSidenav()">
<mat-icon>menu</mat-icon>
</button>
</div>
<div [@toggleRight]="showNavbar" *ngIf="showNavbar" class="navbar-inverse sidenav full-height col-xs-9 col-sm-3"
data-html="false" aria-expanded="true">
<div class="row">
<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 class="navbar__logo">
<svg-icon class="navbar__logo-icon" src="./assets/icons/logo-copy.svg"></svg-icon>
<h2 class="navbar__title">{{title}}</h2>
</div>
<div *ngIf="showNavbar" class="backdrop" (click)="toggleNavBar()"></div>
</nav>
</nav>

View File

@ -2,103 +2,7 @@
.navbar.main:before {
@include degraded-bar(right, 100%, 3px);
}
.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;
box-sizing: border-box;
}
.navbar {
@ -106,40 +10,53 @@ h2.navbar-brand {
margin-bottom: 0px;
}
.menu,
.submenu > span {
margin-top: 15px;
font-size: 20px;
.navbar__inverse {
border: none;
background-color: $dark-green;
box-shadow: 0px 1px 5px -1px black;
width: 100%;
font-family: inherit;
font-weight: 500;
line-height: 1.1;
}
.menu,
.submenu > span {
padding-left: 12px;
color: $grey;
.navbar__buttom {
flex-grow: 1;
display: flex !important;
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;
}
.menu.selected,
.submenu.selected {
background-color: transparent;
& > span {
padding-left: 10px;
border-left: $pallete-green 5px solid;
color: white;
}
}
.menu > .submenu {
margin-left: 30px;
}
a {
color: $grey;
&:hover {
color: white;
}
text-decoration: none;
.mat-icon {
color: white;
}

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 { environment } from 'environments/environment';
import { Component, OnInit } from '@angular/core';
import { SelectedRouteService } from 'app/shared/services/selected-route/selected-route';
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 { TaskanaEngineService } from '../../services/taskana-engine/taskana-engine.service';
import { SidenavService } from '../../services/sidenav/sidenav.service';
@Component({
selector: 'taskana-shared-nav-bar',
templateUrl: './nav-bar.component.html',
styleUrls: ['./nav-bar.component.scss'],
animations: [expandRight]
})
export class NavBarComponent implements OnInit, OnDestroy {
export class NavBarComponent implements OnInit {
selectedRoute = '';
route: string;
title = '';
titleAdministration = 'Administration';
titleWorkbaskets = 'Workbaskets';
titleClassifications = 'Classifications';
titleAccessItems = 'Access items';
titleMonitor = 'Monitor';
titleWorkplace = 'Workplace';
titleHistory = 'History';
showNavbar = 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;
toggle: boolean = false;
selectedRouteSubscription: Subscription;
getDomainsSubscription: Subscription;
constructor(
private selectedRouteService: SelectedRouteService,
private domainService: DomainService,
private taskanaEngineService: TaskanaEngineService,
private window: WindowRefService
) {}
constructor(private selectedRouteService: SelectedRouteService, private sidenavService: SidenavService) {}
ngOnInit() {
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
this.selectedRoute = 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) {
this.domainService.switchDomain(domain);
toggleSidenav() {
this.toggle = !this.toggle;
this.sidenavService.toggleSidenav();
}
toggleNavBar() {
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') {
setTitle(value: string = 'workbaskets') {
if (value.indexOf('workbaskets') === 0) {
this.title = this.titleWorkbaskets;
} else if (value.indexOf('classifications') === 0) {
@ -114,13 +53,4 @@ export class NavBarComponent implements OnInit, OnDestroy {
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-wrap col-xs-offset-5">
<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 class="icon">
<div class="icon__wrap">
<svg-icon class="blue big" src="./assets/icons/user.svg"></svg-icon>
</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 {
margin-top: 85px;
color: white;
> span {
margin: 0 15px 0 15px;
font-size: 20px;
@ -18,13 +19,15 @@
.icon {
width: 100%;
position: relative;
& > .icon-wrap {
position: absolute;
& > .icon__wrap {
border-radius: 50%;
border: solid 2px #22a39f;
width: 72px;
height: 72px;
overflow: hidden;
left: 35%;
//overflow: hidden;
background-color: #175263;
position: absolute;
-webkit-box-shadow: 0px 3px 3px #416b6a;
@ -36,7 +39,7 @@
overflow: hidden;
background-color: white;
position: absolute;
margin: 2px;
margin: 7px -0px 0px 2px;
}
}
}
@ -53,6 +56,11 @@
box-shadow: 0px 3px 3px #416b6a;
}
.user-info_roles {
margin-left: 16px;
color: white;
}
button.transparent {
border: none;
background-color: transparent;
@ -68,3 +76,14 @@ button.transparent {
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/icon?family=Material+Icons" rel="stylesheet">
</head>
<body>
<taskana-root></taskana-root>
<taskana-root></taskana-root>
</body>
</html>
</html>

View File

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

View File

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