diff --git a/web/.vscode/settings.json b/web/.vscode/settings.json index 7880738e9..a422b16c8 100644 --- a/web/.vscode/settings.json +++ b/web/.vscode/settings.json @@ -11,5 +11,6 @@ "[html]": { "editor.formatOnSave": true, "editor.defaultFormatter": "vscode.html-language-features" - } + }, + "eslint.enable": false } \ No newline at end of file diff --git a/web/cypress/cypress/plugins/index.js b/web/cypress/cypress/plugins/index.js index fd170fba6..dffed2532 100644 --- a/web/cypress/cypress/plugins/index.js +++ b/web/cypress/cypress/plugins/index.js @@ -14,4 +14,4 @@ module.exports = (on, config) => { // `on` is used to hook into various events Cypress emits // `config` is the resolved Cypress config -} +}; diff --git a/web/cypress/cypress/support/index.js b/web/cypress/cypress/support/index.js index d68db96df..37a498fb5 100644 --- a/web/cypress/cypress/support/index.js +++ b/web/cypress/cypress/support/index.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import './commands' +import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/web/cypress/fixtures/example.json b/web/cypress/fixtures/example.json index da18d9352..02e425437 100644 --- a/web/cypress/fixtures/example.json +++ b/web/cypress/fixtures/example.json @@ -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" -} \ No newline at end of file +} diff --git a/web/cypress/support/index.js b/web/cypress/support/index.js index d076cec9f..37a498fb5 100644 --- a/web/cypress/support/index.js +++ b/web/cypress/support/index.js @@ -14,7 +14,7 @@ // *********************************************************** // Import commands.js using ES2015 syntax: -import "./commands"; +import './commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.scss b/web/src/app/administration/components/access-items-management/access-items-management.component.scss index babc13209..2b73f1d72 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.scss +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.scss @@ -44,4 +44,3 @@ td { .table__access-item-groups { height: 80px; } - diff --git a/web/src/app/administration/components/classification-details/classification-details.component.scss b/web/src/app/administration/components/classification-details/classification-details.component.scss index be6a2fd56..00c180975 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.scss +++ b/web/src/app/administration/components/classification-details/classification-details.component.scss @@ -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; } - diff --git a/web/src/app/administration/components/classification-overview/classification-overview.component.scss b/web/src/app/administration/components/classification-overview/classification-overview.component.scss index 1799977d8..0c9cc6aa0 100644 --- a/web/src/app/administration/components/classification-overview/classification-overview.component.scss +++ b/web/src/app/administration/components/classification-overview/classification-overview.component.scss @@ -6,5 +6,5 @@ align-items: stretch; } taskana-administration-classification-details { - width: 100% + width: 100%; } diff --git a/web/src/app/administration/components/import-export/import-export.component.scss b/web/src/app/administration/components/import-export/import-export.component.scss index bcf0f7455..e6728976a 100644 --- a/web/src/app/administration/components/import-export/import-export.component.scss +++ b/web/src/app/administration/components/import-export/import-export.component.scss @@ -5,5 +5,5 @@ mat-icon { margin-left: 3px; } button { - color: #555 + color: #555; } diff --git a/web/src/app/administration/components/tree/tree.component.scss b/web/src/app/administration/components/tree/tree.component.scss index d4f109b9c..9dc7adbe1 100644 --- a/web/src/app/administration/components/tree/tree.component.scss +++ b/web/src/app/administration/components/tree/tree.component.scss @@ -6,4 +6,3 @@ svg-icon { fill: #555; top: -5px !important; } - diff --git a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss index 85ace22bc..330f4fa54 100644 --- a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss +++ b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss @@ -2,4 +2,3 @@ width: 100%; display: flex; } - diff --git a/web/src/app/app.component.html b/web/src/app/app.component.html index b59930a8d..4d5ac4c67 100644 --- a/web/src/app/app.component.html +++ b/web/src/app/app.component.html @@ -1,10 +1,29 @@ - - -
-
+ + +
+ +
+ + +
+

Taskana version: {{version}}

+
+
+ + +
+
- + + +
+
- -
+ + \ No newline at end of file diff --git a/web/src/app/app.component.scss b/web/src/app/app.component.scss index e69de29bb..8b34a01af 100644 --- a/web/src/app/app.component.scss +++ b/web/src/app/app.component.scss @@ -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; +} diff --git a/web/src/app/app.component.ts b/web/src/app/app.component.ts index 19c9f8432..491213606 100644 --- a/web/src/app/app.component.ts +++ b/web/src/app/app.component.ts @@ -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() { diff --git a/web/src/app/app.module.ts b/web/src/app/app.module.ts index 44b10764c..c037ed7e8 100644 --- a/web/src/app/app.module.ts +++ b/web/src/app/app.module.ts @@ -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 { return (): Promise => startupService.load(); @@ -97,7 +113,8 @@ export function startupServiceFactory(startupService: StartupService): () => Pro FormsValidatorService, UploadService, NotificationService, - ClassificationCategoriesService + ClassificationCategoriesService, + SidenavService ], bootstrap: [AppComponent] }) diff --git a/web/src/app/shared/components/master-and-detail/master-and-detail.component.scss b/web/src/app/shared/components/master-and-detail/master-and-detail.component.scss index 6366de648..a0c84dda9 100644 --- a/web/src/app/shared/components/master-and-detail/master-and-detail.component.scss +++ b/web/src/app/shared/components/master-and-detail/master-and-detail.component.scss @@ -1,4 +1,3 @@ - .master-detail { min-width: 100vw; } diff --git a/web/src/app/shared/components/nav-bar/nav-bar.component.html b/web/src/app/shared/components/nav-bar/nav-bar.component.html index efe0d1d37..d33a82a64 100644 --- a/web/src/app/shared/components/nav-bar/nav-bar.component.html +++ b/web/src/app/shared/components/nav-bar/nav-bar.component.html @@ -1,78 +1,11 @@ - \ No newline at end of file diff --git a/web/src/app/shared/components/nav-bar/nav-bar.component.scss b/web/src/app/shared/components/nav-bar/nav-bar.component.scss index 513953a12..a3f63570b 100644 --- a/web/src/app/shared/components/nav-bar/nav-bar.component.scss +++ b/web/src/app/shared/components/nav-bar/nav-bar.component.scss @@ -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; } diff --git a/web/src/app/shared/components/nav-bar/nav-bar.component.spec.ts b/web/src/app/shared/components/nav-bar/nav-bar.component.spec.ts new file mode 100644 index 000000000..486ce3880 --- /dev/null +++ b/web/src/app/shared/components/nav-bar/nav-bar.component.spec.ts @@ -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 => ({ + toggleSidenav: jest.fn().mockReturnValue(of()) + }) +); + +const SelectedRouteServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + getSelectedRoute: jest.fn().mockReturnValue(of()) + }) +); + +describe('NavBarComponent', () => { + let component: NavBarComponent; + let fixture: ComponentFixture; + 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); + }); +}); diff --git a/web/src/app/shared/components/nav-bar/nav-bar.component.ts b/web/src/app/shared/components/nav-bar/nav-bar.component.ts index 25edd9cf7..bce0a4436 100644 --- a/web/src/app/shared/components/nav-bar/nav-bar.component.ts +++ b/web/src/app/shared/components/nav-bar/nav-bar.component.ts @@ -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 = []; - 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(); - } - } } diff --git a/web/src/app/shared/components/sidenav-list/sidenav-list.component.html b/web/src/app/shared/components/sidenav-list/sidenav-list.component.html new file mode 100644 index 000000000..d1b23e628 --- /dev/null +++ b/web/src/app/shared/components/sidenav-list/sidenav-list.component.html @@ -0,0 +1,16 @@ + + Administration + Workbaskets + Classifications + Access Items + Monitor + Workplace + History + \ No newline at end of file diff --git a/web/src/app/shared/components/sidenav-list/sidenav-list.component.scss b/web/src/app/shared/components/sidenav-list/sidenav-list.component.scss new file mode 100644 index 000000000..33a4b78d6 --- /dev/null +++ b/web/src/app/shared/components/sidenav-list/sidenav-list.component.scss @@ -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; +} diff --git a/web/src/app/shared/components/sidenav-list/sidenav-list.component.spec.ts b/web/src/app/shared/components/sidenav-list/sidenav-list.component.spec.ts new file mode 100644 index 000000000..c8ecdc7f6 --- /dev/null +++ b/web/src/app/shared/components/sidenav-list/sidenav-list.component.spec.ts @@ -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 => ({ + toggleSidenav: jest.fn().mockReturnValue(of()) + }) +); + +const TaskanaEngingeServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + hasRole: jest.fn().mockReturnValue(of()), + isHistoryProviderEnabled: jest.fn().mockReturnValue(of()) + }) +); + +describe('SidenavListComponent', () => { + let component: SidenavListComponent; + let fixture: ComponentFixture; + 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); + }); +}); diff --git a/web/src/app/shared/components/sidenav-list/sidenav-list.component.ts b/web/src/app/shared/components/sidenav-list/sidenav-list.component.ts new file mode 100644 index 000000000..c31e81f6f --- /dev/null +++ b/web/src/app/shared/components/sidenav-list/sidenav-list.component.ts @@ -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(); + } +} diff --git a/web/src/app/shared/components/user-information/user-information.component.html b/web/src/app/shared/components/user-information/user-information.component.html index c6f851c16..263bbb517 100644 --- a/web/src/app/shared/components/user-information/user-information.component.html +++ b/web/src/app/shared/components/user-information/user-information.component.html @@ -1,16 +1,14 @@ -
-
-
- -
-
- -
- {{roles}} +
+
+
+ +
+ {{roles}} +
\ No newline at end of file diff --git a/web/src/app/shared/components/user-information/user-information.component.scss b/web/src/app/shared/components/user-information/user-information.component.scss index ef831ad54..1d1b90860 100644 --- a/web/src/app/shared/components/user-information/user-information.component.scss +++ b/web/src/app/shared/components/user-information/user-information.component.scss @@ -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; +} diff --git a/web/src/app/shared/components/user-information/user-information.component.spec.ts b/web/src/app/shared/components/user-information/user-information.component.spec.ts new file mode 100644 index 000000000..a4fc29ce1 --- /dev/null +++ b/web/src/app/shared/components/user-information/user-information.component.spec.ts @@ -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 => ({ + hasRole: jest.fn().mockReturnValue(of()), + isHistoryProviderEnabled: jest.fn().mockReturnValue(of()) + }) +); + +describe('UserInformationComponent', () => { + let component: UserInformationComponent; + let fixture: ComponentFixture; + 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); + }); +}); diff --git a/web/src/app/shared/services/sidenav/sidenav.service.spec.ts b/web/src/app/shared/services/sidenav/sidenav.service.spec.ts new file mode 100644 index 000000000..7ea3a5514 --- /dev/null +++ b/web/src/app/shared/services/sidenav/sidenav.service.spec.ts @@ -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(); + })); +}); diff --git a/web/src/app/shared/services/sidenav/sidenav.service.ts b/web/src/app/shared/services/sidenav/sidenav.service.ts new file mode 100644 index 000000000..bf66f43a7 --- /dev/null +++ b/web/src/app/shared/services/sidenav/sidenav.service.ts @@ -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; + } +} diff --git a/web/src/index.html b/web/src/index.html index 47e708fe6..4203c1015 100644 --- a/web/src/index.html +++ b/web/src/index.html @@ -10,8 +10,9 @@ + - + - + \ No newline at end of file diff --git a/web/src/tsconfig.app.json b/web/src/tsconfig.app.json index 9ced1846f..17f075287 100644 --- a/web/src/tsconfig.app.json +++ b/web/src/tsconfig.app.json @@ -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 } diff --git a/web/tsconfig.spec.json b/web/tsconfig.spec.json index b2b20d02c..7f0fc2841 100644 --- a/web/tsconfig.spec.json +++ b/web/tsconfig.spec.json @@ -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"] }