TSK-1399: Workbasket List and Details MD (#1301)

* TSK-1399: Comments in wb-list for better understanding

* TSK-1399: Upgrade angular material 9, added basic selection list

* TSK-1399: implement basic mat list for workbasket list

* TSK-1399: Action buttons and sorting in wb-toolbar

* TSK-1399: Update workbasket list item design

* TSK-1399: Updated workbasket-list toolbar

* TSK-1399: Import-export buttons with correct matToolTip

* TSK-1399: rework pagination component

* TSK-1399: Refactored the filter within filter options

* TSK-1399: Refactored workbasket filter

* TSK-1399: Redesigned workbasket filter

* TSK-1399: Fixed wb-list-toolbar test

* TSK-1399: Added todo comment so we will not forget :)

* TSK-1399: fixed pagination sometimes displaying wrong numbers

* TSK-1399: Partially refactored wb-information component

* TSK-1399: implement new workbasket details toolbar without functionality

* TSK-1399: commented old action toolbar

* TSK-1399: update custom pagination component

* TSK-1399: update pagination and workbasket list layout CSS

* TSK-1399: Update overall layout

* TSK-1399: fixed bugs regarding workbasket list

* TSK-1399: added store states for selecting tabs

* TSK-1399: bind workbasket details action button with workbasket information using ngxs store

* TSK-1399: Update pagination to display correctly while workbaskets being loaded

* TSK-1399: Implement functionality in workbasket information for action toolbar buttons

* TSK-1399: fixed test for workbasket information

* TSK-1399: fixed workbasket-list tests

* TSK-1399: fixed tests in workbasket details, list-toolbar

* TSK-1399: fixed all affected tests during MD redesign

* TSK-1399: fixed workbasket information not rendering full height

* TSK-1399: fixed merging issues with navbar, remove unnecessary css

Co-authored-by: Sofie Hofmann <29145005+sofie29@users.noreply.github.com>
This commit is contained in:
Chi Nguyen 2020-10-15 12:15:29 +02:00 committed by GitHub
parent 7c83c87f32
commit 356e41ea27
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
66 changed files with 1182 additions and 693 deletions

18
web/package-lock.json generated
View File

@ -321,12 +321,11 @@
"integrity": "sha512-tphpf9QHnOPoL2Jl7KpR+R5aHNW3oifLEmRUTajJYJGvo1uzdUDE82+V9OGOinxJsYseCth9gYJhN24aYTB9NA=="
},
"@angular/cdk": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz",
"integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==",
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-9.2.4.tgz",
"integrity": "sha512-iw2+qHMXHYVC6K/fttHeNHIieSKiTEodVutZoOEcBu9rmRTGbLB26V/CRsfIRmA1RBk+uFYWc6UQZnMC3RdnJQ==",
"requires": {
"parse5": "^5.0.0",
"tslib": "^1.7.1"
"parse5": "^5.0.0"
}
},
"@angular/cli": {
@ -634,12 +633,9 @@
"integrity": "sha512-LhjnZlC4WEsEsAJfOZLte+Lks3WBAFVeRv2lzoQNFVr/IMzBNDVfjEaaSqKF1cei3cjY39Df2nYDMJM7HfqbJA=="
},
"@angular/material": {
"version": "8.2.3",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-8.2.3.tgz",
"integrity": "sha512-SOczkIaqes+r+9XF/UUiokidfFKBpHkOPIaFK857sFD0FBNPvPEpOr5oHKCG3feERRwAFqHS7Wo2ohVEWypb5A==",
"requires": {
"tslib": "^1.7.1"
}
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/@angular/material/-/material-9.2.4.tgz",
"integrity": "sha512-LkoTXE6B0slvMhvfZDdPWaz4yaYLkaAp5VSPunI9pxGsPxzqEV9e210wC1/sjG/76Nk8Ep7/2z9XKac8Q9bMwA=="
},
"@angular/platform-browser": {
"version": "9.1.12",

View File

@ -20,11 +20,11 @@
"private": true,
"dependencies": {
"@angular/animations": "9.1.12",
"@angular/cdk": "8.2.3",
"@angular/cdk": "9.2.4",
"@angular/common": "9.1.12",
"@angular/core": "9.1.12",
"@angular/forms": "9.1.12",
"@angular/material": "8.2.3",
"@angular/material": "9.2.4",
"@angular/platform-browser": "9.1.12",
"@angular/platform-browser-dynamic": "9.1.12",
"@angular/router": "9.1.12",

View File

@ -5,7 +5,6 @@ import { AngularSvgIconModule } from 'angular-svg-icon';
import { AlertModule, TypeaheadModule } from 'ngx-bootstrap';
import { SharedModule } from 'app/shared/shared.module';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { TreeModule } from 'angular-tree-component';
import { ClassificationTypesSelectorComponent } from 'app/administration/components/classification-types-selector/classification-types-selector.component';
import { ClassificationCategoriesService } from 'app/shared/services/classification-categories/classification-categories.service';
@ -44,6 +43,9 @@ import { AdministrationOverviewComponent } from './components/administration-ove
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatDividerModule } from '@angular/material/divider';
import { MatListModule } from '@angular/material/list';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
const MODULES = [
CommonModule,
@ -88,7 +90,10 @@ const DECLARATIONS = [
MatTabsModule,
MatInputModule,
MatTooltipModule,
MatDividerModule
MatDividerModule,
MatListModule,
MatProgressBarModule,
MatToolbarModule
],
providers: [
ClassificationDefinitionService,

View File

@ -1,6 +1,6 @@
<div class="classification-details">
<taskana-shared-spinner [isRunning]="requestInProgress" class="floating"
(spinnerIsRunning)="spinnerRunning($event)"></taskana-shared-spinner>
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
<div class="classification-details__wrapper" id="classification-details" *ngIf="classification && !spinnerIsRunning">
<!-- TITLE + ACTION BUTTONS -->
@ -50,6 +50,7 @@
<h6 class="classification-details__subheading" style="margin-top: 65px;"> General </h6>
<mat-divider class="classification-details__horizontal-line"> </mat-divider>
<!-- KEY -->
<mat-form-field appearance="outline">
<mat-label>Key</mat-label>
<label for="classification-key"></label>

View File

@ -15,7 +15,7 @@
padding-top: 0.5rem;
}
.classification-details__action-toolbar {
width: calc(100% - 450px);
width: calc(100% - 500px);
position: fixed;
padding: 12px 32px 12px 24px;
background-color: #fff;

View File

@ -30,6 +30,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@Component({ selector: 'taskana-shared-spinner', template: '' })
class SpinnerStub {
@ -128,6 +129,7 @@ describe('ClassificationDetailsComponent', () => {
MatInputModule,
MatOptionModule,
MatSelectModule,
MatProgressBarModule,
MatMenuModule,
BrowserAnimationsModule
],
@ -250,9 +252,6 @@ describe('ClassificationDetailsComponent', () => {
});
/* HTML */
it('should show spinner component', () => {
expect(debugElement.nativeElement.querySelector('taskana-shared-spinner')).toBeTruthy();
});
it('should not show details when spinner is running', () => {
component.spinnerIsRunning = true;

View File

@ -10,12 +10,13 @@
</button>
<taskana-administration-import-export
class="classification-list__import-export" [currentSelection]="taskanaType.CLASSIFICATIONS">
class="classification-list__import-export" [currentSelection]="taskanaType.CLASSIFICATIONS" [parentComponent]="'classifications'">
</taskana-administration-import-export>
<span class="workbasket-details__spacer" style="flex: 1 1 auto"> </span>
<button mat-stroked-button matTooltip="Display filter options" (click)="displayFilter()">
<mat-icon *ngIf="!showFilter">search</mat-icon>
<mat-icon *ngIf="showFilter" color="warn">clear</mat-icon>
<button mat-stroked-button matTooltip="Display filter options" (click)="displayFilter()" style="color: #555">
<mat-icon *ngIf="!showFilter">filter_list</mat-icon>
<mat-icon *ngIf="showFilter">keyboard_arrow_up</mat-icon>
</button>
</div>
@ -60,8 +61,8 @@
<!-- CLASSIFICATION TREE -->
<taskana-shared-spinner [isRunning]="requestInProgress"
positionClass="centered-spinner-whole-screen"></taskana-shared-spinner>
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
<taskana-administration-tree *ngIf="(classifications && classifications.length) else empty_classifications"
[filterText]="inputValue" [filterIcon]="selectedCategory"
(switchTaskanaSpinnerEmit)="requestInProgress=$event"></taskana-administration-tree>

View File

@ -2,7 +2,7 @@
.classification-list {
height: calc(100vh - 55px);
width: 450px;
width: 500px;
}
.classification-list__action-toolbar {
padding: 0 16px;
@ -40,6 +40,10 @@
top: -2px;
}
.category-filter__filter-button {
color: #555;
}
.filter__input {
width: 100%;
margin-right: 12px;

View File

@ -18,10 +18,12 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatInputModule } from '@angular/material/input';
import { By } from '@angular/platform-browser';
import { MatProgressBarModule } from '@angular/material/progress-bar';
@Component({ selector: 'taskana-administration-import-export', template: '' })
class ImportExportStub {
@Input() currentSelection: TaskanaType;
@Input() parentComponent: string;
}
@Component({ selector: 'taskana-administration-classification-types-selector', template: '' })
@ -88,7 +90,8 @@ describe('ClassificationListComponent', () => {
MatMenuModule,
MatFormFieldModule,
MatInputModule,
BrowserAnimationsModule
BrowserAnimationsModule,
MatProgressBarModule
],
declarations: [
ClassificationListComponent,
@ -188,10 +191,6 @@ describe('ClassificationListComponent', () => {
});
/* HTML: CLASSIFICATION TREE */
it('should display spinner component', () => {
expect(debugElement.nativeElement.querySelector('taskana-shared-spinner')).toBeTruthy();
});
it('should display tree component when classifications exist', () => {
component.classifications = [{ classificationId: '1' }, { classificationId: '2' }];
fixture.detectChanges();

View File

@ -1,23 +1,23 @@
<button mat-stroked-button class="mr-1" matTooltip="Import classification" [ngClass]="{disabled: uploadService?.isInUse}" (click)="selectedFile.click()" title="Import">
Import
<mat-icon>cloud_upload</mat-icon>
</button>
<form class="hidden" enctype="multipart/form-data" method="post">
<input #selectedFile type="file" accept=".json" (change)="uploadFile()" class="hide" />
</form>
<button mat-stroked-button class="mr-1" matTooltip="Export classification" [matMenuTriggerFor]="menu" [ngClass]="{disabled: uploadService?.isInUse}" title="Export">
Export
<mat-icon>cloud_download</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item href="javascript:void(0)" (click)="export()">
All Domains
<div class="import-export">
<button mat-stroked-button class="mr-1" matTooltip="Import {{parentComponent}}" [ngClass]="{disabled: uploadService?.isInUse}" (click)="selectedFile.click()" title="Import">
Import
<mat-icon>cloud_upload</mat-icon>
</button>
<button mat-menu-item *ngFor="let domain of domains" href="javascript:void(0)" (click)="export(domain)">
{{domain === '' ? 'Master' : domain}}
</button>
</mat-menu>
<form class="hidden" enctype="multipart/form-data" method="post">
<input #selectedFile type="file" accept=".json" (change)="uploadFile()" class="hide" />
</form>
<button mat-stroked-button class="mr-1" matTooltip="Export {{parentComponent}}" [matMenuTriggerFor]="menu" [ngClass]="{disabled: uploadService?.isInUse}" title="Export">
Export
<mat-icon>cloud_download</mat-icon>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item href="javascript:void(0)" (click)="export()">
All Domains
</button>
<button mat-menu-item *ngFor="let domain of domains" href="javascript:void(0)" (click)="export(domain)">
{{domain === '' ? 'Master' : domain}}
</button>
</mat-menu>
</div>

View File

@ -1,9 +1,11 @@
.import-export {
display: flex;
}
.hide {
display: none;
}
mat-icon {
margin-left: 3px;
}
button {
color: #555;
}

View File

@ -17,6 +17,7 @@ import { NotificationService } from '../../../shared/services/notifications/noti
})
export class ImportExportComponent implements OnInit {
@Input() currentSelection: TaskanaType;
@Input() parentComponent: string;
@ViewChild('selectedFile', { static: true })
selectedFileInput;

View File

@ -1,3 +1,3 @@
<svg-icon class="{{selected? 'white': 'blue' }} small" src="./assets/icons/{{getIconPath(type)}}" data-toggle="tooltip"
<svg-icon class="{{selected? 'white': 'grayIcon' }} {{size}}" src="./assets/icons/{{getIconPath(type)}}"
[title]="!type? 'All' : type"></svg-icon>
{{text}}

Before

Width:  |  Height:  |  Size: 175 B

After

Width:  |  Height:  |  Size: 160 B

View File

@ -0,0 +1,8 @@
.large {
height: 24px;
width: 24px;
}
.grayIcon {
fill: #555;
}

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input } from '@angular/core';
import { Component, Input } from '@angular/core';
import { ICONTYPES } from 'app/shared/models/icon-types';
@Component({
@ -19,6 +19,9 @@ export class IconTypeComponent {
@Input()
text: string;
@Input()
size = 'small';
public static get allTypes(): Map<string, string> {
return new Map([
['PERSONAL', 'Personal'],

View File

@ -1,6 +1,7 @@
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div *ngIf="workbasket" id="wb-information">
<!-- ACTION TOOLBAR -->
<!--
<div class="panel-heading">
<div class="pull-right btn-group">
<button type="button" (click)="onSubmit()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
@ -14,9 +15,10 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4>
</div>
-->
<!-- ACCESS ITEMS -->
<div class="panel-body">
<div class="workbasket-access-items">
<form [formGroup]="AccessItemsForm">
<table formArrayName="accessItemsGroups" id="table-access-items" class="table table-striped table-center">

View File

@ -1,5 +1,8 @@
@import '../../../../theme/colors';
.workbasket-access-items {
max-width: calc(100vw - 500px);
}
td > input[type='checkbox'] {
margin-top: 0;
}

View File

@ -158,11 +158,8 @@ describe('WorkbasketAccessItemsComponent', () => {
});
it('should undo changes when undo button is clicked', () => {
const undoButton = debugElement.nativeElement.querySelector('button.undo-button');
const clearSpy = jest.spyOn(component, 'clear');
expect(undoButton.title).toMatch('Undo Changes');
undoButton.click();
component.clear();
expect(clearSpy).toHaveBeenCalled();
});

View File

@ -1,34 +1,53 @@
<div class="container container-scrollable" style="min-width: 70vw;">
<taskana-shared-spinner [isRunning]="requestInProgress"></taskana-shared-spinner>
<div id="workbasket-details" *ngIf="workbasket && !requestInProgress">
<ul class="nav nav-tabs" role="tablist">
<li role="presentation" (click)="selectTab('information')" [ngClass]="{'active':tabSelected === 'information'}">
<a aria-controls="work baskets" role="tab" aria-expanded="true">
Information</a>
</li>
<li role="presentation" (click)="selectTab('accessItems')" [ngClass]="{
'disabled': action ==='CREATE',
'active':tabSelected === 'accessItems'}">
<a aria-controls="Acccess" role="tab" aria-expanded="true">
Access</a>
</li>
<li role="presentation" (click)="selectTab('distributionTargets')" [ngClass]="{
'disabled': action ==='CREATE',
'active':tabSelected === 'distributionTargets'}">
<a aria-controls="distribution targets" role="tab" data-toggle="tab" aria-expanded="true">
Distribution targets</a>
</li>
</ul>
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" [ngClass]="{'active':tabSelected === 'information'}" id="work-baskets">
<taskana-administration-workbasket-information [workbasket]="workbasket" [action]="action"></taskana-administration-workbasket-information>
</div>
<div role="tabpanel" class="tab-pane" id="access-items" [ngClass]="{'active':tabSelected === 'accessItems'}">
<taskana-administration-workbasket-access-items [workbasket]="workbasket" [action]="action" [active]="tabSelected"></taskana-administration-workbasket-access-items>
</div>
<div role="tabpanel" class="tab-pane" id="distribution-targets" [ngClass]="{'active':tabSelected === 'distributionTargets'}">
<taskana-administration-workbasket-distribution-targets [workbasket]="workbasket" [action]="action" [active]="tabSelected"></taskana-administration-workbasket-distribution-targets>
</div>
</div>
</div>
<div class="workbasket-details">
<mat-toolbar class="workbasket-details__toolbar">
<h4 class="workbasket-details__title">{{workbasket.name}}&nbsp;
<span class="workbasket-details__title-badge" *ngIf="!workbasket.workbasketId"> {{ badgeMessage }}</span>
</h4>
<span class="workbasket-details__spacer"></span>
<button mat-button class="workbasket-details__button workbasket-details__save-button" matTooltip="Save changes in current workbasket" (click)="onSubmit()">
Save
<mat-icon class="md-20">save</mat-icon>
</button>
<button mat-stroked-button class="workbasket-details__button" matTooltip="Revert changes to previous saved state" (click)="onRestore()">
Undo Changes
<mat-icon class="button__green-blue md-20">restore</mat-icon>
</button>
<button mat-stroked-button [matMenuTriggerFor]="buttonMenu" matTooltip="More actions" class="action-toolbar__button" id="action-toolbar__more-buttons">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #buttonMenu="matMenu">
<button mat-menu-item class="workbasket-details__dropdown" matTooltip="Copy current values to create new workbasket" (click)="onCopy()">
<mat-icon class="button__green-blue">content_copy</mat-icon>
<span>Copy</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown" matTooltip="Remove this workbasket as distribution target" (click)="onRemoveAsDistributionTarget()">
<mat-icon class="button__red">remove_circle_outline</mat-icon>
<span>Remove as distribution target</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown" matTooltip="Delete this workbasket" (click)="onRemoveWorkbasket()">
<mat-icon class="button__red">delete</mat-icon>
<span>Delete</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown" style="border-bottom-style: none;" matTooltip="Close this workbasket and discard all changes" (click)="onClose()">
<mat-icon>close</mat-icon>
<span>Close</span>
</button>
</mat-menu>
</mat-toolbar>
<mat-tab-group animationDuration="0ms" (selectedIndexChange)="selectComponent($event)">
<mat-tab label="Information">
<taskana-administration-workbasket-information [workbasket]="workbasket" [action]="action"></taskana-administration-workbasket-information>
</mat-tab>
<mat-tab label="Access">
<taskana-administration-workbasket-access-items [workbasket]="workbasket" [action]="action" [active]="tabSelected"></taskana-administration-workbasket-access-items>
</mat-tab>
<mat-tab label="Distribution Targets">
<taskana-administration-workbasket-distribution-targets [workbasket]="workbasket" [action]="action" [active]="tabSelected"></taskana-administration-workbasket-distribution-targets>
</mat-tab>
</mat-tab-group>
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
</div>

View File

@ -0,0 +1,36 @@
@import 'src/theme/_colors.scss';
.workbasket-details {
width: 100%;
height: calc(100vh - 100px);
}
.workbasket-details__toolbar {
background-color: white;
padding: 16px 36px 12px 24px;
}
.workbasket-details__title {
font-size: 1.5rem;
padding: 4px;
}
.workbasket-details__title-badge {
background-color: $brown;
border-radius: 4px;
}
.workbasket-details__spacer {
flex: 1 1 auto;
}
.workbasket-details__button {
margin-right: 6px;
}
.workbasket-details__save-button {
background-color: $aquamarine;
color: white;
}
.button__green-blue {
color: $aquamarine;
}
.button__red {
color: $invalid;
}

View File

@ -15,11 +15,20 @@ import { RequestInProgressService } from '../../../shared/services/request-in-pr
import { SelectedRouteService } from '../../../shared/services/selected-route/selected-route';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { selectedWorkbasketMock } from '../../../shared/store/mock-data/mock-store';
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
import {
engineConfigurationMock,
selectedWorkbasketMock,
workbasketReadStateMock
} from '../../../shared/store/mock-data/mock-store';
import { StartupService } from '../../../shared/services/startup/startup.service';
import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from '../../../shared/services/window/window.service';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatTabsModule } from '@angular/material/tabs';
import { MatMenuModule } from '@angular/material/menu';
import { MatToolbarModule } from '@angular/material/toolbar';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@Component({ selector: 'taskana-shared-spinner', template: '' })
class SpinnerStub {
@ -74,7 +83,13 @@ describe('WorkbasketDetailsComponent', () => {
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
MatSnackBarModule,
MatDialogModule
MatDialogModule,
MatIconModule,
MatProgressBarModule,
MatTabsModule,
MatMenuModule,
MatToolbarModule,
BrowserAnimationsModule
],
declarations: [
WorkbasketDetailsComponent,
@ -100,6 +115,10 @@ describe('WorkbasketDetailsComponent', () => {
component = fixture.debugElement.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
workbasket: workbasketReadStateMock
});
fixture.detectChanges();
}));
@ -107,34 +126,12 @@ describe('WorkbasketDetailsComponent', () => {
expect(component).toBeTruthy();
});
it('should display loading spinner while content loads', () => {
component.requestInProgress = true;
fixture.detectChanges();
const spinner = debugElement.nativeElement.querySelector('taskana-shared-spinner');
expect(spinner).toBeTruthy();
expect(spinner.style.display).toContain('');
});
it('should render workbasket-details when workbasket exists and request is not in progress', () => {
component.workbasket = { workbasketId: '1' };
component.requestInProgress = false;
fixture.detectChanges();
const workbasketDetails = debugElement.nativeElement.querySelector('#workbasket-details');
expect(workbasketDetails).toBeTruthy();
});
it('should render information, access items and distribution targets components', () => {
it('should render information component when workbasket details is opened', () => {
component.workbasket = { workbasketId: '1' };
component.requestInProgress = false;
fixture.detectChanges();
const information = debugElement.nativeElement.querySelector('taskana-administration-workbasket-information');
const accessItems = debugElement.nativeElement.querySelector('taskana-administration-workbasket-access-items');
const distributionTargets = debugElement.nativeElement.querySelector(
'taskana-administration-workbasket-distribution-targets'
);
expect(information).toBeTruthy();
expect(accessItems).toBeTruthy();
expect(distributionTargets).toBeTruthy();
});
it('should render new workbasket when action is CREATE', () => {

View File

@ -1,27 +1,35 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Component, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket';
import { ACTION } from 'app/shared/models/action';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { ImportExportService } from 'app/administration/services/import-export.service';
import { Select } from '@ngxs/store';
import { Select, Store } from '@ngxs/store';
import { takeUntil } from 'rxjs/operators';
import { WorkbasketAndAction, WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { TaskanaDate } from '../../../shared/util/taskana.date';
import { ICONTYPES } from '../../../shared/models/icon-types';
import {
DeselectWorkbasket,
OnButtonPressed,
SelectComponent
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { ButtonAction } from '../../models/button-action';
@Component({
selector: 'taskana-administration-workbasket-details',
templateUrl: './workbasket-details.component.html'
templateUrl: './workbasket-details.component.html',
styleUrls: ['./workbasket-details.component.scss']
})
export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
export class WorkbasketDetailsComponent implements OnInit, OnDestroy, OnChanges {
workbasket: Workbasket;
workbasketCopy: Workbasket;
selectedId: string;
requestInProgress = false;
action: ACTION;
tabSelected = 'information';
badgeMessage = '';
@Select(WorkbasketSelectors.selectedWorkbasket)
selectedWorkbasket$: Observable<Workbasket>;
@ -38,7 +46,8 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
private route: ActivatedRoute,
private router: Router,
private domainService: DomainService,
private importExportService: ImportExportService
private importExportService: ImportExportService,
private store: Store
) {}
ngOnInit() {
@ -47,11 +56,13 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
if (this.action === ACTION.CREATE) {
this.tabSelected = 'information';
this.selectedId = undefined;
this.badgeMessage = 'Creating new workbasket';
this.initWorkbasket();
} else if (this.action === ACTION.COPY) {
// delete this.workbasket.key;
this.workbasketCopy = this.workbasket;
this.getWorkbasketInformation();
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
} else if (typeof selectedWorkbasketAndAction.selectedWorkbasket !== 'undefined') {
this.workbasket = { ...selectedWorkbasketAndAction.selectedWorkbasket };
this.getWorkbasketInformation(this.workbasket);
@ -68,6 +79,8 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
});
}
ngOnChanges(changes?: SimpleChanges) {}
addDateToWorkbasket(workbasket: Workbasket) {
const date = TaskanaDate.getDate();
workbasket.created = date;
@ -130,6 +143,48 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
});
}
selectComponent(index) {
this.store.dispatch(new SelectComponent(index));
switch (index) {
case 0:
this.selectTab('information');
break;
case 1:
this.selectTab('access-items');
break;
case 2:
this.selectTab('distribution-targets');
break;
default:
break;
}
}
onSubmit() {
this.store.dispatch(new OnButtonPressed(ButtonAction.SAVE));
}
onRestore() {
this.store.dispatch(new OnButtonPressed(ButtonAction.UNDO));
}
onCopy() {
this.store.dispatch(new OnButtonPressed(ButtonAction.COPY));
}
onRemoveAsDistributionTarget() {
this.store.dispatch(new OnButtonPressed(ButtonAction.REMOVE_AS_DISTRIBUTION_TARGETS));
}
onRemoveWorkbasket() {
this.store.dispatch(new OnButtonPressed(ButtonAction.DELETE));
}
onClose() {
this.store.dispatch(new OnButtonPressed(ButtonAction.CLOSE));
this.store.dispatch(new DeselectWorkbasket());
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();

View File

@ -1,5 +1,6 @@
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<!-- ACTION TOOLBAR-->
<!--
<div class="panel-heading">
<div class="pull-right btn-group">
<button type="button" (click)="onSave()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save"
@ -15,7 +16,7 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4>
</div>
-->
<!-- DISTRIBUTION TABLE-->
<div #panelBody class="panel-body">

View File

@ -1,5 +1,6 @@
<taskana-shared-spinner [isRunning]="requestInProgress" class="floating"></taskana-shared-spinner>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
<div *ngIf="workbasket" id="wb-information">
<!--
<div class="panel-heading">
<div class="pull-right btn-group">
<button type="button" (click)="onSubmit()" data-toggle="tooltip" title="Save"
@ -28,36 +29,97 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4>
</div>
-->
<div class="panel-body">
<div class="workbasket-information-wrapper">
<ng-form #WorkbasketForm="ngForm">
<div class="col-md-6">
<!-- GENERAL FIELDS -->
<div class="workbasket-information">
<h6 class="workbasket-information__subheading"> General </h6>
<mat-divider class="workbasket-information__horizontal-line"> </mat-divider>
<!-- KEY -->
<div class="form-group required">
<label for="wb-key" class="control-label">Key</label>
<input type="text" required maxlength="64" #key="ngModel" class="form-control" id="wb-key"
placeholder="Key"
[(ngModel)]="workbasket.key" name="workbasket.key" (input)="validateInputOverflow(key, 64, $event)">
<div *ngIf="inputOverflowMap.get(key.name)" class="error">{{lengthError}}</div>
<taskana-shared-field-error-display [displayError]="!isFieldValid('workbasket.key')"
[validationTrigger]="this.toggleValidationMap.get('workbasket.key')"
errorMessage="* Key is required">
</taskana-shared-field-error-display>
</div>
<mat-form-field appearance="outline">
<mat-label>Key</mat-label>
<label for="workbasket-key"></label>
<input matInput required type="text" #key="ngModel" maxlength="64"
[disabled]="action == 0 || action == 3"
id="workbasket-key" placeholder="Key" [(ngModel)]="workbasket.key"
name="workbasket.key" (input)="validateInputOverflow(key, 64, $event)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(key.name)" class="error">{{lengthError}}</div>
<taskana-shared-field-error-display [displayError]="!isFieldValid('workbasket.key')"
[validationTrigger]="this.toggleValidationMap.get('workbasket.key')"
errorMessage="* Key is required">
</taskana-shared-field-error-display>
<!-- NAME -->
<div class="form-group required">
<label for="wb-name" class="control-label">Name</label>
<input type="text" required maxlength="255" #name="ngModel" class="form-control" id="wb-name"
placeholder="Name"
[(ngModel)]="workbasket.name" name="workbasket.name" (input)="validateInputOverflow(name, 255)">
<div *ngIf="inputOverflowMap.get(name.name)" class="error">{{lengthError}}</div>
<taskana-shared-field-error-display [displayError]="!isFieldValid('workbasket.name')"
[validationTrigger]="this.toggleValidationMap.get('workbasket.name')"
errorMessage="* Name is required">
</taskana-shared-field-error-display>
<mat-form-field appearance="outline">
<mat-label>Name</mat-label>
<label for="workbasket-name"></label>
<input matInput type="text" required maxlength="255" #name="ngModel"
id="workbasket-name" placeholder="Name"
[(ngModel)]="workbasket.name" name="workbasket.name"
(input)="validateInputOverflow(name, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(name.name)" class="error">{{lengthError}}</div>
<taskana-shared-field-error-display [displayError]="!isFieldValid('workbasket.name')"
[validationTrigger]="this.toggleValidationMap.get('workbasket.name')"
errorMessage="* Name is required">
</taskana-shared-field-error-display>
<div class="workbasket-information__domain-and-type">
<!-- DOMAIN -->
<mat-form-field class="workbasket-information__mat-form-field" appearance="outline">
<mat-label>Domain</mat-label>
<label for="workbasket-domain"></label>
<input matInput type="text" disabled id="workbasket-domain"
placeholder="Domain" [(ngModel)]="workbasket.domain"
name="classification.domain">
</mat-form-field>
<!-- TYPE -->
<mat-form-field appearance="outline">
<mat-label>Type</mat-label>
<mat-select [(value)]="this.workbasket.type">
<mat-select-trigger>
<taskana-administration-icon-type [type]='workbasket.type'></taskana-administration-icon-type>
{{allTypes.get(workbasket.type)}}
</mat-select-trigger>
<mat-option *ngFor="let type of allTypes | mapValues | removeEmptyType" value="{{type.key}}">
<taskana-administration-icon-type
[type]='type.key' [text]="type.value">
</taskana-administration-icon-type>
</mat-option>
</mat-select>
</mat-form-field>
</div>
<!-- DESCRIPTION -->
<mat-form-field appearance="outline">
<mat-label>Description</mat-label>
<label for="workbasket-description"></label>
<textarea matInput
cdkTextareaAutosize
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="5"
maxlength="255"
id="workbasket-description" placeholder="Description"
[(ngModel)]="workbasket.description"
name="workbasket.description" #description="ngModel"
(input)="validateInputOverflow(description, 255)"></textarea>
</mat-form-field>
<div *ngIf="inputOverflowMap.get(description.name)" class="error">{{lengthError}}</div>
<!-- OWNER -->
<div class="input-group form-group col-xs-12 required">
<label for="wb-owner" class="control-label ">Owner</label>
@ -70,6 +132,7 @@
width="100%" (input)="validateInputOverflow(owner, 128)">
<div *ngIf="inputOverflowMap.get(owner.name)" class="error">{{lengthError}}</div>
</taskana-shared-type-ahead>
<ng-template #ownerInput>
<input type="text" required maxlength="128" #owner="ngModel" class="form-control" id="wb-owner"
placeholder="Owner"
@ -82,93 +145,68 @@
</ng-template>
</div>
<!-- DOMAIN -->
<div class="form-group ">
<label for="wb-domain" class="control-label">Domain</label>
<input type="text" #domain="ngModel" class="form-control" disabled id="wb-domain"
placeholder="Domain"
[(ngModel)]="workbasket.domain" name="workbasket.domain">
</div>
<!-- TYPE & DESCRIPTION-->
<div class="row">
<div class="form-group col-xs-4">
<label class="control-label">Type</label>
<div class="dropdown">
<button class="btn btn-default" type="button" id="dropdownMenu24"
data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<taskana-administration-icon-type
[type]='workbasket.type'></taskana-administration-icon-type>
{{allTypes.get(workbasket.type)}}
<span class="caret"></span>
</button>
<ul class="dropdown-menu dropdown-menu" aria-labelledby="dropdownMenu">
<li>
<a *ngFor="let type of allTypes | mapValues | removeEmptyType"
(click)="selectType(type.key)">
<taskana-administration-icon-type [type]='type.key'
[text]="type.value"></taskana-administration-icon-type>
</a>
</li>
</ul>
</div>
</div>
<div class="form-group col-xs-8">
<label for="wb-description" class="control-label">Description</label>
<textarea #description="ngModel" maxlength="255" class="form-control" rows="7" id="wb-description"
placeholder="Description"
[(ngModel)]="workbasket.description" name="workbasket.description"
(input)="validateInputOverflow(description, 255)"></textarea>
<div *ngIf="inputOverflowMap.get(description.name)" class="error">{{lengthError}}</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="wb-org-level-1" class="control-label">OrgLevel 1</label>
<input type="text" class="form-control" id="wb-org-level-1" placeholder="OrgLevel 1"
[(ngModel)]="workbasket.orgLevel1"
name="workbasket.orgLevel1" maxlength="255" #orgLevel1="ngModel"
(input)="validateInputOverflow(orgLevel1, 255)">
<div *ngIf="inputOverflowMap.get(orgLevel1.name)" class="error">{{lengthError}}</div>
</div>
<div class="form-group">
<label for="wb-org-level-2" class="control-label">OrgLevel 2</label>
<input type="text" class="form-control" id="wb-org-level-2" placeholder="OrgLevel 2"
[(ngModel)]="workbasket.orgLevel2"
name="workbasket.orgLevel2" maxlength="255" #orgLevel2="ngModel"
(input)="validateInputOverflow(orgLevel2, 255)">
<div *ngIf="inputOverflowMap.get(orgLevel2.name)" class="error">{{lengthError}}</div>
</div>
<div class="form-group" style="padding-top: 18px;">
<label for="wb-org-level-3" class="control-label">OrgLevel 3</label>
<input type="text" class="form-control" id="wb-org-level-3" placeholder="OrgLevel 3"
[(ngModel)]="workbasket.orgLevel3"
name="workbasket.orgLevel3" maxlength="255" #orgLevel3="ngModel"
(input)="validateInputOverflow(orgLevel3, 255)">
<div *ngIf="inputOverflowMap.get(orgLevel3.name)" class="error">{{lengthError}}</div>
</div>
<div class="form-group">
<label for="wb-org-level-4" class="control-label">OrgLevel 4</label>
<input type="text" class="form-control" id="wb-org-level-4" placeholder="OrgLevel 4"
[(ngModel)]="workbasket.orgLevel4"
name="workbasket.orgLevel4" maxlength="255" #orgLevel4="ngModel"
(input)="validateInputOverflow(orgLevel4, 255)">
<div *ngIf="inputOverflowMap.get(orgLevel4.name)" class="error">{{lengthError}}</div>
</div>
<ng-container *ngFor="let customField of customFields$ | async; let index = index">
<div *ngIf="customField.visible" class="custom-fields form-group">
<label for='wb-custom-{{index+1}}' class="control-label">{{customField.field}}</label>
<input type="text" class="form-control" id="wb-custom-{{index+1}}"
[placeholder]="customField.field"
[(ngModel)]="workbasket[getWorkbasketCustomProperty(index + 1)]"
name="workbasket[{{getWorkbasketCustomProperty(index + 1)}}]" maxlength="255" #custom="ngModel"
(input)="validateInputOverflow(custom, 255)">
<!-- ORGASATIONAL LEVELS -->
<h6 class="workbasket-information__subheading" style="margin-top: 65px;"> Organisational Levels </h6>
<mat-divider class="workbasket-information__horizontal-line"> </mat-divider>
<mat-form-field appearance="outline">
<mat-label>OrgLevel 1</mat-label>
<input matInput type="text" #orgLevel1="ngModel" maxlength="255"
placeholder="OrgLevel 1" [(ngModel)]="workbasket.orgLevel1"
name="workbasket.orgLevel1" (input)="validateInputOverflow(orgLevel1, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(orgLevel1.name)" class="error">{{lengthError}}</div>
<mat-form-field appearance="outline">
<mat-label>OrgLevel 2</mat-label>
<input matInput type="text" #orgLevel2="ngModel" maxlength="255"
placeholder="OrgLevel 2" [(ngModel)]="workbasket.orgLevel2"
name="workbasket.orgLevel2" (input)="validateInputOverflow(orgLevel2, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(orgLevel2.name)" class="error">{{lengthError}}</div>
<mat-form-field appearance="outline">
<mat-label>OrgLevel 3</mat-label>
<input matInput type="text" #orgLevel3="ngModel" maxlength="255"
placeholder="OrgLevel 3" [(ngModel)]="workbasket.orgLevel3"
name="workbasket.orgLevel3" (input)="validateInputOverflow(orgLevel3, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(orgLevel3.name)" class="error">{{lengthError}}</div>
<mat-form-field appearance="outline">
<mat-label>OrgLevel 4</mat-label>
<input matInput type="text" #orgLevel4="ngModel" maxlength="255"
placeholder="OrgLevel 4" [(ngModel)]="workbasket.orgLevel4"
name="workbasket.orgLevel4" (input)="validateInputOverflow(orgLevel4, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(orgLevel4.name)" class="error">{{lengthError}}</div>
<!-- CUSTOM FIELDS -->
<h6 class="workbasket-information__subheading" style="margin-top: 65px;"> Custom Fields </h6>
<mat-divider class="workbasket-information__horizontal-line"> </mat-divider>
<div *ngFor="let customField of customFields$ | async; let index = index">
<div *ngIf="customField.visible">
<mat-form-field appearance="outline" class="workbasket-information__custom-fields">
<mat-label>{{customField.field}}</mat-label>
<label for='wb-custom-{{index+1}}'></label>
<input matInput type="text"
[placeholder]="customField.field"
[(ngModel)]="workbasket[getWorkbasketCustomProperty(index + 1)]"
id="wb-custom-{{index+1}}"
name="workbasket[{{getWorkbasketCustomProperty(index + 1)}}]"
maxlength="255"
#custom="ngModel"
(input)="validateInputOverflow(custom, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(custom.name)" class="error">{{lengthError}}</div>
</div>
</ng-container>
</div>
</div>
</ng-form>
</div>
</div>

View File

@ -1,3 +1,35 @@
.workbasket-information-wrapper {
height: calc(100vh - 213px);
overflow-y: auto ;
}
.workbasket-information {
padding: 15px;
display: flex;
flex-direction: column;
}
.workbasket-information__subheading {
font-weight: bold;
padding-left: 15px;
margin-bottom: 0;
}
.workbasket-information__horizontal-line {
margin: 5px 5px 25px 5px;
border-top-color: #555;
border-top-width: 1.35px;
}
.workbasket-information__domain-and-type {
display: flex;
justify-content: space-between;
}
.workbasket-information__mat-form-field {
width: 70%;
margin-right: 10px;
}
.dropdown-menu {
min-width: auto;
}

View File

@ -34,6 +34,11 @@ import {
import { StartupService } from '../../../shared/services/startup/startup.service';
import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from '../../../shared/services/window/window.service';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatDividerModule } from '@angular/material/divider';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
@Component({ selector: 'taskana-shared-spinner', template: '' })
class SpinnerStub {
@ -59,7 +64,8 @@ const workbasketServiceMock = jest.fn().mockImplementation(
triggerWorkBasketSaved: triggerWorkbasketSavedFn,
updateWorkbasket: jest.fn().mockReturnValue(of(true)),
markWorkbasketForDeletion: jest.fn().mockReturnValue(of(true)),
createWorkbasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock }))
createWorkbasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock })),
getWorkBasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock }))
})
);
@ -103,7 +109,12 @@ describe('WorkbasketInformationComponent', () => {
TypeaheadModule.forRoot(),
ReactiveFormsModule,
RouterTestingModule.withRoutes([]),
BrowserAnimationsModule
BrowserAnimationsModule,
MatProgressBarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule
],
declarations: [
WorkbasketInformationComponent,
@ -149,7 +160,7 @@ describe('WorkbasketInformationComponent', () => {
});
it('should display custom fields correctly', () => {
const customFields = debugElement.nativeElement.getElementsByClassName('custom-fields');
const customFields = debugElement.nativeElement.getElementsByClassName('workbasket-information__custom-fields');
expect(customFields.length).toBe(3); //mock data has custom1->4 but engineConfig disables custom3 -> [1,2,4]
});
@ -171,12 +182,6 @@ describe('WorkbasketInformationComponent', () => {
expect(component.badgeMessage).toContain(`Copying workbasket: ${component.workbasket.key}`);
});
it('should set type variable in selectType', () => {
const type = ICONTYPES.GROUP;
component.selectType(type);
expect(component.workbasket.type).toMatch(type);
});
it('should submit when validatorService is true', () => {
const formsValidatorService = TestBed.inject(FormsValidatorService);
component.onSubmit();

View File

@ -2,7 +2,6 @@ import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChil
import { Observable, Subject } from 'rxjs';
import { NgForm } from '@angular/forms';
import { Select, Store } from '@ngxs/store';
import { ICONTYPES } from 'app/shared/models/icon-types';
import { ACTION } from 'app/shared/models/action';
import { customFieldCount, Workbasket } from 'app/shared/models/workbasket';
import { TaskanaDate } from 'app/shared/util/taskana.date';
@ -10,7 +9,7 @@ import { SavingInformation, SavingWorkbasketService } from 'app/administration/s
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { map, takeUntil } from 'rxjs/operators';
import { filter, map, take, takeUntil } from 'rxjs/operators';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
@ -20,8 +19,12 @@ import {
MarkWorkbasketForDeletion,
RemoveDistributionTarget,
SaveNewWorkbasket,
SelectComponent,
UpdateWorkbasket
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketComponent } from '../../models/workbasket-component';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { ButtonAction } from '../../models/button-action';
@Component({
selector: 'taskana-administration-workbasket-information',
@ -52,6 +55,12 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
@Select(EngineConfigurationSelectors.workbasketsCustomisation)
workbasketsCustomisation$: Observable<WorkbasketsCustomisation>;
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
@Select(WorkbasketSelectors.selectedComponent)
selectedComponent$: Observable<WorkbasketComponent>;
customFields$: Observable<CustomField[]>;
destroy$ = new Subject<void>();
@ -65,6 +74,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
) {}
ngOnInit() {
this.store.dispatch(new SelectComponent(WorkbasketComponent.INFORMATION));
this.allTypes = new Map([
['PERSONAL', 'Personal'],
['GROUP', 'Group'],
@ -87,6 +97,35 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
this.validateInputOverflow = (inputFieldModel, maxLength) => {
this.formsValidatorService.validateInputOverflow(inputFieldModel, maxLength);
};
this.buttonAction$
.pipe(takeUntil(this.destroy$))
.pipe(filter((buttonAction) => typeof buttonAction !== 'undefined'))
.subscribe((button) => {
this.selectedComponent$
.pipe(take(1))
.pipe(filter((component) => component === WorkbasketComponent.INFORMATION))
.subscribe((component) => {
switch (button) {
case ButtonAction.SAVE:
this.onSave();
break;
case ButtonAction.UNDO:
this.onUndo();
break;
case ButtonAction.COPY:
this.copyWorkbasket();
break;
case ButtonAction.REMOVE_AS_DISTRIBUTION_TARGETS:
this.removeDistributionTargets();
break;
case ButtonAction.DELETE:
this.removeWorkbasket();
break;
default:
break;
}
});
});
}
ngOnChanges(changes?: SimpleChanges) {
@ -98,10 +137,6 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
}
}
selectType(type: ICONTYPES) {
this.workbasket.type = type;
}
onSubmit() {
this.formsValidatorService.formSubmitAttempt = true;
this.formsValidatorService.validateFormInformation(this.workbasketForm, this.toggleValidationMap).then((value) => {

View File

@ -1,20 +1,32 @@
<li id="wb-action-toolbar" class="list-group-item tab-align">
<div class="row">
<div class="col-xs-7 btn-group">
<button type="button" (click)="addWorkbasket()" data-toggle="tooltip" title="Add" class="btn btn-default">
<span class="material-icons md-20 green-blue">add_circle_outline</span>
</button>
<taskana-administration-import-export class="btn-group" [currentSelection]="selectionToImport"></taskana-administration-import-export>
</div>
<div class="margin-right pull-right btn-group">
<taskana-shared-sort [sortingFields]="sortingFields" (performSorting)="sorting($event)" class="btn-group" [defaultSortBy]="workbasketDefaultSortBy"></taskana-shared-sort>
<button class="btn btn-default collapsed" type="button" id="collapsedMenufilterWb" aria-expanded="false" (click)="toolbarState=!toolbarState"
data-toggle="tooltip" title="Filter">
<span class="material-icons md-20 blue">{{!toolbarState? 'search' : 'expand_less'}}</span>
</button>
</div>
<div class="workbasket-list-toolbar">
<div class="workbasket-list-toolbar__action-toolbar">
<!-- ACTION BUTTONS -->
<button mat-flat-button class="workbasket-list-toolbar__add-button mr-1" matTooltip="Create new workbasket"
(click)="addWorkbasket()">
Add
<mat-icon class="md-20">add</mat-icon>
</button>
<!-- IMPORT EXPORT -->
<taskana-administration-import-export [currentSelection]="selectionToImport" [parentComponent]="'workbaskets'"></taskana-administration-import-export>
<span class="workbasket-details__spacer" style="flex: 1 1 auto"> </span>
<!-- SORTING -->
<taskana-shared-sort
style="margin-right: 4px;" [sortingFields]="sortingFields" (performSorting)="sorting($event)" [defaultSortBy]="workbasketDefaultSortBy">
</taskana-shared-sort>
<!-- FILTER -->
<button mat-stroked-button class="workbasket-list-toolbar__filter-button" matTooltip="Display filter options" (click)="onClickFilter()">
<mat-icon *ngIf="!showFilter">filter_list</mat-icon>
<mat-icon *ngIf="showFilter">keyboard_arrow_up</mat-icon>
</button>
</div>
<div [@toggleDown]="toolbarState" class="row no-overflow">
<taskana-shared-filter (performFilter)="filtering($event)"></taskana-shared-filter>
</div>
</li>
<taskana-shared-filter *ngIf="showFilter" (performFilter)="filtering($event)"></taskana-shared-filter>
</div>

View File

@ -1,13 +1,34 @@
.list-group-item {
padding: 5px 0px 2px 1px;
@import 'src/theme/_colors.scss';
.workbasket-list-toolbar {
padding: 16px 16px 8px 16px;
flex-wrap: wrap;
min-height: 68px;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.workbasket-list-toolbar__action-toolbar {
padding: 0 4px;
display: flex;
border: none;
margin-bottom: 0;
}
.tab-align {
padding: 8px 12px 8px 4px;
margin-bottom: 0px;
& > div {
margin: 6px 0px;
}
.workbasket-list-toolbar__filter-and-sorting {
display: flex;
border: none;
margin-bottom: 0;
padding: 16px 4px 8px 4px;
}
.workbasket-list-toolbar__add-button {
background-color: $aquamarine;
color: white;
height: 36px;
}
.workbasket-list-toolbar__filter-button {
padding: 0 5px;
color: #555;
height: 36px;
}

View File

@ -7,14 +7,15 @@ import { HttpClientTestingModule } from '@angular/common/http/testing';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
import { DomainService } from '../../../shared/services/domain/domain.service';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { CreateWorkbasket } from '../../../shared/store/workbasket-store/workbasket.actions';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Filter } from '../../../shared/models/filter';
import { Sorting } from '../../../shared/models/sorting';
import { ACTION } from '../../../shared/models/action';
import { TaskanaType } from '../../../shared/models/taskana-type';
import { MatIconModule } from '@angular/material/icon';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
const getDomainFn = jest.fn().mockReturnValue(true);
const domainServiceMock = jest.fn().mockImplementation(
@ -26,6 +27,7 @@ const domainServiceMock = jest.fn().mockImplementation(
@Component({ selector: 'taskana-administration-import-export', template: '' })
class ImportExportStub {
@Input() currentSelection: TaskanaType;
@Input() parentComponent;
}
@Component({ selector: 'taskana-shared-sort', template: '' })
@ -52,9 +54,10 @@ describe('WorkbasketListToolbarComponent', () => {
imports: [
HttpClientTestingModule,
NgxsModule.forRoot([WorkbasketState]),
BrowserAnimationsModule,
MatIconModule,
MatSnackBarModule,
MatDialogModule,
BrowserAnimationsModule
MatDialogModule
],
declarations: [WorkbasketListToolbarComponent, ImportExportStub, SortStub, FilterStub],
providers: [{ provide: DomainService, useClass: domainServiceMock }, WorkbasketService]
@ -73,6 +76,8 @@ describe('WorkbasketListToolbarComponent', () => {
expect(component).toBeTruthy();
});
/* Typescript */
it('should dispatch CreateWorkbasket when addWorkbasket is called', async((done) => {
component.action = ACTION.COPY;
let actionDispatched = false;
@ -110,4 +115,36 @@ describe('WorkbasketListToolbarComponent', () => {
component.filtering(filterBy);
expect(filterBy).toMatchObject(mockFilter);
});
/* HTML */
it('should call AddWorkbasket() when add-workbasket button is clicked', async () => {
const button = debugElement.nativeElement.querySelector('.workbasket-list-toolbar__add-button');
expect(button).toBeTruthy();
expect(button.textContent).toContain('add');
expect(button.textContent).toContain('Add');
component.addWorkbasket = jest.fn().mockImplementation();
button.click();
expect(component.addWorkbasket).toHaveBeenCalled();
});
it('should display import-export component', () => {
expect(debugElement.nativeElement.querySelector('taskana-administration-import-export')).toBeTruthy();
});
it('should display sort component', () => {
expect(debugElement.nativeElement.querySelector('taskana-shared-sort')).toBeTruthy();
});
it('should show filter component only when filter button is clicked', () => {
const button = debugElement.nativeElement.querySelector('.workbasket-list-toolbar__filter-button');
expect(button).toBeTruthy();
expect(button.textContent).toBe('filter_list');
expect(debugElement.nativeElement.querySelector('filter')).toBeFalsy();
button.click();
fixture.detectChanges();
expect(component.showFilter).toBe(true);
expect(button.textContent).toBe('keyboard_arrow_up');
expect(debugElement.nativeElement.querySelector('taskana-shared-filter')).toBeTruthy();
});
});

View File

@ -10,6 +10,7 @@ import { takeUntil } from 'rxjs/operators';
import { ACTION } from '../../../shared/models/action';
import { CreateWorkbasket } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
@Component({
selector: 'taskana-administration-workbasket-list-toolbar',
@ -40,8 +41,8 @@ export class WorkbasketListToolbarComponent implements OnInit {
]);
filterParams = { name: '', key: '', type: '', description: '', owner: '' };
toolbarState = false;
filterType = TaskanaType.WORKBASKETS;
showFilter = false;
@Select(WorkbasketSelectors.workbasketActiveAction)
workbasketActiveAction$: Observable<ACTION>;
@ -49,7 +50,7 @@ export class WorkbasketListToolbarComponent implements OnInit {
destroy$ = new Subject<void>();
action: ACTION;
constructor(private store: Store) {}
constructor(private store: Store, private workbasketService: WorkbasketService) {}
ngOnInit() {
this.workbasketActiveAction$.pipe(takeUntil(this.destroy$)).subscribe((action) => {
@ -71,6 +72,11 @@ export class WorkbasketListToolbarComponent implements OnInit {
}
}
onClickFilter() {
this.showFilter = !this.showFilter;
this.workbasketService.expandWorkbasketActionToolbar(this.showFilter);
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();

View File

@ -1,43 +1,56 @@
<div class="workbasket-list footer-space-pagination-list">
<div #wbToolbar>
<div class="workbasket-list">
<!-- TOOLBAR -->
<section #wbToolbar class="workbasket-list__toolbar">
<taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)"
(performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy">
</taskana-administration-workbasket-list-toolbar>
</div>
<div *ngIf="((workbasketsSummary$ | async) && (workbasketsSummary$ | async)?.length > 0) else empty_workbaskets">
<ul #wbList id="wb-list-container" class="list-group">
<li class="list-group-item no-space">
<div class="row"></div>
</li>
<li class="list-group-item" *ngFor="let workbasket of (workbasketsSummary$ | async)"
[class.active]="workbasket.workbasketId == selectedId"
type="text" (click)="selectWorkbasket(workbasket.workbasketId)">
<div class="row">
<dl class="col-xs-1">
<taskana-administration-icon-type class="vertical-align" [type]="workbasket.type" tooltip="true" [selected]="workbasket.workbasketId === selectedId"></taskana-administration-icon-type>
</dl>
<dl class="col-xs-10">
<dt data-toggle="tooltip" title="{{workbasket.name}}">{{workbasket.name}},
<i data-toggle="tooltip" title="{{workbasket.key}}">{{workbasket.key}} </i>
</dt>
<dd data-toggle="tooltip" title="{{workbasket.description}}">{{workbasket.description}} &nbsp;</dd>
<dd data-toggle="tooltip" title="{{workbasket.owner}}">{{workbasket.owner}} &nbsp;</dd>
</dl>
<dl *ngIf="workbasket.markedForDeletion">
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
</section>
<!-- WORKBASKET LIST -->
<div class="workbasket-list__workbaskets" *ngIf="((workbasketsSummary$ | async) && (workbasketsSummary$ | async)?.length > 0) else empty_workbaskets">
<mat-selection-list #workbasket [multiple]="false">
<mat-list-option class="workbasket-list__workbaskets-item"
*ngFor="let workbasket of (workbasketsSummary$ | async)"
(click)="selectWorkbasket(workbasket.workbasketId)"
[selected]="workbasket.workbasketId == selectedId"
[value]="workbasket.workbasketId">
<div class="workbaskets-item__wrapper">
<div class="workbaskets-item__icon">
<taskana-administration-icon-type [type]="workbasket.type" size="large" tooltip="true" [selected]="workbasket.workbasketId === selectedId"></taskana-administration-icon-type>
</div>
<div class="workbaskets-item__info">
<p>
<b>{{workbasket.name}}</b>, <i>{{workbasket.key}} </i>
</p>
<p>{{workbasket.description}} &nbsp;</p>
<p>{{workbasket.owner}} &nbsp;</p>
</div>
<div class="workbaskets-item__marked" *ngIf="workbasket.markedForDeletion">
<span title="Marked for deletion" data-toggle="tooltip" class="material-icons md-20 {{workbasket.workbasketId === selectedId ? 'white': 'red' }} ">error</span>
</dl>
</div>
</div>
</li>
</ul>
<mat-divider></mat-divider>
</mat-list-option>
</mat-selection-list>
</div>
<taskana-shared-spinner [isRunning]="requestInProgress"></taskana-shared-spinner>
<!-- SPINNER and EMPTY WORKBASKET LIST -->
<ng-template #empty_workbaskets>
<div *ngIf="!requestInProgress" class="col-xs-12 container-no-items center-block">
<div *ngIf="!requestInProgress" class="workbasket-list__no-items">
<h3 class="grey">There are no workbaskets</h3>
<svg-icon class="img-responsive empty-icon" src="./assets/icons/wb-empty.svg"></svg-icon>
</div>
</ng-template>
</div>
<!-- PAGINATION -->
<taskana-shared-pagination
[page]="(workbasketsSummaryRepresentation$ | async) ? (workbasketsSummaryRepresentation$ | async)?.page : (workbasketsSummaryRepresentation$ | async)"
[type]="type"

View File

@ -1,41 +1,39 @@
@import 'src/theme/_colors.scss';
.workbasket-list {
min-width: 30vw;
height: calc(100vh - 156px);
overflow-x: hidden;
overflow-y: hidden;
min-width: 500px;
}
.row.list-group {
margin-left: 2px;
.workbasket-list__workbaskets {
overflow-y: hidden;
}
.list-group > li {
border-left: none;
border-right: none;
.workbasket-list__workbaskets-item {
}
a > label {
height: 2em;
width: 100%;
.mat-list-item {
height: 90px !important;
}
dd,
dt {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.mat-list-single-selected-option {
background-color: $green !important;
color: white;
}
dt > i {
font-weight: normal;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
.workbaskets-item__wrapper {
display: flex;
}
li > div.row > dl {
margin-bottom: 0px;
.workbaskets-item__icon {
padding: 32px 32px 32px 16px;
}
li > div.row > dl:first-child {
margin-left: 15px;
.workbaskets-item__info {
display: block;
padding: 8px 0;
}
.no-space {
border-top: none;
padding: 0px;
p {
margin: 0;
}
.workbasket-list__no-items {
text-align: center;
padding-top: 150px;
}

View File

@ -16,15 +16,23 @@ import { Sorting } from '../../../shared/models/sorting';
import { Filter } from '../../../shared/models/filter';
import { ICONTYPES } from '../../../shared/models/icon-types';
import { Page } from '../../../shared/models/page';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { FormsModule } from '@angular/forms';
import { MatListModule, MatSelectionList } from '@angular/material/list';
import { DomainService } from '../../../shared/services/domain/domain.service';
const workbasketSavedTriggeredFn = jest.fn().mockReturnValue(of(1));
const workbasketSummaryFn = jest.fn().mockReturnValue(of({}));
const getWorkbasketFn = jest.fn().mockReturnValue(of({ workbasketId: '1' }));
const getWorkbasketActionToolbarExpansionFn = jest.fn().mockReturnValue(of(false));
const workbasketServiceMock = jest.fn().mockImplementation(
(): Partial<WorkbasketService> => ({
workbasketSavedTriggered: workbasketSavedTriggeredFn,
getWorkBasketsSummary: workbasketSummaryFn,
getWorkBasket: getWorkbasketFn
getWorkBasket: getWorkbasketFn,
getWorkbasketActionToolbarExpansion: getWorkbasketActionToolbarExpansionFn
})
);
@ -43,6 +51,13 @@ const importExportServiceMock = jest.fn().mockImplementation(
})
);
const domainServiceSpy = jest.fn().mockImplementation(
(): Partial<DomainService> => ({
getSelectedDomainValue: jest.fn().mockReturnValue(of()),
getSelectedDomain: jest.fn().mockReturnValue(of())
})
);
@Component({ selector: 'taskana-administration-workbasket-list-toolbar', template: '' })
class WorkbasketListToolbarStub {
@Input() workbaskets: Array<WorkbasketSummary>;
@ -82,7 +97,15 @@ describe('WorkbasketListComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [NgxsModule.forRoot([WorkbasketState]), MatSnackBarModule, MatDialogModule],
imports: [
NgxsModule.forRoot([WorkbasketState]),
MatSnackBarModule,
MatDialogModule,
FormsModule,
MatProgressBarModule,
MatSelectModule,
MatListModule
],
declarations: [
WorkbasketListComponent,
WorkbasketListToolbarStub,
@ -94,7 +117,8 @@ describe('WorkbasketListComponent', () => {
providers: [
{ provide: WorkbasketService, useClass: workbasketServiceMock },
{ provide: OrientationService, useClass: orientationServiceMock },
{ provide: ImportExportService, useClass: importExportServiceMock }
{ provide: ImportExportService, useClass: importExportServiceMock },
{ provide: DomainService, useClass: domainServiceSpy }
]
}).compileComponents();

View File

@ -20,6 +20,8 @@ import {
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { Workbasket } from '../../../shared/models/workbasket';
import { MatSelectionList } from '@angular/material/list';
import { DomainService } from '../../../shared/services/domain/domain.service';
@Component({
selector: 'taskana-administration-workbasket-list',
@ -51,11 +53,14 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
destroy$ = new Subject<void>();
@ViewChild('workbasket') workbasketList: MatSelectionList;
constructor(
private store: Store,
private workbasketService: WorkbasketService,
private orientationService: OrientationService,
private importExportService: ImportExportService,
private domainService: DomainService,
private ngxsActions$: Actions
) {
this.ngxsActions$.pipe(ofActionDispatched(GetWorkbasketsSummary), takeUntil(this.destroy$)).subscribe(() => {
@ -100,6 +105,23 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
.subscribe((value: Boolean) => {
this.refreshWorkbasketList();
});
this.domainService
.getSelectedDomain()
.pipe(takeUntil(this.destroy$))
.subscribe((domain) => {
this.performRequest();
});
this.workbasketService
.getWorkbasketActionToolbarExpansion()
.pipe(takeUntil(this.destroy$))
.subscribe((value) => {
this.requestInProgress = true;
setTimeout(() => {
this.refreshWorkbasketList();
}, 1);
});
}
selectWorkbasket(id: string) {
@ -128,30 +150,34 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
refreshWorkbasketList() {
this.cards = this.orientationService.calculateNumberItemsList(
window.innerHeight,
72,
170 + this.toolbarElement.nativeElement.offsetHeight,
92,
200 + this.toolbarElement.nativeElement.offsetHeight,
false
);
this.performRequest();
}
performRequest(): void {
this.store.dispatch(
new GetWorkbasketsSummary(
true,
this.sort.sortBy,
this.sort.sortDirection,
'',
this.filterBy.filterParams.name,
this.filterBy.filterParams.description,
'',
this.filterBy.filterParams.owner,
this.filterBy.filterParams.type,
'',
this.filterBy.filterParams.key,
''
this.store
.dispatch(
new GetWorkbasketsSummary(
true,
this.sort.sortBy,
this.sort.sortDirection,
'',
this.filterBy.filterParams.name,
this.filterBy.filterParams.description,
'',
this.filterBy.filterParams.owner,
this.filterBy.filterParams.type,
'',
this.filterBy.filterParams.key,
''
)
)
);
.subscribe(() => {
this.requestInProgress = false;
});
TaskanaQueryParameters.pageSize = this.cards;
}

View File

@ -1,17 +1,15 @@
<div class="workbasket-overview">
<div class="vertical-right-divider">
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
</div>
<div *ngIf="showDetail; else showEmptyPage">
<taskana-administration-workbasket-details></taskana-administration-workbasket-details>
</div>
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
<div class="vertical-right-divider"></div>
<taskana-administration-workbasket-details
*ngIf="showDetail; else showEmptyPage"></taskana-administration-workbasket-details>
<ng-template #showEmptyPage>
<div class="hidden-xs hidden-sm col-md-8 container-no-items">
<div class="center-block no-detail">
<h3 class="grey">Select a workbasket</h3>
<svg-icon class="img-responsive empty-icon" src="./assets/icons/wb-empty.svg"></svg-icon>
</div>
</div>
</ng-template>
<ng-template #showEmptyPage>
<div class="hidden-xs hidden-sm col-md-8 container-no-items">
<div class="center-block no-detail">
<h3 class="grey">Select a workbasket</h3>
<svg-icon class="img-responsive empty-icon" src="./assets/icons/wb-empty.svg"></svg-icon>
</div>
</div>
</ng-template>
</div>

View File

@ -1,4 +1,13 @@
.workbasket-overview {
width: 100%;
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
overflow: hidden;
align-items: stretch;
}
taskana-administration-workbasket-details {
width: calc(100% - 500px);
height: calc(100% - 213px);
}

View File

@ -13,7 +13,6 @@ import { SelectedRouteService } from '../../../shared/services/selected-route/se
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { ActivatedRoute } from '@angular/router';
import { CreateWorkbasket, SelectWorkbasket } from '../../../shared/store/workbasket-store/workbasket.actions';
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
import { StartupService } from '../../../shared/services/startup/startup.service';
import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from '../../../shared/services/window/window.service';

View File

@ -0,0 +1,8 @@
export enum ButtonAction {
SAVE,
UNDO,
COPY,
REMOVE_AS_DISTRIBUTION_TARGETS,
DELETE,
CLOSE
}

View File

@ -0,0 +1,5 @@
export enum WorkbasketComponent {
INFORMATION,
ACCESS_ITEMS,
DISTRIBUTION_TARGETS
}

View File

@ -6,9 +6,7 @@
<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-shared-user-information class="sidenav__drawer-user-info"></taskana-shared-user-information>
<taskana-sidenav-list></taskana-sidenav-list>
<div class="sidenav__drawer-version">
<p> Taskana version: {{version}} </p>
@ -16,14 +14,13 @@
</mat-sidenav>
<mat-sidenav-content>
<taskana-shared-nav-bar></taskana-shared-nav-bar>
<div (window:resize)="onResize()" class="">
<mat-progress-bar mode="query" *ngIf="requestInProgress"></mat-progress-bar>
<div (window:resize)="onResize()">
<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>
</div>
<taskana-shared-type-ahead></taskana-shared-type-ahead>
</div>
</mat-sidenav-content>
</mat-sidenav-container>
</mat-sidenav-container>

View File

@ -15,7 +15,6 @@
box-shadow: none;
width: 350px;
background-color: $dark-green;
box-shadow: 3px 0px 10px -1px $dark-green;
}
.sidenav__drawer-list-item {
@ -24,14 +23,15 @@
.sidenav__drawer-logout {
text-align: end;
color: white;
}
.sidenav__drawer-version {
color: $grey;
color: white;
position: absolute;
bottom: 5px;
left: 16px;
font-size: 12px;
margin-left: 16px;
}
.sidenav__drawer-user-info {
@ -40,14 +40,9 @@
margin-left: -16px;
}
.mat-icon-button {
outline: none;
mat-sidenav-content {
height: unset;
}
.mat-icon {
color: white;
}
::ng-deep .mat-drawer-inner-container {
overflow: visible !important;
}

View File

@ -1,7 +1,6 @@
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';
@ -12,6 +11,7 @@ import { ErrorModel } from './shared/models/error-model';
import { TaskanaEngineService } from './shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from 'app/shared/services/window/window.service';
import { environment } from 'environments/environment';
import { MatSidenav } from '@angular/material/sidenav';
@Component({
selector: 'taskana-root',

View File

@ -13,14 +13,6 @@ 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
@ -59,6 +51,14 @@ import { UserGuard } from './shared/guards/user.guard';
import { ClassificationCategoriesService } from './shared/services/classification-categories/classification-categories.service';
import { environment } from '../environments/environment';
import { STATES } from './shared/store';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatIconModule } from '@angular/material/icon';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatListModule } from '@angular/material/list';
import { MatButtonModule } from '@angular/material/button';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatProgressBarModule } from '@angular/material/progress-bar';
const MODULES = [
TabsModule.forRoot(),
@ -90,7 +90,7 @@ export function startupServiceFactory(startupService: StartupService): () => Pro
@NgModule({
declarations: DECLARATIONS,
imports: MODULES,
imports: [MODULES, MatSidenavModule, MatIconModule, MatToolbarModule, MatProgressBarModule],
providers: [
WindowRefService,
DomainService,

View File

@ -1,49 +1,68 @@
<div class="list-group-search">
<div *ngIf="filterTypeIsWorkbasket(); else tasktype">
<div class="row">
<div class="dropdown col-xs-2">
<button class="btn btn-default btn-sm" data-toggle="dropdown" type="button" id="dropdownMenufilter" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true">
<taskana-administration-icon-type [type]="filter.filterParams?.type"></taskana-administration-icon-type>
<div>
<!-- WORKBASKET FILTER -->
<div class="filter" *ngIf="filterTypeIsWorkbasket(); else tasktype">
<!-- TEXT INPUT -->
<div class="filter__text-input">
<div class="filter__name-and-key-input">
<mat-form-field appearance="legacy" floatLabel="auto" class="filter__input-field">
<mat-label>Filter by name</mat-label>
<input matInput [(ngModel)]="filter.filterParams.name" matTooltip="Type to filter by name" (keyup.enter)="search()">
</mat-form-field>
<mat-form-field appearance="legacy" floatLabel="auto" class="filter__input-field">
<mat-label>Filter by key</mat-label>
<input matInput [(ngModel)]="filter.filterParams.key" matTooltip="Type to filter by key" (keyup.enter)="search()">
</mat-form-field>
</div>
<mat-form-field appearance="legacy" floatLabel="auto" class="filter__input-field">
<mat-label>Filter by description</mat-label>
<input matInput [(ngModel)]="filter.filterParams.description" matTooltip="Type to filter by description" (keyup.enter)="search()">
</mat-form-field>
<mat-form-field appearance="legacy" floatLabel="auto" class="filter__input-field">
<mat-label>Filter by owner</mat-label>
<input matInput [(ngModel)]="filter.filterParams.owner" matTooltip="Type to filter by owner" (keyup.enter)="search()">
</mat-form-field>
</div>
<!-- SEARCH AND CLEAR BUTTON -->
<div class="filter__action-buttons">
<!-- TYPE FILTER -->
<button mat-stroked-button [matMenuTriggerFor]="menu" matTooltip="Filter workbaskets by type">
Filter by type
<mat-icon *ngIf="filter.filterParams?.type == ''" style="color: #555">filter_list</mat-icon>
<taskana-administration-icon-type *ngIf="filter.filterParams?.type != ''" [type]="filter.filterParams?.type"> </taskana-administration-icon-type>
</button>
<mat-menu #menu="matMenu">
<button mat-menu-item *ngFor="let type of allTypes | mapValues" (click)="selectType(type.key); search()">
<taskana-administration-icon-type [type]='type.key' [text]="type.value"></taskana-administration-icon-type>
</button>
<ul class="dropdown-menu dropdown-menu-users" role="menu">
<li>
<a *ngFor="let type of allTypes | mapValues" type="button" (click)="selectType(type.key); search()"
data-toggle="tooltip" [title]="type.value">
<taskana-administration-icon-type [type]='type.key' [text]="type.value"></taskana-administration-icon-type>
</a>
</li>
</ul>
</div>
<div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.name" (keyup.enter)="search()" class="form-control input-sm"
placeholder="Filter name">
</div>
<div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.key" (keyup.enter)="search()" class="form-control input-sm" placeholder="Filter key">
</div>
<button (click)="clear(); search()" type="button" class="btn btn-default btn-sm pull-right margin-right"
data-toggle="tooltip" title="Clear">
<span class="material-icons md-20 blue">clear</span>
</button>
</div>
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.description" (keyup.enter)="search()" class="form-control input-sm"
placeholder="Filter description">
</div>
</div>
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.owner" (keyup.enter)="search()" class="form-control input-sm"
placeholder="Filter owner">
</div>
<button (click)="search()" type="button" class="btn btn-default btn-sm pull-right margin-right" data-toggle="tooltip"
title="Search">
<span class="material-icons md-20 blue ">search</span>
</mat-menu>
<!-- CLEAR BUTTON -->
<button mat-stroked-button (click)="clear(); search()" matTooltip="Clear workbasket filter">
Reset filter
<mat-icon style="color: #555">undo</mat-icon>
</button>
<!-- SEARCH BUTTON -->
<button mat-stroked-button (click)="search()" matTooltip="Search by given filter" class="filter__search-button">
Apply filter
<mat-icon>search</mat-icon>
</button>
</div>
</div>
<!-- TASK FILTER -->
<ng-template #tasktype>
<div class="row">
<div class="col-xs-2">

View File

@ -1,3 +1,37 @@
@import 'src/theme/_colors.scss';
.filter {
display: flex;
flex-direction: column;
justify-content: space-between;
margin: 10px;
}
.filter__text-input {
display: flex;
flex-direction: column;
}
.filter__action-buttons {
width: 100%;
padding: 10px 0;
display: flex;
justify-content: space-between;
}
.filter__search-button {
background: $aquamarine;
color: white;
margin-left: 4px;
}
.filter__name-and-key-input{
display: flex;
justify-content: space-between;
}
.dropdown-menu-users {
& > li {
margin-bottom: 5px;

View File

@ -1,4 +1,4 @@
import { Component, EventEmitter, Input, Output, OnInit } from '@angular/core';
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { ICONTYPES } from 'app/shared/models/icon-types';
import { Filter } from 'app/shared/models/filter';
import { TaskanaType } from 'app/shared/models/taskana-type';
@ -9,12 +9,12 @@ import { TaskanaType } from 'app/shared/models/taskana-type';
styleUrls: ['./filter.component.scss']
})
export class FilterComponent implements OnInit {
@Input() allTypes: Map<string, string> = new Map([
['ALL', 'All'],
['PERSONAL', 'Personal'],
['GROUP', 'Group'],
['CLEARANCE', 'Clearance'],
['TOPIC', 'Topic']
@Input() allTypes: Map<ICONTYPES, string> = new Map([
[ICONTYPES.ALL, 'All'],
[ICONTYPES.PERSONAL, 'Personal'],
[ICONTYPES.GROUP, 'Group'],
[ICONTYPES.CLEARANCE, 'Clearance'],
[ICONTYPES.TOPIC, 'Topic']
]);
@Input() allStates: Map<string, string> = new Map([

View File

@ -1,4 +1,4 @@
<nav class="navbar navbar__inverse">
<mat-toolbar class="navbar">
<div class="navbar__button">
<button mat-icon-button class="navbar_button-toggle" (click)="toggleSidenav()">
<mat-icon>menu</mat-icon>
@ -6,6 +6,6 @@
</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 class="navbar__title">/ {{title}}</div>
</div>
</nav>
</mat-toolbar>

View File

@ -1,58 +1,33 @@
@import '../../../../theme/variables';
.navbar.main:before {
@include degraded-bar(right, 100%, 3px);
box-sizing: border-box;
}
.navbar {
height: 52px;
margin-bottom: 0px;
}
.navbar__inverse {
border: none;
background-color: $dark-green;
box-shadow: 0px 1px 5px -1px black;
width: 100%;
}
.navbar__buttom {
flex-grow: 1;
display: flex !important;
order: 1;
margin-top: -10px;
max-height: 52px;
overflow-x: hidden;
padding-right: 50px;
}
.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;
}
margin: auto;
}
svg-icon.navbar__logo-icon {
float: left;
width: 150px;
height: 50px;
padding: 4px 10px 0 0;
position: initial;
}
.navbar__title {
padding-top: 9px;
color: white;
font-size: 1.25rem;
@media only screen and (max-width: 700px) {
display: none;
}
}
.mat-icon-button {
outline: none;
}

View File

@ -1,13 +1,14 @@
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 { SelectedRouteService } from '../../services/selected-route/selected-route';
import { SidenavService } from '../../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';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
const SidenavServiceSpy = jest.fn().mockImplementation(
(): Partial<SidenavService> => ({
@ -30,7 +31,7 @@ describe('NavBarComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [NavBarComponent],
imports: [MatIconModule, HttpClientTestingModule, AngularSvgIconModule],
imports: [MatIconModule, HttpClientTestingModule, AngularSvgIconModule, MatToolbarModule],
providers: [
{ provide: SidenavService, useClass: SidenavServiceSpy },
{ provide: SelectedRouteService, useClass: SelectedRouteServiceSpy }

View File

@ -1,21 +1,11 @@
<ul id="wb-pagination" class="pagination vertical-center">
<li>
<a *ngIf="hasItems" (click)="changeToPage(1)" aria-label="First">
First</a>
</li>
<li *ngFor="let pageNumber of page?.totalPages | spreadNumber: page?.number: maxPagesAvailable">
<a *ngIf="pageNumber + 1 !== page?.number" (click)="changeToPage(pageNumber+1)">{{pageNumber + 1}}</a>
<a *ngIf="pageNumber + 1 === page?.number" class="pagination">
<input [(ngModel)]="pageSelected" (keyup.enter)="changeToPage(pageSelected)" type="number" (blur)="changeToPage(pageSelected)" >
</a>
</li>
<li>
<a *ngIf="hasItems" (click)="changeToPage(page?.totalPages)" aria-label="Last">Last</a>
</li>
</ul>
<span class="footer pull-right" [hidden]="numberOfItems === 0">
<i [innerHTML]="getPagesTextToShow()"></i>
</span>
<span class="footer pull-right" [hidden]="numberOfItems !== 0 && page?.totalElements !== 0">
<i>Loading...</i>
</span>
<div class="pagination">
<mat-paginator [length]="page?.totalElements" [pageIndex]="pageSelected - 1 " hidePageSize="true" [pageSize]="page?.size" (page)="changeToPage($event)" showFirstLastButtons="true"></mat-paginator>
<div class="pagination__go-to">
<div class="pagination__go-to-label">Page: </div>
<mat-form-field>
<mat-select [(ngModel)]="pageSelected" (selectionChange)="goToPage(pageSelected)">
<mat-option *ngFor="let pageNumber of pageNumbers" [value]="pageNumber">{{ pageNumber }}</mat-option>
</mat-select>
</mat-form-field>
</div>
</div>

View File

@ -1,47 +1,27 @@
$blue: #66afe9;
.pagination {
margin: 15px 0 0 0;
display: flex;
max-width: 100%;
background-color: #fafafa;
}
a.pagination {
width: 35px;
height: 35px;
margin: 0 10px;
padding: 0;
background-color: aliceblue;
> input {
margin: 1px;
width: 30px;
height: 30px;
text-align: center;
background-color: aliceblue;
border: none;
outline: none;
&:focus {
border-color: $blue;
outline: 0;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6);
}
mat-paginator {
background-color: #fafafa;
width: 70%;
font-feature-settings: "tnum";
font-variant-numeric: tabular-nums;
}
//Original
.pagination__go-to {
margin-right: 12px;
display: flex;
align-items: baseline;
mat-form-field {
width: 56px;
margin: 6px 4px 0 4px;
font-size: 12px;
}
.pagination__go-to-label {
margin: 0 4px;
font-size: 12px;
color: rgba(0,0,0,.54);
}
}
ul.pagination {
cursor: pointer;
overflow: hidden;
}
.footer {
margin: 0px 5px 0 0;
color: $blue;
}
// small "hack" to remove the arrows in the input fields of type numbers
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
input[type='number'] {
-moz-appearance: textfield;
}

View File

@ -1,60 +1,78 @@
import { Component, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core';
import { Page } from 'app/shared/models/page';
import { MatPaginator } from '@angular/material/paginator';
@Component({
selector: 'taskana-shared-pagination',
templateUrl: './pagination.component.html',
styleUrls: ['./pagination.component.scss']
})
export class PaginationComponent implements OnChanges {
export class PaginationComponent implements OnInit, OnChanges {
@Input()
page: Page;
@Input()
type: String;
@Output()
workbasketsResourceChange = new EventEmitter<Page>();
@Input()
numberOfItems: number;
@Output()
changePage = new EventEmitter<number>();
@Input()
numberOfItems: number;
@ViewChild(MatPaginator, { static: true })
paginator: MatPaginator;
hasItems = true;
previousPageSelected = 1;
pageSelected = 1;
maxPagesAvailable = 8;
pageNumbers: number[];
ngOnInit() {
// Custom label: EG. "1-7 of 21 workbaskets"
// return `${start} - ${end} of ${length} workbaskets`;
this.paginator._intl.itemsPerPageLabel = 'Per page';
this.paginator._intl.getRangeLabel = (page: number, pageSize: number, length: number) => {
page += 1;
const start = pageSize * (page - 1) + 1;
const end = pageSize * page < length ? pageSize * page : length;
if (length === 0) {
return 'loading...';
} else {
return `${start} - ${end} of ${length}`;
}
};
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.page && changes.page.currentValue) {
this.pageSelected = changes.page.currentValue.number;
}
this.hasItems = this.numberOfItems > 0;
}
changeToPage(p) {
let page = p;
if (page < 1) {
this.pageSelected = 1;
page = this.pageSelected;
}
if (page > this.page.totalPages) {
page = this.page.totalPages;
}
if (this.previousPageSelected !== page) {
this.changePage.emit(page);
this.previousPageSelected = page;
this.pageSelected = page;
if (changes.page) {
this.updateGoto();
}
}
getPagesTextToShow(): string {
if (!this.page) {
return '';
changeToPage(event) {
let currentPageIndex = event.pageIndex;
if (currentPageIndex > event.previousPageIndex) {
this.pageSelected += 1;
} else {
this.pageSelected -= 1;
}
const text = `${this.numberOfItems}`;
return `${text} of ${this.page.totalElements} ${this.type}`;
this.changePage.emit(currentPageIndex + 1);
}
updateGoto() {
this.pageNumbers = [];
for (let i = 1; i <= this.page?.totalPages; i++) {
this.pageNumbers.push(i);
}
}
goToPage(page: number) {
this.paginator.pageIndex = page - 1;
this.pageSelected = page;
this.changePage.emit(page);
}
}

View File

@ -1,16 +1,18 @@
<mat-nav-list>
<a mat-list-item class="list-item list-item__admin" [routerLink]=[workbasketsUrl] [routerLinkActive]="['active']"
<mat-nav-list class="navlist">
<a mat-list-item class="navlist__item navlist__admin" [routerLink]=[administrationsUrl] [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']"
<div class="navlist__admin-item">
<a mat-list-item class="navlist__item navlist__admin-workbaskets" [routerLink]=[workbasketsUrl]
[routerLinkActive]="['active']" *ngIf="administrationAccess" (click)="toggleSidenav()">Workbaskets</a>
<a mat-list-item class="navlist__item navlist__admin-classifications" [routerLink]=[classificationUrl]
[routerLinkActive]="['active']" *ngIf="administrationAccess" (click)="toggleSidenav()">Classifications</a>
<a mat-list-item class="navlist__item navlist__admin-access-items" [routerLink]=[accessUrl]
[routerLinkActive]="['active']" (click)="toggleSidenav()" *ngIf="administrationAccess">Access Items</a>
</div>
<a mat-list-item class="navlist__item navlist__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']"
<a mat-list-item class="navlist__item navlist__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']"
<a mat-list-item class="navlist__item navlist__history" [routerLink]=[historyUrl] [routerLinkActive]="['active']"
*ngIf="historyAccess" (click)="toggleSidenav()">History</a>
</mat-nav-list>
</mat-nav-list>

View File

@ -1,33 +1,17 @@
@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;
.navlist__admin-item {
margin-left: 30px;
}
.list-item__admin-acces-items {
color: $grey;
margin-left: 30px;
.navlist__item {
color: #ddd;
}
.active {
color: white !important;
color: $aquamarine;
border-left: 5px solid $aquamarine;
}
::ng-deep .mat-drawer-container {

View File

@ -1,15 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement } from '@angular/core';
import { 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 { SidenavService } from '../../services/sidenav/sidenav.service';
import { BrowserModule, By } from '@angular/platform-browser';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { RouterModule } from '@angular/router';
@ -17,6 +10,12 @@ 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';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatGridListModule } from '@angular/material/grid-list';
import { MatListModule } from '@angular/material/list';
import { MatIconModule } from '@angular/material/icon';
const SidenavServiceSpy = jest.fn().mockImplementation(
(): Partial<SidenavService> => ({
@ -75,7 +74,7 @@ describe('SidenavListComponent', () => {
component.workplaceAccess = true;
component.historyAccess = true;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.list-item'));
const menuList = debugElement.queryAll(By.css('.navlist__item'));
expect(menuList.length).toBe(7);
fixture.detectChanges();
});
@ -86,14 +85,14 @@ describe('SidenavListComponent', () => {
component.workplaceAccess = false;
component.historyAccess = false;
fixture.detectChanges();
const menuList = debugElement.queryAll(By.css('.list-item'));
const menuList = debugElement.queryAll(By.css('.navlist__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;
const button = debugElement.query(By.css('.navlist__admin-workbaskets')).nativeElement;
expect(button).toBeTruthy();
button.click();
expect(component.toggle).toBe(false);

View File

@ -19,14 +19,13 @@ export class SidenavListComponent implements OnInit {
accessUrl = 'taskana/administration/access-items-management';
classificationUrl = 'taskana/administration/classifications';
workbasketsUrl = 'taskana/administration/workbaskets';
administrationsUrl = 'taskana/administration';
administrationAccess = false;
monitorAccess = false;
workplaceAccess = false;
historyAccess = false;
admin_url_list: any[];
constructor(private taskanaEngineService: TaskanaEngineService, private sidenavService: SidenavService) {}
ngOnInit() {

View File

@ -1,36 +1,60 @@
<div class="dropdown">
<button class="btn btn-default" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="true">
<span class="material-icons md-20 blue {{sort.sortDirection === 'asc'? '' : 'flip' }}"
data-toggle="tooltip"
title="{{sort.sortDirection === 'asc'? 'A-Z' : 'Z-A' }}">sort</span>
<div class="sort">
<button mat-stroked-button style="color: #555;" [matMenuTriggerFor]="sortMenu" matTooltip="Sort workbaskets">
<mat-icon>sort</mat-icon>
<mat-menu #sortMenu="matMenu">
<button mat-menu-item [matMenuTriggerFor]="sortDirection">Sort direction</button>
<button mat-menu-item [matMenuTriggerFor]="sortValue">Sort value</button>
</mat-menu>
<!-- SORT DIRECTION -->
<mat-menu #sortDirection="matMenu">
<!-- ASCENDING ORDER BUTTON -->
<button mat-menu-item (click)="changeOrder('asc')">
<span *ngIf="sort.sortDirection === 'asc'; else coloredAsc">
<mat-icon class="sort__selected-value"> arrow_upward </mat-icon>
<span class="sort__selected-value"> Ascending </span>
</span>
<ng-template #coloredAsc>
<mat-icon> arrow_upward </mat-icon>
<span> Ascending </span>
</ng-template>
</button>
<!-- DESCENDING ORDER BUTTON -->
<button mat-menu-item (click)="changeOrder('desc')">
<span *ngIf="sort.sortDirection === 'desc'; else coloredDesc">
<mat-icon class="sort__selected-value"> arrow_downward </mat-icon>
<span class="sort__selected-value"> Descending </span>
</span>
<ng-template #coloredDesc>
<mat-icon> arrow_downward </mat-icon>
<span> Descending </span>
</ng-template>
</button>
</mat-menu>
<!-- SORT VALUE -->
<mat-menu #sortValue="matMenu">
<button mat-menu-item *ngFor="let sortingField of sortingFields | mapValues"
(click)="changeSortBy(sortingField.key)">
<span *ngIf="sortingField.value.toLowerCase() === sort.sortBy; else coloredValue" class="sort__selected-value">
{{sortingField.value}}
</span>
<ng-template #coloredValue>
<span>{{sortingField.value}}</span>
</ng-template>
</button>
</mat-menu>
</button>
<div class="dropdown-menu dropdown-menu-{{menuPosition}} sortby-dropdown popup"
aria-labelledby="sortingDropdown">
<div>
<div class="col-xs-6">
<h5>Sort By</h5>
</div>
<div class="btn-group">
<button id="sort-by-direction-asc" type="button" (click)="changeOrder('asc')"
data-toggle="tooltip"
title="A-Z"
class="btn btn-default {{sort.sortDirection === 'asc'? 'selected' : '' }}">
<span class="material-icons md-20 blue ">sort</span>
</button>
<button id="sort-by-direction-desc" type="button" (click)="changeOrder('desc')"
data-toggle="tooltip"
title="Z-A"
class="btn btn-default {{sort.sortDirection === 'desc'? 'selected' : '' }}">
<span class="material-icons md-20 blue flip">sort</span>
</button>
</div>
</div>
<div role="separator" class="divider"></div>
<mat-radio-group name="sort" color="accent" class="radio-group">
<mat-radio-button *ngFor="let sortingField of sortingFields | mapValues"
name="sort" id="sort-by-{{sortingField.key}}" [checked]="sortingField.key === defaultSortBy"
(change)="changeSortBy(sortingField.key)" [value]="sortingField.value"> {{ sortingField.value }}</mat-radio-button>
</mat-radio-group>
</div>
</div>
<!-- with radio buttons
<mat-radio-group name="sort" class="radio-group">
<mat-radio-button *ngFor="let sortingField of sortingFields | mapValues"
name="sort" [checked]="sortingField.key === defaultSortBy"
(change)="changeSortBy(sortingField.key)" [value]="sortingField.value"> {{ sortingField.value }}</mat-radio-button>
</mat-radio-group> -->

View File

@ -1,14 +1,7 @@
.sortby-dropdown {
min-width: 200px;
}
@import 'src/theme/_colors.scss';
.bold-blue {
color: #337ab7;
.sort__selected-value {
color: $aquamarine;
font-weight: bold;
}
.radio-group {
display: flex;
flex-direction: column;
margin: 0rem 2.5rem;
}

View File

@ -31,4 +31,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { MatSidenav } from '@angular/material';
import { MatSidenav } from '@angular/material/sidenav';
@Injectable({
providedIn: 'root'

View File

@ -19,6 +19,7 @@ import { WorkbasketRepresentation } from '../../models/workbasket-representation
export class WorkbasketService {
public workBasketSelected = new Subject<string>();
public workBasketSaved = new Subject<number>();
public workbasketActionToolbarExpanded = new Subject<boolean>();
private workbasketSummaryRef: Observable<WorkbasketSummaryRepresentation> = new Observable();
constructor(private httpClient: HttpClient, private domainService: DomainService) {}
@ -146,6 +147,14 @@ export class WorkbasketService {
return this.workBasketSelected.asObservable();
}
expandWorkbasketActionToolbar(value: boolean) {
this.workbasketActionToolbarExpanded.next(value);
}
getWorkbasketActionToolbarExpansion(): Observable<boolean> {
return this.workbasketActionToolbarExpanded.asObservable();
}
triggerWorkBasketSaved() {
this.workBasketSaved.next(Date.now());
}

View File

@ -54,6 +54,11 @@ import { ToastComponent } from './components/toast/toast.component';
import { DialogPopUpComponent } from './components/popup/dialog-pop-up.component';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSelectModule } from '@angular/material/select';
const MODULES = [
CommonModule,
@ -99,7 +104,17 @@ const DECLARATIONS = [
@NgModule({
declarations: DECLARATIONS,
imports: [MODULES, MatRadioModule, MatFormFieldModule, MatInputModule],
imports: [
MODULES,
MatRadioModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
MatMenuModule,
MatTooltipModule,
MatPaginatorModule,
MatSelectModule
],
exports: DECLARATIONS,
providers: [
{

View File

@ -3,6 +3,8 @@ import { TaskanaQueryParameters } from '../../util/query-parameters';
import { Direction } from '../../models/sorting';
import { ACTION } from '../../models/action';
import { WorkbasketAccessItems } from '../../models/workbasket-access-items';
import { WorkbasketComponent } from '../../../administration/models/workbasket-component';
import { ButtonAction } from '../../../administration/models/button-action';
// Workbasket List
export class GetWorkbasketsSummary {
@ -43,6 +45,17 @@ export class SetActiveAction {
constructor(public action: ACTION) {}
}
//Workbasket Details
export class SelectComponent {
static readonly type = '[Workbasket] Select component';
constructor(public component: WorkbasketComponent) {}
}
export class OnButtonPressed {
static readonly type = '[Workbasket] Button pressed';
constructor(public button: ButtonAction) {}
}
// Workbasket Information
export class SaveNewWorkbasket {
static readonly type = '[Workbasket] Save new workbasket';

View File

@ -6,6 +6,8 @@ import { ACTION } from '../../models/action';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { WorkbasketDistributionTargets } from '../../models/workbasket-distribution-targets';
import { Workbasket } from '../../models/workbasket';
import { WorkbasketComponent } from '../../../administration/models/workbasket-component';
import { ButtonAction } from '../../../administration/models/button-action';
export class WorkbasketSelectors {
// Workbasket
@ -37,6 +39,16 @@ export class WorkbasketSelectors {
};
}
@Selector([WorkbasketState])
static selectedComponent(state: WorkbasketStateModel): WorkbasketComponent {
return state.selectedComponent;
}
@Selector([WorkbasketState])
static buttonAction(state: WorkbasketStateModel): ButtonAction {
return state.button;
}
// Workbasket Access Items
@Selector([WorkbasketState])
static workbasketAccessItems(state: WorkbasketStateModel): WorkbasketAccessItemsRepresentation {

View File

@ -12,8 +12,10 @@ import {
GetWorkbasketDistributionTargets,
GetWorkbasketsSummary,
MarkWorkbasketForDeletion,
OnButtonPressed,
RemoveDistributionTarget,
SaveNewWorkbasket,
SelectComponent,
SelectWorkbasket,
SetActiveAction,
UpdateWorkbasket,
@ -27,6 +29,8 @@ import { NotificationService } from '../../services/notifications/notification.s
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { WorkbasketDistributionTargets } from '../../models/workbasket-distribution-targets';
import { WorkbasketSummary } from '../../models/workbasket-summary';
import { WorkbasketComponent } from '../../../administration/models/workbasket-component';
import { ButtonAction } from '../../../administration/models/button-action';
class InitializeStore {
static readonly type = '[Workbasket] Initializing state';
@ -42,6 +46,9 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(GetWorkbasketsSummary)
getWorkbasketsSummary(ctx: StateContext<WorkbasketStateModel>, action: GetWorkbasketsSummary): Observable<any> {
ctx.patchState({
paginatedWorkbasketsSummary: undefined
});
return this.workbasketService
.getWorkBasketsSummary(
action.forceRequest,
@ -110,8 +117,31 @@ export class WorkbasketState implements NgxsAfterBootstrap {
return of(null);
}
@Action(SelectComponent)
selectComponent(ctx: StateContext<WorkbasketStateModel>, action: SelectComponent): Observable<any> {
switch (action.component) {
case WorkbasketComponent.INFORMATION:
ctx.patchState({ selectedComponent: WorkbasketComponent.INFORMATION });
break;
case WorkbasketComponent.ACCESS_ITEMS:
ctx.patchState({ selectedComponent: WorkbasketComponent.ACCESS_ITEMS });
break;
case WorkbasketComponent.DISTRIBUTION_TARGETS:
ctx.patchState({ selectedComponent: WorkbasketComponent.DISTRIBUTION_TARGETS });
break;
}
return of(null);
}
@Action(OnButtonPressed)
doWorkbasketDetailsAction(ctx: StateContext<WorkbasketStateModel>, action: OnButtonPressed): Observable<any> {
ctx.patchState({ button: action.button });
return of(null);
}
@Action(SaveNewWorkbasket)
saveNewWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: SaveNewWorkbasket): Observable<any> {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.createWorkbasket(action.workbasket).pipe(
take(1),
tap(
@ -134,6 +164,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(CopyWorkbasket)
copyWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: CopyWorkbasket): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets/(detail:new-workbasket)'));
ctx.dispatch(new OnButtonPressed(undefined));
ctx.patchState({
action: ACTION.COPY
});
@ -142,6 +173,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(UpdateWorkbasket)
updateWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: UpdateWorkbasket): Observable<any> {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.updateWorkbasket(action.url, action.workbasket).pipe(
take(1),
tap(
@ -156,7 +188,6 @@ export class WorkbasketState implements NgxsAfterBootstrap {
paginatedWorkbasketSummary.workbaskets,
action.workbasket
);
ctx.patchState({
selectedWorkbasket: updatedWorkbasket,
paginatedWorkbasketsSummary: paginatedWorkbasketSummary
@ -171,6 +202,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(RemoveDistributionTarget)
removeDistributionTarget(ctx: StateContext<WorkbasketStateModel>, action: RemoveDistributionTarget): Observable<any> {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.removeDistributionTarget(action.url).pipe(
take(1),
tap(
@ -193,6 +225,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(MarkWorkbasketForDeletion)
deleteWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: MarkWorkbasketForDeletion): Observable<any> {
ctx.dispatch(new OnButtonPressed(undefined));
return this.workbasketService.markWorkbasketForDeletion(action.url).pipe(
take(1),
tap((response) => {
@ -308,4 +341,6 @@ export interface WorkbasketStateModel {
action: ACTION;
workbasketAccessItems: WorkbasketAccessItemsRepresentation;
workbasketDistributionTargets: WorkbasketDistributionTargets;
selectedComponent: WorkbasketComponent;
button: ButtonAction | undefined;
}