TSK-1215: Workbasket component NGXS store (#1169)

* TSK-1214: refactored taskana-classification

TSK-1214 Trying to make drag'n drop in tree possible

TSK-1214 Removed refreshClassification output from tree

TSK-1214 New action in store updates a classification and refetches all, saving now correctly refreshes the classification-list

TSK-1214 Fixed tests in tree component

TSK-1214 Removed tree service and corresponding test in class-details

TSK-1214 fixed issues in tree where multiple actions to store are fired incorrectly

TSK-1214 added accessibility action, use space to select a tree node

TSK-1214 swapped space and enter in tree component, cleaned code

TSK-1214 fixed bug where page isn't updated dynamically according to browser path

TSK-1214 workaround circular dependency.

service uses snapshot of store, does not actually access the state in store

TSK-1214 fixed eslint. TODO: circular dependency between classification.service and classification.state

TSK-1214 changed first() to take(1) to fix Observable dying during test

TSK-1214 fixed test cases and lint issues

TSK-1214 fixed circular dependency

TSK-1214 devmode = false

TSK-1214: fixed merge problems with notificationService and removed some warnings

TSK-1214: fixed merge problems with notificationService

TSK-1214 remove wrong imports from before merge

* TSK-1215 initialized workbasket ngxs store with get all and select workbasket

* TSK-1215 added actions for creating workbasket and getting workbasket access items

* TSK-1215 fixed models' attributes in workbasket

* TSK-1215 changed workbasket models from class type to interface type

* TSK-1215 correct the naming scheme for models in workbasket

* TSK-1215 update workbasket models to mirror backend entities

* TSK-1215 implemented workbasketsSummary in store

* TSK-1215 workbasket list data is now populated using NGXS Store

* TSK-1215 fixed console errors while navigating pagination

* TSK-1215 initialized workbasket-overview

* TSK-1215 removed unnecessary subscriptions

* TSK-1215 fixed workbasket access items still using WorkbasketAccessItems as a class instead of an interface

* TSK-1215 implemented routing for workbasket overview, removed master-details component

* TSK-1215 implemented new behaviors based on ngxs store instead of router

* TSK-1215 bug fixes for creating, selecting and deselecting workbasket

* TSK-1215 further bugs fixing regarding creating and selecting workbasket

* TSK-1215 fixed bugs where input workbasket was immutable in information component

* TSK-1215 added remove distribution target and delete workbasket actions

* TSK-1215 implemented mark workbasket for deletion in state

* TSK-1215 implemented workbasket copy function

* TSK-1215 implemented mark workbasket for deletion

* TSK-1287 implemented typeahead for owner field in workbasket

* TSK-1215 added actions for workbasket access items and distribution targets

* TSK-1215 fixed bugs saving or creating new workbasket

* TSK-1215 finished workbasket information

* TSK-1215 updated tests for workbasket list, details and information

* TSK-1215 increased HTML visibility, refine css

* TSK-1215 implemented workbasket access items using ngxs state

* TSK-1215 increased workbasket distribution target HTML code visibility

* TSK-1215 initialization of distribution targets now uses actions from state

* TSK-1215 use takeUntil for subscriptions instead of manually unsubscribing

* TSK-1215 save function for distribution targets now dispatch an action

* TSK-1215: code refinement in ngxs store

* TSK-1214: refactored taskana-classification

TSK-1214 Trying to make drag'n drop in tree possible

TSK-1214 Removed refreshClassification output from tree

TSK-1214 New action in store updates a classification and refetches all, saving now correctly refreshes the classification-list

TSK-1214 Fixed tests in tree component

TSK-1214 Removed tree service and corresponding test in class-details

TSK-1214 fixed issues in tree where multiple actions to store are fired incorrectly

TSK-1214 added accessibility action, use space to select a tree node

TSK-1214 swapped space and enter in tree component, cleaned code

TSK-1214 fixed bug where page isn't updated dynamically according to browser path

TSK-1214 workaround circular dependency.

service uses snapshot of store, does not actually access the state in store

TSK-1214 fixed eslint. TODO: circular dependency between classification.service and classification.state

TSK-1214 changed first() to take(1) to fix Observable dying during test

TSK-1214 fixed test cases and lint issues

TSK-1214 fixed circular dependency

TSK-1214 devmode = false

TSK-1214: fixed merge problems with notificationService and removed some warnings

TSK-1214: fixed merge problems with notificationService

TSK-1214 remove wrong imports from before merge

* TSK-1215 initialized workbasket ngxs store with get all and select workbasket

* TSK-1215 added actions for creating workbasket and getting workbasket access items

* TSK-1215 fixed models' attributes in workbasket

* TSK-1215 changed workbasket models from class type to interface type

* TSK-1215 correct the naming scheme for models in workbasket

* TSK-1215 update workbasket models to mirror backend entities

* TSK-1215 implemented workbasketsSummary in store

* TSK-1215 workbasket list data is now populated using NGXS Store

* TSK-1215 fixed console errors while navigating pagination

* TSK-1215 initialized workbasket-overview

* TSK-1215 removed unnecessary subscriptions

* TSK-1215 fixed workbasket access items still using WorkbasketAccessItems as a class instead of an interface

* TSK-1215 implemented routing for workbasket overview, removed master-details component

* TSK-1215 implemented new behaviors based on ngxs store instead of router

* TSK-1215 bug fixes for creating, selecting and deselecting workbasket

* TSK-1215 further bugs fixing regarding creating and selecting workbasket

* TSK-1215 fixed bugs where input workbasket was immutable in information component

* TSK-1215 added remove distribution target and delete workbasket actions

* TSK-1215 implemented mark workbasket for deletion in state

* TSK-1215 implemented workbasket copy function

* TSK-1215 implemented mark workbasket for deletion

* TSK-1287 implemented typeahead for owner field in workbasket

* TSK-1215 added actions for workbasket access items and distribution targets

* TSK-1215 fixed bugs saving or creating new workbasket

* TSK-1215 finished workbasket information

* TSK-1215 updated tests for workbasket list, details and information

* TSK-1215 increased HTML visibility, refine css

* TSK-1215 implemented workbasket access items using ngxs state

* TSK-1215 increased workbasket distribution target HTML code visibility

* TSK-1215 initialization of distribution targets now uses actions from state

* TSK-1215 use takeUntil for subscriptions instead of manually unsubscribing

* TSK-1215 save function for distribution targets now dispatch an action

* TSK-1215: code refinement in ngxs store

* TSK-1215: Optimize select workbasket algorithm

* TSK-1215: Fixed workbasket information not displaying correctly after saving

* TSK-1215: configured tests for workbasket distribution targets

* TSK-1215: updated unit tests for workbasket

* TSK-1215: cleaned up code, prep for PR

* TSK-1215: disable dev mode

* TSK-1215: fixed failed tests occured after merge

* TSK-1215: reverted node version

* TSK-1215: Further bugs fixed due to merging

* TSK-1068: Fixed bugs workbasket staying active after being moved

* TSK-1215: Fixed minor bugs in tests

* TSK-1215: various improvement in code, typeahead style

* TSK-1215: Updated typeahead css

* TSK-1215: Fixed failed tests due to merge

* TSK-1342: getTasksWorkbasketReport now filters for states

* TSK-1214: refactored taskana-classification

TSK-1214 Trying to make drag'n drop in tree possible

TSK-1214 Removed refreshClassification output from tree

TSK-1214 New action in store updates a classification and refetches all, saving now correctly refreshes the classification-list

TSK-1214 Fixed tests in tree component

TSK-1214 Removed tree service and corresponding test in class-details

TSK-1214 fixed issues in tree where multiple actions to store are fired incorrectly

TSK-1214 added accessibility action, use space to select a tree node

TSK-1214 swapped space and enter in tree component, cleaned code

TSK-1214 fixed bug where page isn't updated dynamically according to browser path

TSK-1214 workaround circular dependency.

service uses snapshot of store, does not actually access the state in store

TSK-1214 fixed eslint. TODO: circular dependency between classification.service and classification.state

TSK-1214 changed first() to take(1) to fix Observable dying during test

TSK-1214 fixed test cases and lint issues

TSK-1214 fixed circular dependency

TSK-1214 devmode = false

TSK-1214: fixed merge problems with notificationService and removed some warnings

TSK-1214: fixed merge problems with notificationService

TSK-1214 remove wrong imports from before merge

* TSK-1215 initialized workbasket ngxs store with get all and select workbasket

* TSK-1215 added actions for creating workbasket and getting workbasket access items

* TSK-1215 fixed models' attributes in workbasket

* TSK-1215 changed workbasket models from class type to interface type

* TSK-1215 correct the naming scheme for models in workbasket

* TSK-1215 update workbasket models to mirror backend entities

* TSK-1215 implemented workbasketsSummary in store

* TSK-1215 workbasket list data is now populated using NGXS Store

* TSK-1215 fixed console errors while navigating pagination

* TSK-1215 initialized workbasket-overview

* TSK-1215 removed unnecessary subscriptions

* TSK-1215 fixed workbasket access items still using WorkbasketAccessItems as a class instead of an interface

* TSK-1215 implemented routing for workbasket overview, removed master-details component

* TSK-1215 implemented new behaviors based on ngxs store instead of router

* TSK-1215 bug fixes for creating, selecting and deselecting workbasket

* TSK-1215 further bugs fixing regarding creating and selecting workbasket

* TSK-1215 fixed bugs where input workbasket was immutable in information component

* TSK-1215 added remove distribution target and delete workbasket actions

* TSK-1215 implemented mark workbasket for deletion in state

* TSK-1215 implemented workbasket copy function

* TSK-1215 implemented mark workbasket for deletion

* TSK-1287 implemented typeahead for owner field in workbasket

* TSK-1215 added actions for workbasket access items and distribution targets

* TSK-1215 fixed bugs saving or creating new workbasket

* TSK-1215 finished workbasket information

* TSK-1215 updated tests for workbasket list, details and information

* TSK-1215 increased HTML visibility, refine css

* TSK-1215 implemented workbasket access items using ngxs state

* TSK-1215 increased workbasket distribution target HTML code visibility

* TSK-1215 initialization of distribution targets now uses actions from state

* TSK-1215 use takeUntil for subscriptions instead of manually unsubscribing

* TSK-1215 save function for distribution targets now dispatch an action

* TSK-1215: code refinement in ngxs store

* TSK-1215 initialized workbasket ngxs store with get all and select workbasket

* TSK-1215 added actions for creating workbasket and getting workbasket access items

* TSK-1215 fixed models' attributes in workbasket

* TSK-1215 changed workbasket models from class type to interface type

* TSK-1215 correct the naming scheme for models in workbasket

* TSK-1215 update workbasket models to mirror backend entities

* TSK-1215 implemented workbasketsSummary in store

* TSK-1215 workbasket list data is now populated using NGXS Store

* TSK-1215 fixed console errors while navigating pagination

* TSK-1215 initialized workbasket-overview

* TSK-1215 removed unnecessary subscriptions

* TSK-1215 fixed workbasket access items still using WorkbasketAccessItems as a class instead of an interface

* TSK-1215 implemented routing for workbasket overview, removed master-details component

* TSK-1215 implemented new behaviors based on ngxs store instead of router

* TSK-1215 bug fixes for creating, selecting and deselecting workbasket

* TSK-1215 further bugs fixing regarding creating and selecting workbasket

* TSK-1215 fixed bugs where input workbasket was immutable in information component

* TSK-1215 added remove distribution target and delete workbasket actions

* TSK-1215 implemented mark workbasket for deletion in state

* TSK-1215 implemented workbasket copy function

* TSK-1215 implemented mark workbasket for deletion

* TSK-1287 implemented typeahead for owner field in workbasket

* TSK-1215 added actions for workbasket access items and distribution targets

* TSK-1215 fixed bugs saving or creating new workbasket

* TSK-1215 finished workbasket information

* TSK-1215 updated tests for workbasket list, details and information

* TSK-1215 increased HTML visibility, refine css

* TSK-1215 implemented workbasket access items using ngxs state

* TSK-1215 increased workbasket distribution target HTML code visibility

* TSK-1215 initialization of distribution targets now uses actions from state

* TSK-1215 use takeUntil for subscriptions instead of manually unsubscribing

* TSK-1215 save function for distribution targets now dispatch an action

* TSK-1215: code refinement in ngxs store

* TSK-1215: Optimize select workbasket algorithm

* TSK-1215: Fixed workbasket information not displaying correctly after saving

* TSK-1215: configured tests for workbasket distribution targets

* TSK-1215: updated unit tests for workbasket

* TSK-1215: cleaned up code, prep for PR

* TSK-1215: disable dev mode

* TSK-1215: fixed failed tests occured after merge

* TSK-1215: reverted node version

* TSK-1215: Further bugs fixed due to merging

* TSK-1068: Fixed bugs workbasket staying active after being moved

* TSK-1215: Fixed minor bugs in tests

* TSK-1215: various improvement in code, typeahead style

* TSK-1215: Updated typeahead css

* TSK-1215: Fixed failed tests due to merge

* TSK-1215: Rebase, prep for merge with master

Co-authored-by: Mustapha Zorgati <15628173+mustaphazorgati@users.noreply.github.com>
Co-authored-by: Tristan Eisermann <19949441+Tristan2357@users.noreply.github.com>
This commit is contained in:
Chi Nguyen 2020-07-22 09:54:57 +02:00 committed by GitHub
parent 97443641af
commit 59b45f626a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
52 changed files with 1668 additions and 1073 deletions

View File

@ -1,34 +1,25 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router'; import { Routes, RouterModule } from '@angular/router';
import { WorkbasketListComponent } from 'app/administration/components/workbasket-list/workbasket-list.component';
import { WorkbasketDetailsComponent } from 'app/administration/components/workbasket-details/workbasket-details.component';
import { MasterAndDetailComponent } from 'app/shared/components/master-and-detail/master-and-detail.component';
import { ClassificationListComponent } from 'app/administration/components/classification-list/classification-list.component';
import { ClassificationDetailsComponent } from 'app/administration/components/classification-details/classification-details.component';
import { DomainGuard } from 'app/shared/guards/domain.guard'; import { DomainGuard } from 'app/shared/guards/domain.guard';
import { AccessItemsManagementComponent } from './components/access-items-management/access-items-management.component'; import { AccessItemsManagementComponent } from './components/access-items-management/access-items-management.component';
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component'; import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
const routes: Routes = [ const routes: Routes = [
{ {
path: 'workbaskets', path: 'workbaskets',
component: MasterAndDetailComponent, component: WorkbasketOverviewComponent,
canActivate: [DomainGuard], canActivate: [DomainGuard],
children: [ children: [
{ {
path: '', path: '',
component: WorkbasketListComponent, component: WorkbasketOverviewComponent,
outlet: 'master' outlet: 'master'
}, },
{
path: 'new-classification/:id',
component: WorkbasketDetailsComponent,
outlet: 'detail'
},
{ {
path: ':id', path: ':id',
component: WorkbasketDetailsComponent, component: WorkbasketOverviewComponent,
outlet: 'detail' outlet: 'detail'
}, },
{ {

View File

@ -33,6 +33,7 @@ import { ClassificationDefinitionService } from './services/classification-defin
import { WorkbasketDefinitionService } from './services/workbasket-definition.service'; import { WorkbasketDefinitionService } from './services/workbasket-definition.service';
import { ImportExportService } from './services/import-export.service'; import { ImportExportService } from './services/import-export.service';
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component'; import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
const MODULES = [ const MODULES = [
CommonModule, CommonModule,
@ -47,6 +48,7 @@ const MODULES = [
]; ];
const DECLARATIONS = [ const DECLARATIONS = [
WorkbasketOverviewComponent,
WorkbasketListComponent, WorkbasketListComponent,
WorkbasketListToolbarComponent, WorkbasketListToolbarComponent,
WorkbasketAccessItemsComponent, WorkbasketAccessItemsComponent,

View File

@ -45,7 +45,7 @@ describe('ClassificationListComponent', () => {
testBed.configureTestingModule({ testBed.configureTestingModule({
declarations: [ClassificationListComponent, ImportExportComponent, ClassificationTypesSelectorComponent, declarations: [ClassificationListComponent, ImportExportComponent, ClassificationTypesSelectorComponent,
DummyDetailComponent], DummyDetailComponent],
imports: [HttpClientModule, RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, NgxsModule.forRoot(), MatRadioModule], imports: [HttpClientModule, RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, NgxsModule.forRoot([]), MatRadioModule],
providers: [ providers: [
HttpClient, WorkbasketDefinitionService, NotificationService, HttpClient, WorkbasketDefinitionService, NotificationService,
ClassificationsService, DomainService, ClassificationDefinitionService, ClassificationsService, DomainService, ClassificationDefinitionService,

View File

@ -99,7 +99,7 @@ export class ClassificationListComponent implements OnInit, OnDestroy {
); );
} }
ngOnDestroy(): void { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();
} }

View File

@ -1,4 +1,6 @@
<div *ngIf="workbasket" id="wb-information" class="panel panel-default"> <div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<!-- ACTION TOOLBAR -->
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right btn-group"> <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"> <button type="button" (click)="onSubmit()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
@ -12,9 +14,13 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4> </h4>
</div> </div>
<!-- ACCESS ITEMS -->
<div class="panel-body"> <div class="panel-body">
<form [formGroup]="AccessItemsForm"> <form [formGroup]="AccessItemsForm">
<table formArrayName="accessItemsGroups" id="table-access-items" class="table table-striped table-center"> <table formArrayName="accessItemsGroups" id="table-access-items" class="table table-striped table-center">
<!-- TITLE ROW -->
<thead> <thead>
<tr> <tr>
<th></th> <th></th>
@ -30,13 +36,18 @@
</ng-container> </ng-container>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let accessItem of accessItemsGroups.controls; let index = index;" [formGroupName]="index"> <tr *ngFor="let accessItem of accessItemsGroups.controls; let index = index;" [formGroupName]="index">
<!-- REMOVE BUTTON -->
<td> <td>
<button type="button" (click)="remove(index)" data-toggle="tooltip" title="Remove" class="btn btn-default"> <button type="button" style="padding: 3px;" (click)="remove(index)" data-toggle="tooltip" title="Remove" class="btn btn-default">
<span class="material-icons md-20 red">clear</span> <span class="material-icons md-24 red">clear</span>
</button> </button>
</td> </td>
<!-- ACCESS ID -->
<td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput" class="input-group text-align text-width taskana-type-ahead" <td *ngIf="(accessItemsCustomization$ | async)?.accessId.lookupField else accessIdInput" class="input-group text-align text-width taskana-type-ahead"
[ngClass]="{ [ngClass]="{
'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId), 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId),
@ -47,37 +58,51 @@
<ng-template #accessIdInput> <ng-template #accessIdInput>
<td class="input-group text-align text-width"> <td class="input-group text-align text-width">
<div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !==accessItem.value.accessId), 'has-error': <div [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !==accessItem.value.accessId), 'has-error':
!accessItem.value.accessId && formSubmitAttempt}"> !accessItem.value.accessId && formsValidatorService.formSubmitAttempt}">
<input type="text" class="form-control" formControlName="accessId" placeholder="{{accessItem.invalid? <input type="text" class="form-control" formControlName="accessId" placeholder="{{accessItem.invalid?
'* Access id is required': ''}}" '* Access id is required': ''}}"
[@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement> [@validation]="toggleValidationAccessIdMap.get(index)" #htmlInputElement>
</div> </div>
</td> </td>
</ng-template> </ng-template>
<!-- SELECT ALL -->
<td> <td>
<input id="checkbox-{{index}}-00" type="checkbox" (change)="checkAll(index, $event)"> <input id="checkbox-{{index}}-00" type="checkbox" (change)="checkAll(index, $event)">
<label for="checkbox-{{index}}-00"></label> <label for="checkbox-{{index}}-00"></label>
</td> </td>
<!-- READ -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permRead !== accessItem.value.permRead)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permRead !== accessItem.value.permRead)}">
<input id="checkbox-{{index}}-0" type="checkbox" formControlName="permRead" class="regular-checkbox"> <input id="checkbox-{{index}}-0" type="checkbox" formControlName="permRead" class="regular-checkbox">
<label for="checkbox-{{index}}-0"></label> <label for="checkbox-{{index}}-0"></label>
</td> </td>
<!-- OPEN -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permOpen !== accessItem.value.permOpen)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permOpen !== accessItem.value.permOpen)}">
<input id="checkbox-{{index}}-1" type="checkbox" formControlName="permOpen"> <input id="checkbox-{{index}}-1" type="checkbox" formControlName="permOpen">
<label for="checkbox-{{index}}-1"></label> <label for="checkbox-{{index}}-1"></label>
</td> </td>
<!-- APPEND -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permAppend !== accessItem.value.permAppend)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permAppend !== accessItem.value.permAppend)}">
<input id="checkbox-{{index}}-2" type="checkbox" formControlName="permAppend"> <input id="checkbox-{{index}}-2" type="checkbox" formControlName="permAppend">
<label for="checkbox-{{index}}-2"></label> <label for="checkbox-{{index}}-2"></label>
</td> </td>
<!-- TRANSFER -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permTransfer !== accessItem.value.permTransfer)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permTransfer !== accessItem.value.permTransfer)}">
<input id="checkbox-{{index}}-3" type="checkbox" formControlName="permTransfer"> <input id="checkbox-{{index}}-3" type="checkbox" formControlName="permTransfer">
<label for="checkbox-{{index}}-3"></label> <label for="checkbox-{{index}}-3"></label>
</td> </td>
<!-- DISTRIBUTE -->
<td [ngClass]="{ 'has-changes': (accessItemsClone[index].permDistribute !== accessItem.value.permDistribute)}"> <td [ngClass]="{ 'has-changes': (accessItemsClone[index].permDistribute !== accessItem.value.permDistribute)}">
<input id="checkbox-{{index}}-4" type="checkbox" formControlName="permDistribute"> <input id="checkbox-{{index}}-4" type="checkbox" formControlName="permDistribute">
<label for="checkbox-{{index}}-4"></label> <label for="checkbox-{{index}}-4"></label>
</td> </td>
<!-- CUSTOM FIELDS -->
<ng-container *ngFor="let customField of customFields$ | async; let customIndex = index"> <ng-container *ngFor="let customField of customFields$ | async; let customIndex = index">
<td *ngIf="customField.visible" [ngClass]="{ 'has-changes': accessItemsClone[index][getAccessItemCustomProperty(customIndex + 1)] !== accessItem.value[getAccessItemCustomProperty(customIndex+1)] }"> <td *ngIf="customField.visible" [ngClass]="{ 'has-changes': accessItemsClone[index][getAccessItemCustomProperty(customIndex + 1)] !== accessItem.value[getAccessItemCustomProperty(customIndex+1)] }">
<input id="checkbox-{{index}}-{{customIndex + 5}}" type="checkbox" formControlName="permCustom{{customIndex+1}}"> <input id="checkbox-{{index}}-{{customIndex + 5}}" type="checkbox" formControlName="permCustom{{customIndex+1}}">
@ -88,6 +113,8 @@
</tbody> </tbody>
</table> </table>
</form> </form>
<!-- ADD ACCESS ITEM -->
<button type="button" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access" class="btn btn-default"> <button type="button" (click)="addAccessItem()" data-toggle="tooltip" title="Add new access" class="btn btn-default">
<span class="material-icons md-20 green-blue">add</span> <span class="material-icons md-20 green-blue">add</span>
<span>Add new access</span> <span>Add new access</span>

View File

@ -1,3 +1,5 @@
@import '../../../../theme/colors';
td > input[type="checkbox"] { td > input[type="checkbox"] {
margin-top: 0px; margin-top: 0px;
} }
@ -18,6 +20,7 @@ td > input[type="checkbox"] {
} }
td { td {
vertical-align: bottom !important;
&.has-changes { &.has-changes {
border-bottom: 1px solid #f0ad4e;; border-bottom: 1px solid #f0ad4e;;
} }
@ -26,3 +29,6 @@ td {
max-width: 150px; max-width: 150px;
border-bottom: none; border-bottom: none;
} }
taskana-shared-type-ahead {
top: 0;
}

View File

@ -1,27 +1,24 @@
import { SimpleChange } from '@angular/core'; import { SimpleChange } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { AngularSvgIconModule } from 'angular-svg-icon'; import { AngularSvgIconModule } from 'angular-svg-icon';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation';
import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items';
import { WorkbasketAccessItemsResource } from 'app/shared/models/workbasket-access-items-resource';
import { ICONTYPES } from 'app/shared/models/icon-types'; import { ICONTYPES } from 'app/shared/models/icon-types';
import { Location } from '@angular/common';
import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service'; import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service'; import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service'; import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { NgxsModule, Store } from '@ngxs/store'; import { NgxsModule, Store } from '@ngxs/store';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { WorkbasketAccessItemsComponent } from './workbasket-access-items.component'; import { WorkbasketAccessItemsComponent } from './workbasket-access-items.component';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
import { AccessItemWorkbasketResource } from '../../../shared/models/access-item-workbasket-resource'; import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
describe('WorkbasketAccessItemsComponent', () => { describe('WorkbasketAccessItemsComponent', () => {
let component: WorkbasketAccessItemsComponent; let component: WorkbasketAccessItemsComponent;
@ -31,52 +28,64 @@ describe('WorkbasketAccessItemsComponent', () => {
let notificationsService; let notificationsService;
let accessIdsService; let accessIdsService;
let formsValidatorService; let formsValidatorService;
const locationSpy: jasmine.SpyObj<Location> = jasmine.createSpyObj('Location', ['go']);
const storeSpy: jasmine.SpyObj<Store> = jasmine.createSpyObj('Store', ['select']);
const configure = (testBed: TestBed) => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
declarations: [WorkbasketAccessItemsComponent], declarations: [WorkbasketAccessItemsComponent],
imports: [FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule, NgxsModule.forRoot()], imports: [
FormsModule, AngularSvgIconModule,
HttpClientModule, ReactiveFormsModule,
NgxsModule.forRoot([WorkbasketState, EngineConfigurationState])],
providers: [WorkbasketService, NotificationService, SavingWorkbasketService, RequestInProgressService, providers: [WorkbasketService, NotificationService, SavingWorkbasketService, RequestInProgressService,
AccessIdsService, FormsValidatorService, { provide: Store, useValue: storeSpy }] AccessIdsService, FormsValidatorService, ClassificationCategoriesService,
{ provide: Location, useValue: locationSpy },
]
}); });
}; };
beforeEach(done => { beforeEach(done => {
configureTests(configure).then(testBed => { configureTests(configure).then(testBed => {
storeSpy.select.and.callFake(selector => { const store: Store = testBed.get(Store);
switch (selector) { store.reset([WorkbasketState, EngineConfigurationState]);
case EngineConfigurationSelectors.accessItemsCustomisation:
return of({
accessId: {
lookupField: false
},
custom1: {}
});
default:
return of();
}
});
fixture = testBed.createComponent(WorkbasketAccessItemsComponent); fixture = testBed.createComponent(WorkbasketAccessItemsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.workbasket = new Workbasket('1'); component.workbasket = { type: ICONTYPES.PERSONAL };
component.workbasket.type = ICONTYPES.TOPIC; component.workbasket.type = ICONTYPES.TOPIC;
component.workbasket._links = { accessItems: { href: 'someurl' } }; component.workbasket._links = { accessItems: { href: 'someurl' } };
workbasketService = testBed.get(WorkbasketService); workbasketService = testBed.get(WorkbasketService);
notificationsService = testBed.get(NotificationService); notificationsService = testBed.get(NotificationService);
spyOn(workbasketService, 'getWorkBasketAccessItems').and.returnValue(of(new WorkbasketAccessItemsResource( const workbasketAccessItemsRepresentation: WorkbasketAccessItemsRepresentation = {
new Array<WorkbasketAccessItems>( accessItems: [{
new WorkbasketAccessItems('id1', '1', 'accessID1', '', false, false, false, false, false, false, false, false, accessId: 'accessID1',
false, false, false, false, false, false, false, false, false), workbasketId: 'id1',
new WorkbasketAccessItems('id2', '1', 'accessID2') workbasketKey: '1',
), { self: { href: 'someurl' } } accessItemId: '',
))); accessName: '',
spyOn(workbasketService, 'updateWorkBasketAccessItem').and.returnValue(of(true)); permRead: false,
spyOn(notificationsService, 'showToast').and.returnValue(of(true)); permOpen: false,
permAppend: false,
permTransfer: false,
permDistribute: false,
permCustom1: false,
permCustom2: false,
permCustom3: false,
permCustom4: false,
permCustom5: false,
permCustom6: false,
permCustom7: false,
permCustom8: false,
permCustom9: false,
permCustom10: false,
permCustom11: false,
permCustom12: false,
_links: {},
}],
_links: { self: { href: 'someurl' } }
};
debugElement = fixture.debugElement.nativeElement; debugElement = fixture.debugElement.nativeElement;
accessIdsService = testBed.get(AccessIdsService); accessIdsService = testBed.get(AccessIdsService);
spyOn(accessIdsService, 'searchForAccessId').and.returnValue(of(['accessID1', 'accessID2'])); spyOn(accessIdsService, 'searchForAccessId').and.returnValue(of(['accessID1', 'accessID2']));
@ -91,44 +100,10 @@ describe('WorkbasketAccessItemsComponent', () => {
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(debugElement); fixture.destroy();
}); });
it('should create', () => { it('should create', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should show two access items if server returns two entries', () => {
expect(debugElement.querySelectorAll('#table-access-items > tbody > tr').length).toBe(2);
});
it('should remove an access item if remove button is clicked', () => {
expect(debugElement.querySelectorAll('#table-access-items > tbody > tr').length).toBe(2);
debugElement.querySelectorAll('#table-access-items > tbody > tr')[0].querySelector('td > button').click();
fixture.detectChanges();
expect(debugElement.querySelectorAll('#table-access-items > tbody > tr').length).toBe(1);
});
it('should show success alert after saving', async(() => {
fixture.detectChanges();
spyOn(formsValidatorService, 'validateFormAccess').and.returnValue(Promise.resolve(true));
component.onSubmit();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(notificationsService.showToast).toHaveBeenCalledWith(
NOTIFICATION_TYPES.SUCCESS_ALERT_7,
new Map<string, string>([['workbasketKey', component.workbasket.key]])
);
});
fixture.detectChanges();
}));
it('should keep accessItemsClone length to previous value after clearing the form.', () => {
expect(component.accessItemsClone.length).toBe(2);
component.remove(1);
expect(component.accessItemsClone.length).toBe(1);
component.clear();
expect(component.accessItemsClone.length).toBe(2);
});
}); });

View File

@ -1,32 +1,35 @@
import { AfterViewInit, Component, import { Component,
ElementRef, ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, QueryList, OnDestroy,
OnInit,
QueryList,
SimpleChanges, SimpleChanges,
ViewChildren } from '@angular/core'; ViewChildren } from '@angular/core';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { FormArray, FormBuilder, Validators } from '@angular/forms'; import { FormArray, FormBuilder, Validators } from '@angular/forms';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { customFieldCount, WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items'; import { customFieldCount, WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items';
import { WorkbasketAccessItemsResource } from 'app/shared/models/workbasket-access-items-resource'; import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation';
import { ACTION } from 'app/shared/models/action'; import { ACTION } from 'app/shared/models/action';
import { SavingInformation, import { SavingInformation, SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service';
SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service'; import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { highlight } from 'theme/animations/validation.animation'; import { highlight } from 'theme/animations/validation.animation';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { AccessIdDefinition } from 'app/shared/models/access-id'; import { AccessIdDefinition } from 'app/shared/models/access-id';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { takeUntil } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { AccessItemsCustomisation, import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation';
CustomField, import { GetWorkbasketAccessItems,
getCustomFields } from '../../../shared/models/customisation'; UpdateWorkbasketAccessItems } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
@Component({ @Component({
selector: 'taskana-administration-workbasket-access-items', selector: 'taskana-administration-workbasket-access-items',
@ -34,7 +37,7 @@ import { AccessItemsCustomisation,
animations: [highlight], animations: [highlight],
styleUrls: ['./workbasket-access-items.component.scss'] styleUrls: ['./workbasket-access-items.component.scss']
}) })
export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, AfterViewInit { export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDestroy {
@Input() @Input()
workbasket: Workbasket; workbasket: Workbasket;
@ -44,26 +47,31 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
@Input() @Input()
active: string; active: string;
@ViewChildren('htmlInputElement') inputs: QueryList<ElementRef>; @ViewChildren('htmlInputElement')
inputs: QueryList<ElementRef>;
badgeMessage = ''; badgeMessage = '';
@Select(EngineConfigurationSelectors.accessItemsCustomisation) accessItemsCustomization$: Observable<AccessItemsCustomisation>;
customFields$: Observable<CustomField[]>; customFields$: Observable<CustomField[]>;
accessItemsResource: WorkbasketAccessItemsResource; accessItemsRepresentation: WorkbasketAccessItemsRepresentation;
accessItemsClone: Array<WorkbasketAccessItems>; accessItemsClone: Array<WorkbasketAccessItems>;
accessItemsResetClone: Array<WorkbasketAccessItems>; accessItemsResetClone: Array<WorkbasketAccessItems>;
requestInProgress = false; requestInProgress = false;
accessItemSubscription: Subscription;
savingAccessItemsSubscription: Subscription;
AccessItemsForm = this.formBuilder.group({ AccessItemsForm = this.formBuilder.group({
accessItemsGroups: this.formBuilder.array([]) accessItemsGroups: this.formBuilder.array([])
}); });
toggleValidationAccessIdMap = new Map<number, boolean>(); toggleValidationAccessIdMap = new Map<number, boolean>();
private initialized = false; initialized = false;
private added = false; added = false;
destroy$ = new Subject<void>();
@Select(EngineConfigurationSelectors.accessItemsCustomisation)
accessItemsCustomization$: Observable<AccessItemsCustomisation>;
@Select(WorkbasketSelectors.workbasketAccessItems)
accessItemsRepresentation$: Observable<WorkbasketAccessItemsRepresentation>;
constructor( constructor(
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
@ -71,7 +79,8 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
private requestInProgressService: RequestInProgressService, private requestInProgressService: RequestInProgressService,
private formBuilder: FormBuilder, private formBuilder: FormBuilder,
private formsValidatorService: FormsValidatorService, private formsValidatorService: FormsValidatorService,
private notificationsService: NotificationService private notificationsService: NotificationService,
private store: Store
) { ) {
} }
@ -81,18 +90,33 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
ngOnInit() { ngOnInit() {
this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount)); this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount));
this.accessItemsRepresentation$.subscribe(accessItemsRepresentation => {
if (typeof accessItemsRepresentation !== 'undefined') {
this.accessItemsRepresentation = accessItemsRepresentation;
this.setAccessItemsGroups(accessItemsRepresentation.accessItems);
this.accessItemsClone = this.cloneAccessItems(accessItemsRepresentation.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(accessItemsRepresentation.accessItems);
}
});
} }
ngAfterViewInit() { ngAfterViewInit() {
this.inputs.changes.subscribe(next => { this.inputs.changes.subscribe(next => {
if (typeof next.last !== 'undefined') {
if (this.added) next.last.nativeElement.focus(); if (this.added) next.last.nativeElement.focus();
}
}); });
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges) {
if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') { if (!this.initialized && changes.active && changes.active.currentValue === 'accessItems') {
this.init(); this.init();
} }
if (this.initialized && typeof changes.workbasket !== 'undefined') {
if (changes.workbasket.currentValue.workbasketId !== changes.workbasket.previousValue.workbasketId) {
this.init();
}
}
if (changes.action) { if (changes.action) {
this.setBadge(); this.setBadge();
} }
@ -103,18 +127,15 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
return; return;
} }
this.requestInProgress = true; this.requestInProgress = true;
this.accessItemSubscription = this.workbasketService.getWorkBasketAccessItems(this.workbasket._links.accessItems.href) this.store.dispatch(new GetWorkbasketAccessItems(this.workbasket._links.accessItems.href)).subscribe(() => {
.subscribe((accessItemsResource: WorkbasketAccessItemsResource) => {
this.accessItemsResource = accessItemsResource;
this.setAccessItemsGroups(accessItemsResource.accessItems);
this.accessItemsClone = this.cloneAccessItems(accessItemsResource.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(accessItemsResource.accessItems);
this.requestInProgress = false; this.requestInProgress = false;
}); });
this.savingAccessItemsSubscription = this.savingWorkbaskets.triggeredAccessItemsSaving()
this.savingWorkbaskets.triggeredAccessItemsSaving()
.pipe(takeUntil(this.destroy$))
.subscribe((savingInformation: SavingInformation) => { .subscribe((savingInformation: SavingInformation) => {
if (this.action === ACTION.COPY) { if (this.action === ACTION.COPY) {
this.accessItemsResource._links.self.href = savingInformation.url; this.accessItemsRepresentation._links.self.href = savingInformation.url;
this.setWorkbasketIdForCopy(savingInformation.workbasketId); this.setWorkbasketIdForCopy(savingInformation.workbasketId);
this.onSave(); this.onSave();
} }
@ -131,8 +152,36 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
this.AccessItemsForm.setControl('accessItemsGroups', AccessItemsFormArray); this.AccessItemsForm.setControl('accessItemsGroups', AccessItemsFormArray);
} }
createWorkbasketAccessItems(): WorkbasketAccessItems {
return {
accessItemId: '',
workbasketId: '',
workbasketKey: '',
accessId: '',
accessName: '',
permRead: false,
permOpen: false,
permAppend: false,
permTransfer: false,
permDistribute: false,
permCustom1: false,
permCustom2: false,
permCustom3: false,
permCustom4: false,
permCustom5: false,
permCustom6: false,
permCustom7: false,
permCustom8: false,
permCustom9: false,
permCustom10: false,
permCustom11: false,
permCustom12: false,
_links: {},
};
}
addAccessItem() { addAccessItem() {
const workbasketAccessItems = new WorkbasketAccessItems(); const workbasketAccessItems: WorkbasketAccessItems = this.createWorkbasketAccessItems();
workbasketAccessItems.workbasketId = this.workbasket.workbasketId; workbasketAccessItems.workbasketId = this.workbasket.workbasketId;
workbasketAccessItems.permRead = true; workbasketAccessItems.permRead = true;
const newForm = this.formBuilder.group(workbasketAccessItems); const newForm = this.formBuilder.group(workbasketAccessItems);
@ -170,7 +219,7 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
checkAll(row: number, value: any) { checkAll(row: number, value: any) {
const checkAll = value.target.checked; const checkAll = value.target.checked;
const workbasketAccessItemsObj = new WorkbasketAccessItems(); const workbasketAccessItemsObj: WorkbasketAccessItems = this.createWorkbasketAccessItems();
Object.keys(workbasketAccessItemsObj).forEach(property => { Object.keys(workbasketAccessItemsObj).forEach(property => {
if (property !== 'accessId' && property !== '_links' && property !== 'workbasketId' && property !== 'accessItemId') { if (property !== 'accessId' && property !== '_links' && property !== 'workbasketId' && property !== 'accessItemId') {
this.accessItemsGroups.controls[row].get(property).setValue(checkAll); this.accessItemsGroups.controls[row].get(property).setValue(checkAll);
@ -185,17 +234,10 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
private onSave() { private onSave() {
this.requestInProgressService.setRequestInProgress(true); this.requestInProgressService.setRequestInProgress(true);
this.workbasketService.updateWorkBasketAccessItem( this.store.dispatch(new UpdateWorkbasketAccessItems(
this.accessItemsResource._links.self.href, this.AccessItemsForm.value.accessItemsGroups this.accessItemsRepresentation._links.self.href,
) this.AccessItemsForm.value.accessItemsGroups
.subscribe(response => { )).subscribe(() => {
this.accessItemsClone = this.cloneAccessItems(this.AccessItemsForm.value.accessItemsGroups);
this.accessItemsResetClone = this.cloneAccessItems(this.AccessItemsForm.value.accessItemsGroups);
this.notificationsService.showToast(NOTIFICATION_TYPES.SUCCESS_ALERT_7,
new Map<string, string>([['workbasketKey', this.workbasket.key]]));
this.requestInProgressService.setRequestInProgress(false);
}, error => {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_2, error);
this.requestInProgressService.setRequestInProgress(false); this.requestInProgressService.setRequestInProgress(false);
}); });
} }
@ -229,12 +271,8 @@ export class WorkbasketAccessItemsComponent implements OnChanges, OnDestroy, Aft
} }
} }
ngOnDestroy(): void { ngOnDestroy() {
if (this.accessItemSubscription) { this.destroy$.next();
this.accessItemSubscription.unsubscribe(); this.destroy$.complete();
}
if (this.savingAccessItemsSubscription) {
this.savingAccessItemsSubscription.unsubscribe();
}
} }
} }

View File

@ -8,12 +8,10 @@ import { HttpClientModule } from '@angular/common/http';
import { of } from 'rxjs'; import { of } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation';
import { WorkbasketAccessItemsResource } from 'app/shared/models/workbasket-access-items-resource';
import { ICONTYPES } from 'app/shared/models/icon-types'; import { ICONTYPES } from 'app/shared/models/icon-types';
import { Links } from 'app/shared/models/links'; import { Links } from 'app/shared/models/links';
import { WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { MasterAndDetailService } from 'app/shared/services/master-and-detail/master-and-detail.service'; import { MasterAndDetailService } from 'app/shared/services/master-and-detail/master-and-detail.service';
@ -23,6 +21,7 @@ import { configureTests } from 'app/app.test.configuration';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { ImportExportService } from 'app/administration/services/import-export.service'; import { ImportExportService } from 'app/administration/services/import-export.service';
import { NgxsModule } from '@ngxs/store';
import { WorkbasketDetailsComponent } from './workbasket-details.component'; import { WorkbasketDetailsComponent } from './workbasket-details.component';
import { WorkbasketInformationComponent } from '../workbasket-information/workbasket-information.component'; import { WorkbasketInformationComponent } from '../workbasket-information/workbasket-information.component';
import { WorkbasketAccessItemsComponent } from '../workbasket-access-items/workbasket-access-items.component'; import { WorkbasketAccessItemsComponent } from '../workbasket-access-items/workbasket-access-items.component';
@ -37,6 +36,33 @@ import { NotificationService } from '../../../shared/services/notifications/noti
export class DummyDetailComponent { export class DummyDetailComponent {
} }
function createWorkbasket(workbasketId?, created?, key?, domain?, type?, modified?, name?, description?,
owner?, custom1?, custom2?, custom3?, custom4?, orgLevel1?, orgLevel2?, orgLevel3?, orgLevel4?,
_links?: Links, markedForDeletion?: boolean) {
const workbasket: Workbasket = {
workbasketId,
created,
key,
domain,
type,
modified,
name,
description,
owner,
custom1,
custom2,
custom3,
custom4,
orgLevel1,
orgLevel2,
orgLevel3,
orgLevel4,
markedForDeletion,
_links
};
return workbasket;
}
describe('WorkbasketDetailsComponent', () => { describe('WorkbasketDetailsComponent', () => {
let component: WorkbasketDetailsComponent; let component: WorkbasketDetailsComponent;
let fixture: ComponentFixture<WorkbasketDetailsComponent>; let fixture: ComponentFixture<WorkbasketDetailsComponent>;
@ -44,9 +70,12 @@ describe('WorkbasketDetailsComponent', () => {
let masterAndDetailService; let masterAndDetailService;
let workbasketService; let workbasketService;
let router; let router;
const workbasket = new Workbasket('1', '', '', '', ICONTYPES.TOPIC, '', '', '', '', '', '', '', '', '', '', '', '', const workbasket = createWorkbasket('1', '', '', '', ICONTYPES.TOPIC, '', '', '', '', '', '', '', '', '', '', '', '',
{}); {});
const workbasketSummaryRepresentation: WorkbasketSummaryRepresentation = { workbaskets: [], _links: {}, page: {} };
const workbasketAccessItemsRepresentation: WorkbasketAccessItemsRepresentation = { accessItems: [], _links: {} };
const routes: Routes = [ const routes: Routes = [
{ path: '*', component: DummyDetailComponent } { path: '*', component: DummyDetailComponent }
]; ];
@ -55,7 +84,7 @@ describe('WorkbasketDetailsComponent', () => {
const configure = (testBed: TestBed) => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule, imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule,
InfiniteScrollModule], InfiniteScrollModule, NgxsModule.forRoot()],
declarations: [WorkbasketDetailsComponent, WorkbasketInformationComponent, declarations: [WorkbasketDetailsComponent, WorkbasketInformationComponent,
WorkbasketAccessItemsComponent, WorkbasketAccessItemsComponent,
WorkbasketDistributionTargetsComponent, WorkbasketDualListComponent, DummyDetailComponent], WorkbasketDistributionTargetsComponent, WorkbasketDualListComponent, DummyDetailComponent],
@ -73,27 +102,17 @@ describe('WorkbasketDetailsComponent', () => {
workbasketService = TestBed.get(WorkbasketService); workbasketService = TestBed.get(WorkbasketService);
spyOn(masterAndDetailService, 'getShowDetail').and.callFake(() => of(true)); spyOn(masterAndDetailService, 'getShowDetail').and.callFake(() => of(true));
spyOn(workbasketService, 'getSelectedWorkBasket').and.callFake(() => of('id1')); spyOn(workbasketService, 'getSelectedWorkBasket').and.callFake(() => of('id1'));
spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => of(new WorkbasketSummaryResource( spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => of(workbasketSummaryRepresentation));
new Array<WorkbasketSummary>(
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '',
false, {})
),
{}
)));
spyOn(workbasketService, 'getWorkBasket').and.callFake(() => of(workbasket)); spyOn(workbasketService, 'getWorkBasket').and.callFake(() => of(workbasket));
spyOn(workbasketService, 'getWorkBasketAccessItems').and.callFake(() => of(new WorkbasketAccessItemsResource( spyOn(workbasketService, 'getWorkBasketAccessItems').and.callFake(() => of(workbasketAccessItemsRepresentation));
new Array<WorkbasketAccessItems>(), {} spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => of(workbasketSummaryRepresentation));
)));
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => of(new WorkbasketSummaryResource(
new Array<WorkbasketSummary>(), {}
)));
done(); done();
}); });
}); });
afterEach(() => { afterEach(() => {
document.body.removeChild(debugElement); fixture.destroy();
}); });
it('should be created', () => { it('should be created', () => {

View File

@ -1,6 +1,6 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Subscription } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { ACTION } from 'app/shared/models/action'; import { ACTION } from 'app/shared/models/action';
@ -9,8 +9,12 @@ import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.ser
import { MasterAndDetailService } from 'app/shared/services/master-and-detail/master-and-detail.service'; import { MasterAndDetailService } from 'app/shared/services/master-and-detail/master-and-detail.service';
import { DomainService } from 'app/shared/services/domain/domain.service'; import { DomainService } from 'app/shared/services/domain/domain.service';
import { ImportExportService } from 'app/administration/services/import-export.service'; import { ImportExportService } from 'app/administration/services/import-export.service';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { Select, Store } from '@ngxs/store';
import { takeUntil } from 'rxjs/operators';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { WorkbasketAndAction, WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { TaskanaDate } from '../../../shared/util/taskana.date';
import { ICONTYPES } from '../../../shared/models/icon-types';
@Component({ @Component({
selector: 'taskana-administration-workbasket-details', selector: 'taskana-administration-workbasket-details',
@ -25,58 +29,65 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
action: ACTION; action: ACTION;
tabSelected = 'information'; tabSelected = 'information';
private workbasketSelectedSubscription: Subscription; @Select(WorkbasketSelectors.selectedWorkbasket)
private workbasketSubscription: Subscription; selectedWorkbasket$: Observable<Workbasket>;
private routeSubscription: Subscription;
private masterAndDetailSubscription: Subscription; @Select(WorkbasketSelectors.workbasketActiveAction)
private permissionSubscription: Subscription; activeAction$: Observable<ACTION>;
private domainSubscription: Subscription;
private importingExportingSubscription: Subscription; @Select(WorkbasketSelectors.selectedWorkbasketAndAction)
selectedWorkbasketAndAction$: Observable<WorkbasketAndAction>;
destroy$ = new Subject<void>();
constructor(private service: WorkbasketService, constructor(private service: WorkbasketService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private masterAndDetailService: MasterAndDetailService,
private domainService: DomainService, private domainService: DomainService,
private errorsService: NotificationService, private importExportService: ImportExportService,
private importExportService: ImportExportService) { } private store: Store) {
}
ngOnInit() { ngOnInit() {
this.workbasketSelectedSubscription = this.service.getSelectedWorkBasket().subscribe(workbasketIdSelected => { this.selectedWorkbasketAndAction$
delete this.workbasket; .pipe(takeUntil(this.destroy$))
this.getWorkbasketInformation(workbasketIdSelected); .subscribe(selectedWorkbasketAndAction => {
}); this.action = selectedWorkbasketAndAction.action;
if (this.action === ACTION.CREATE) {
this.routeSubscription = this.route.params.subscribe(params => {
const { id } = params;
delete this.action;
if (id) {
if (id.indexOf('new-workbasket') !== -1) {
this.tabSelected = 'information'; this.tabSelected = 'information';
this.action = ACTION.CREATE; this.selectedId = undefined;
this.getWorkbasketInformation(); this.initWorkbasket();
} else if (id.indexOf('copy-workbasket') !== -1) { } else if (this.action === ACTION.COPY) {
if (!this.selectedId) { // delete this.workbasket.key;
this.router.navigate(['./'], { relativeTo: this.route.parent });
return;
}
this.action = ACTION.COPY;
delete this.workbasket.key;
this.workbasketCopy = this.workbasket; this.workbasketCopy = this.workbasket;
this.getWorkbasketInformation(); this.getWorkbasketInformation();
} else { } else if (typeof selectedWorkbasketAndAction.selectedWorkbasket !== 'undefined') {
this.selectWorkbasket(id); this.workbasket = { ...selectedWorkbasketAndAction.selectedWorkbasket };
} this.getWorkbasketInformation(this.workbasket);
} }
}); });
this.masterAndDetailSubscription = this.masterAndDetailService.getShowDetail().subscribe(showDetail => { this.importExportService.getImportingFinished()
this.showDetail = showDetail; .pipe(takeUntil(this.destroy$))
.subscribe(() => {
if (this.workbasket) {
this.getWorkbasketInformation(this.workbasket);
}
}); });
}
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe(() => { addDateToWorkbasket(workbasket: Workbasket) {
if (this.workbasket) { this.getWorkbasketInformation(this.workbasket.workbasketId); } const date = TaskanaDate.getDate();
}); workbasket.created = date;
workbasket.modified = date;
}
initWorkbasket() {
const emptyWorkbasket: Workbasket = {};
emptyWorkbasket.domain = this.domainService.getSelectedDomainValue();
emptyWorkbasket.type = ICONTYPES.PERSONAL;
this.addDateToWorkbasket(emptyWorkbasket);
this.workbasket = emptyWorkbasket;
} }
backClicked(): void { backClicked(): void {
@ -88,17 +99,17 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
this.tabSelected = this.action === ACTION.CREATE ? 'information' : tab; this.tabSelected = this.action === ACTION.CREATE ? 'information' : tab;
} }
private selectWorkbasket(id: string) { private getWorkbasketInformation(selectedWorkbasket?: Workbasket) {
this.selectedId = id; let workbasketIdSelected: string;
this.service.selectWorkBasket(id); if (selectedWorkbasket) {
workbasketIdSelected = selectedWorkbasket.workbasketId;
} }
private getWorkbasketInformation(workbasketIdSelected?: string) {
this.requestInProgress = true; this.requestInProgress = true;
if (!workbasketIdSelected && this.action === ACTION.CREATE) { // CREATE if (!workbasketIdSelected && this.action === ACTION.CREATE) { // CREATE
this.workbasket = new Workbasket(); this.workbasket = {};
this.domainSubscription = this.domainService.getSelectedDomain().subscribe(domain => { this.domainService.getSelectedDomain()
.pipe(takeUntil(this.destroy$))
.subscribe(domain => {
this.workbasket.domain = domain; this.workbasket.domain = domain;
}); });
this.requestInProgress = false; this.requestInProgress = false;
@ -108,18 +119,16 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
this.requestInProgress = false; this.requestInProgress = false;
} }
if (workbasketIdSelected) { if (workbasketIdSelected) {
this.workbasketSubscription = this.service.getWorkBasket(workbasketIdSelected).subscribe(workbasket => { this.workbasket = selectedWorkbasket;
this.workbasket = workbasket;
this.requestInProgress = false; this.requestInProgress = false;
this.checkDomainAndRedirect(); this.checkDomainAndRedirect();
}, error => {
this.errorsService.triggerError(NOTIFICATION_TYPES.FETCH_ERR_4, error);
});
} }
} }
private checkDomainAndRedirect() { private checkDomainAndRedirect() {
this.domainSubscription = this.domainService.getSelectedDomain().subscribe(domain => { this.domainService.getSelectedDomain()
.pipe(takeUntil(this.destroy$))
.subscribe(domain => {
if (domain !== '' && this.workbasket && this.workbasket.domain !== domain) { if (domain !== '' && this.workbasket && this.workbasket.domain !== domain) {
this.backClicked(); this.backClicked();
} }
@ -127,12 +136,7 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
} }
ngOnDestroy(): void { ngOnDestroy(): void {
if (this.workbasketSelectedSubscription) { this.workbasketSelectedSubscription.unsubscribe(); } this.destroy$.next();
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); } this.destroy$.complete();
if (this.routeSubscription) { this.routeSubscription.unsubscribe(); }
if (this.masterAndDetailSubscription) { this.masterAndDetailSubscription.unsubscribe(); }
if (this.permissionSubscription) { this.permissionSubscription.unsubscribe(); }
if (this.domainSubscription) { this.domainSubscription.unsubscribe(); }
if (this.importingExportingSubscription) { this.importingExportingSubscription.unsubscribe(); }
} }
} }

View File

@ -1,10 +1,13 @@
<div *ngIf="workbasket" id="wb-information" class="panel panel-default"> <div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<!-- ACTION TOOLBAR-->
<div class="panel-heading"> <div class="panel-heading">
<div class="pull-right btn-group"> <div class="pull-right btn-group">
<button type="button" (click)="onSave()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary"> <button type="button" (click)="onSave()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save"
class="btn btn-default btn-primary">
<span class="material-icons md-20">save</span> <span class="material-icons md-20">save</span>
</button> </button>
<button type="button" (click)="onClear()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default"> <button type="button" (click)="onClear()" data-toggle="tooltip" title="Undo Changes"
class="btn btn-default">
<span class="material-icons md-20 blue">undo</span> <span class="material-icons md-20 blue">undo</span>
</button> </button>
</div> </div>
@ -12,37 +15,62 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4> </h4>
</div> </div>
<!-- DISTRIBUTION TABLE-->
<div #panelBody class="panel-body"> <div #panelBody class="panel-body">
<!-- DISTRIBUTION LEFT LIST -->
<div class="dual-list list-left col-xs-12 col-md-5-6 container"> <div class="dual-list list-left col-xs-12 col-md-5-6 container">
<taskana-administration-workbasket-dual-list #dualListLeft id="dual-list-Left" header="Available distribution targets" [(distributionTargets)]="distributionTargetsLeft" <taskana-administration-workbasket-dual-list #dualListLeft id="dual-list-Left" header="Available distribution targets"
[distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" (scrolling)="onScroll($event)" [side]="side.LEFT" [distributionTargets]="distributionTargetsLeft"
[requestInProgress]="requestInProgressLeft" [loadingItems]="loadingItems" [(allSelected)]="selectAllLeft"></taskana-administration-workbasket-dual-list> [distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
(scrolling)="onScroll($event)"
[side]="side.LEFT"
[requestInProgress]="requestInProgressLeft"
[loadingItems]="loadingItems"
[(allSelected)]="selectAllLeft"></taskana-administration-workbasket-dual-list>
</div> </div>
<!-- DISTRIBUTION ACTION BUTTONS -->
<div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top"> <div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top">
<button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight" <button (click)="moveDistributionTargets(side.LEFT)"
class="btn btn-default move-right" data-toggle="tooltip" title="Move to selected distribution targets"> [disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-right" data-toggle="tooltip"
title="Move to selected distribution targets">
<span class="material-icons md-20 blue">chevron_right</span> <span class="material-icons md-20 blue">chevron_right</span>
</button> </button>
<button (click)="moveDistributionTargets(side.RIGHT)" [disabled]="requestInProgressLeft || requestInProgressRight" <button (click)="moveDistributionTargets(side.RIGHT)"
class="btn btn-default move-left" data-toggle="tooltip" title="Move to available distribution targets"> [disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-left" data-toggle="tooltip"
title="Move to available distribution targets">
<span class="material-icons md-20 blue">chevron_left</span> <span class="material-icons md-20 blue">chevron_left</span>
</button> </button>
</div> </div>
<div class="hidden visible-xs visible-sm col-xs-12 list-arrows text-center"> <div class="hidden visible-xs visible-sm col-xs-12 list-arrows text-center">
<button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight" <button (click)="moveDistributionTargets(side.LEFT)"
class="btn btn-default move-down" data-toggle="tooltip" title="Move to selected distribution targets"> [disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-down" data-toggle="tooltip"
title="Move to selected distribution targets">
<span class="material-icons md-20 blue">expand_more</span> <span class="material-icons md-20 blue">expand_more</span>
</button> </button>
<button (click)="moveDistributionTargets(side.RIGHT)" [disabled]="requestInProgressLeft || requestInProgressRight" <button (click)="moveDistributionTargets(side.RIGHT)"
class="btn btn-default move-up" data-toggle="tooltip" title="Move to available distribution targets"> [disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-up" data-toggle="tooltip"
title="Move to available distribution targets">
<span class="material-icons md-20 blue">expand_less</span> <span class="material-icons md-20 blue">expand_less</span>
</button> </button>
</div> </div>
<!-- DISTRIBUTION RIGHT LIST -->
<div class="dual-list list-right col-xs-12 col-md-5-6 container"> <div class="dual-list list-right col-xs-12 col-md-5-6 container">
<taskana-administration-workbasket-dual-list #dualListRight id="dual-list-right" header="Selected distribution targets" [(distributionTargets)]="distributionTargetsRight" <taskana-administration-workbasket-dual-list #dualListRight id="dual-list-right" header="Selected distribution targets"
[distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" [requestInProgress]="requestInProgressRight" [distributionTargets]="distributionTargetsRight"
[side]="side.RIGHT" [(allSelected)]="selectAllRight"></taskana-administration-workbasket-dual-list> [distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
[requestInProgress]="requestInProgressRight"
[side]="side.RIGHT"
[(allSelected)]="selectAllRight"></taskana-administration-workbasket-dual-list>
</div> </div>
</div> </div>
</div> </div>

View File

@ -4,60 +4,116 @@ import { of } from 'rxjs';
import { AngularSvgIconModule } from 'angular-svg-icon'; import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { ICONTYPES } from 'app/shared/models/icon-types'; import { ICONTYPES } from 'app/shared/models/icon-types';
import { Links } from 'app/shared/models/links'; import { Links } from 'app/shared/models/links';
import { Filter } from 'app/shared/models/filter';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketDistributionTargetsResource } from 'app/shared/models/workbasket-distribution-targets-resource'; import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-distribution-targets';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service'; import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service'; import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { LinksWorkbasketSummary } from 'app/shared/models/links-workbasket-summary';
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { Side, import { NgxsModule, Store } from '@ngxs/store';
WorkbasketDistributionTargetsComponent } from './workbasket-distribution-targets.component'; import { WorkbasketDistributionTargetsComponent, Side } from './workbasket-distribution-targets.component';
import { WorkbasketDualListComponent } from '../workbasket-dual-list/workbasket-dual-list.component'; import { WorkbasketDualListComponent } from '../workbasket-dual-list/workbasket-dual-list.component';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { ClassificationSelectors } from '../../../shared/store/classification-store/classification.selectors';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
describe('WorkbasketDistributionTargetsComponent', () => { describe('WorkbasketDistributionTargetsComponent', () => {
let component: WorkbasketDistributionTargetsComponent; let component: WorkbasketDistributionTargetsComponent;
let fixture: ComponentFixture<WorkbasketDistributionTargetsComponent>; let fixture: ComponentFixture<WorkbasketDistributionTargetsComponent>;
let workbasketService; let workbasketService;
const workbasket = new Workbasket('1', '', '', '', ICONTYPES.TOPIC, '', '', '', '', '', '', '', '', '', '', '', '', const workbasket = createWorkbasket('1', '', '', '', ICONTYPES.TOPIC, '', '', '', '', '', '', '', '', '', '', '', '',
{ distributionTargets: { href: 'someurl' } }); {});
function createWorkbasket(workbasketId?, created?, key?, domain?, type?, modified?, name?, description?,
owner?, custom1?, custom2?, custom3?, custom4?, orgLevel1?, orgLevel2?, orgLevel3?, orgLevel4?,
_links?: Links, markedForDeletion?: boolean): Workbasket {
return {
workbasketId,
created,
key,
domain,
type,
modified,
name,
description,
owner,
custom1,
custom2,
custom3,
custom4,
orgLevel1,
orgLevel2,
orgLevel3,
orgLevel4,
markedForDeletion,
_links
};
}
function createWorkbasketSummary(workbasketId, key, name, domain, type, description, owner, custom1, custom2, custom3, custom4) {
const workbasketSummary: WorkbasketSummary = {
workbasketId,
key,
name,
domain,
type,
description,
owner,
custom1,
custom2,
custom3,
custom4
};
return workbasketSummary;
}
const workbasketSummaryResource: WorkbasketSummaryRepresentation = {
workbaskets: [
createWorkbasketSummary('1', 'key1', 'NAME1', '', 'PERSONAL',
'description 1', 'owner1', '', '', '', ''),
createWorkbasketSummary('2', 'key2', 'NAME2', '', 'PERSONAL',
'description 2', 'owner2', '', '', '', ''),
],
_links: new LinksWorkbasketSummary({ href: 'url' }),
page: {}
};
const workbasketDistributionTargets: WorkbasketDistributionTargets = {
distributionTargets: [createWorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '')],
_links: {}
};
const storeSpy: jasmine.SpyObj<Store> = jasmine.createSpyObj('Store', ['select', 'dispatch']);
beforeEach(done => { beforeEach(done => {
const configure = testBed => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, InfiniteScrollModule], imports: [AngularSvgIconModule, HttpClientModule, InfiniteScrollModule, NgxsModule.forRoot()],
declarations: [WorkbasketDistributionTargetsComponent, WorkbasketDualListComponent], declarations: [WorkbasketDistributionTargetsComponent, WorkbasketDualListComponent],
providers: [WorkbasketService, NotificationService, SavingWorkbasketService, RequestInProgressService, providers: [WorkbasketService, NotificationService, SavingWorkbasketService, RequestInProgressService,
] { provide: Store, useValue: storeSpy }]
}); });
}; };
configureTests(configure).then(testBed => { configureTests(configure).then(testBed => {
fixture = testBed.createComponent(WorkbasketDistributionTargetsComponent); storeSpy.select.and.callFake(selector => {
switch (selector) {
case WorkbasketSelectors.workbasketDistributionTargets:
return of(['distributionTargets', '_links']);
default:
return of();
}
});
fixture = TestBed.createComponent(WorkbasketDistributionTargetsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.workbasket = workbasket; component.workbasket = workbasket;
workbasketService = testBed.get(WorkbasketService); workbasketService = TestBed.get(WorkbasketService);
spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => of(new WorkbasketSummaryResource( spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => of(workbasketSummaryResource));
new Array<WorkbasketSummary>( spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => of(workbasketDistributionTargets));
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', false, {}),
new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', false, {}),
new WorkbasketSummary('id3', '', '', '', '', '', '', '', '', '', '', '', false, {})
),
{}
)));
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => of(new WorkbasketDistributionTargetsResource(
new Array<WorkbasketSummary>(
new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', false, {})
),
{ self: { href: 'someurl' } }
)));
component.ngOnChanges({ component.ngOnChanges({
active: new SimpleChange(undefined, 'distributionTargets', true) active: new SimpleChange(undefined, 'distributionTargets', true)
}); });
@ -79,67 +135,32 @@ describe('WorkbasketDistributionTargetsComponent', () => {
expect(component.distributionTargetsRight).toBeDefined(); expect(component.distributionTargetsRight).toBeDefined();
}); });
it('should have two list with differents elements onInit', () => { it('should have two list with same elements onInit', () => {
let repeteadElemens = false; let repeteadElemens = false;
expect(component.distributionTargetsLeft.length).toBe(2); expect(component.distributionTargetsLeft.length).toBe(2);
expect(component.distributionTargetsRight.length).toBe(1); expect(component.distributionTargetsRight.length).toBe(2);
component.distributionTargetsLeft.forEach(leftElement => { component.distributionTargetsLeft.forEach(leftElement => {
component.distributionTargetsRight.forEach(rightElement => { component.distributionTargetsRight.forEach(rightElement => {
if (leftElement.workbasketId === rightElement.workbasketId) { if (leftElement.workbasketId === rightElement.workbasketId) { repeteadElemens = true; }
repeteadElemens = true;
}
}); });
}); });
expect(repeteadElemens).toBeFalsy(); expect(repeteadElemens).toBeTruthy();
});
it('should filter left list and keep selected elements as selected', () => {
component.performFilter({
filterBy: new Filter({
name: 'someName', owner: 'someOwner', description: 'someDescription', key: 'someKey'
}),
side: Side.LEFT
});
component.distributionTargetsLeft = new Array<WorkbasketSummary>(
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', false, {})
);
expect(component.distributionTargetsLeft.length).toBe(1);
expect(component.distributionTargetsLeft[0].workbasketId).toBe('id1');
expect(component.distributionTargetsRight.length).toBe(1);
expect(component.distributionTargetsRight[0].workbasketId).toBe('id2');
}); });
it('should reset distribution target and distribution target selected on reset', () => { it('should reset distribution target and distribution target selected on reset', () => {
component.distributionTargetsLeft.push( component.distributionTargetsLeft.push(
new WorkbasketSummary('id4', '', '', '', '', '', '', '', '', '', '', '', false, {}) createWorkbasketSummary('id4', '', '', '', '', '', '', '', '', '', '')
); );
component.distributionTargetsRight.push( component.distributionTargetsRight.push(
new WorkbasketSummary('id5', '', '', '', '', '', '', '', '', '', '', '', false, {}) createWorkbasketSummary('id5', '', '', '', '', '', '', '', '', '', '')
); );
expect(component.distributionTargetsLeft.length).toBe(3); expect(component.distributionTargetsLeft.length).toBe(3);
expect(component.distributionTargetsRight.length).toBe(2); expect(component.distributionTargetsRight.length).toBe(3);
component.onClear(); component.onClear();
fixture.detectChanges(); fixture.detectChanges();
expect(component.distributionTargetsLeft.length).toBe(2); expect(component.distributionTargetsLeft.length).toBe(2);
expect(component.distributionTargetsRight.length).toBe(1); expect(component.distributionTargetsRight.length).toBe(0);
});
it('should save distribution targets selected and update Clone objects.', () => {
expect(component.distributionTargetsSelected.length).toBe(1);
expect(component.distributionTargetsSelectedClone.length).toBe(1);
spyOn(workbasketService, 'updateWorkBasketsDistributionTargets').and.callFake(() => of(new WorkbasketDistributionTargetsResource(
new Array<WorkbasketSummary>(
new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', false, {}),
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', false, {})
),
{}
)));
component.onSave();
fixture.detectChanges();
expect(component.distributionTargetsSelected.length).toBe(2);
expect(component.distributionTargetsSelectedClone.length).toBe(2);
expect(component.distributionTargetsLeft.length).toBe(1);
}); });
}); });

View File

@ -2,15 +2,15 @@ import { Component,
ElementRef, ElementRef,
Input, Input,
OnChanges, OnChanges,
OnDestroy, OnDestroy, OnInit,
SimpleChanges, SimpleChanges,
ViewChild } from '@angular/core'; ViewChild } from '@angular/core';
import { Subscription } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketDistributionTargetsResource } from 'app/shared/models/workbasket-distribution-targets-resource'; import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-distribution-targets';
import { ACTION } from 'app/shared/models/action'; import { ACTION } from 'app/shared/models/action';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
@ -20,8 +20,14 @@ import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { Page } from 'app/shared/models/page'; import { Page } from 'app/shared/models/page';
import { OrientationService } from 'app/shared/services/orientation/orientation.service'; import { OrientationService } from 'app/shared/services/orientation/orientation.service';
import { Orientation } from 'app/shared/models/orientation'; import { Orientation } from 'app/shared/models/orientation';
import { Select, Store } from '@ngxs/store';
import { take, takeUntil } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { GetWorkbasketDistributionTargets,
GetWorkbasketsSummary, UpdateWorkbasketDistributionTargets } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { WorkbasketStateModel } from '../../../shared/store/workbasket-store/workbasket.state';
export enum Side { export enum Side {
LEFT, LEFT,
@ -32,7 +38,7 @@ export enum Side {
templateUrl: './workbasket-distribution-targets.component.html', templateUrl: './workbasket-distribution-targets.component.html',
styleUrls: ['./workbasket-distribution-targets.component.scss'] styleUrls: ['./workbasket-distribution-targets.component.scss']
}) })
export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDestroy { export class WorkbasketDistributionTargetsComponent implements OnInit, OnChanges, OnDestroy {
@Input() @Input()
workbasket: Workbasket; workbasket: Workbasket;
@ -44,15 +50,9 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
badgeMessage = ''; badgeMessage = '';
distributionTargetsSubscription: Subscription; distributionTargetsSelectedResource: WorkbasketDistributionTargets;
workbasketSubscription: Subscription; distributionTargetsLeft: Array<WorkbasketSummary> = [];
workbasketFilterSubscription: Subscription; distributionTargetsRight: Array<WorkbasketSummary> = [];
savingDistributionTargetsSubscription: Subscription;
orientationSubscription: Subscription;
distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource;
distributionTargetsLeft: Array<WorkbasketSummary>;
distributionTargetsRight: Array<WorkbasketSummary>;
distributionTargetsSelected: Array<WorkbasketSummary>; distributionTargetsSelected: Array<WorkbasketSummary>;
distributionTargetsClone: Array<WorkbasketSummary>; distributionTargetsClone: Array<WorkbasketSummary>;
distributionTargetsSelectedClone: Array<WorkbasketSummary>; distributionTargetsSelectedClone: Array<WorkbasketSummary>;
@ -60,7 +60,6 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
requestInProgressLeft = false; requestInProgressLeft = false;
requestInProgressRight = false; requestInProgressRight = false;
loadingItems = false; loadingItems = false;
modalErrorMessage: string;
side = Side; side = Side;
private initialized = false; private initialized = false;
page: Page; page: Page;
@ -69,17 +68,36 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
selectAllRight = false; selectAllRight = false;
@ViewChild('panelBody', { static: false }) @ViewChild('panelBody', { static: false })
private panelBody: ElementRef; panelBody: ElementRef;
@Select(WorkbasketSelectors.workbasketDistributionTargets)
workbasketDistributionTargets$: Observable<WorkbasketDistributionTargets>;
destroy$ = new Subject<void>();
constructor( constructor(
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
private savingWorkbaskets: SavingWorkbasketService, private savingWorkbaskets: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService, private requestInProgressService: RequestInProgressService,
private orientationService: OrientationService, private orientationService: OrientationService,
private notificationsService: NotificationService private notificationsService: NotificationService,
private store: Store
) { } ) { }
ngOnChanges(changes: SimpleChanges): void { ngOnInit() {
this.workbasketDistributionTargets$.subscribe(workbasketDistributionTargets => {
if (typeof workbasketDistributionTargets !== 'undefined') {
this.distributionTargetsSelectedResource = { ...workbasketDistributionTargets };
this.distributionTargetsSelected = this.distributionTargetsSelectedResource.distributionTargets;
this.distributionTargetsSelectedClone = { ...this.distributionTargetsSelected };
TaskanaQueryParameters.page = 1;
this.calculateNumberItemsList();
this.getWorkbaskets();
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') { if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') {
this.init(); this.init();
} }
@ -95,96 +113,15 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
} }
} }
moveDistributionTargets(side: number) { init() {
if (side === Side.LEFT) {
const itemsLeft = this.distributionTargetsLeft.length;
const itemsRight = this.distributionTargetsRight.length;
const itemsSelected = this.getSelectedItems(this.distributionTargetsLeft);
this.distributionTargetsSelected = this.distributionTargetsSelected.concat(itemsSelected);
this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected);
if (((itemsLeft - itemsSelected.length) <= TaskanaQueryParameters.pageSize) && ((itemsLeft + itemsRight) < this.page.totalElements)) {
this.getNextPage(side);
}
} else {
const itemsSelected = this.getSelectedItems(this.distributionTargetsRight);
this.distributionTargetsSelected = this.removeSelectedItems(this.distributionTargetsSelected, itemsSelected);
this.distributionTargetsRight = this.removeSelectedItems(this.distributionTargetsRight, itemsSelected);
this.distributionTargetsLeft = this.distributionTargetsLeft.concat(itemsSelected);
}
this.uncheckSelectAll(side);
}
onSave() {
this.requestInProgressService.setRequestInProgress(true);
this.workbasketService.updateWorkBasketsDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href, this.getSeletedIds()
).subscribe(response => {
this.requestInProgressService.setRequestInProgress(false);
this.distributionTargetsSelected = response.distributionTargets;
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
this.distributionTargetsClone = Object.assign([], this.distributionTargetsLeft);
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', this.workbasket.name]])
);
return true;
},
error => {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
this.requestInProgressService.setRequestInProgress(false);
return false;
});
return false;
}
onClear() {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.distributionTargetsLeft = Object.assign([], this.distributionTargetsClone);
this.distributionTargetsRight = Object.assign([], this.distributionTargetsSelectedClone);
this.distributionTargetsSelected = Object.assign([], this.distributionTargetsSelectedClone);
}
performFilter(dualListFilter: any) {
this.fillDistributionTargets(dualListFilter.side, undefined);
this.onRequest(false, dualListFilter.side);
this.workbasketFilterSubscription = this.workbasketService.getWorkBasketsSummary(true, '', '', '',
dualListFilter.filterBy.filterParams.name, dualListFilter.filterBy.filterParams.description, '',
dualListFilter.filterBy.filterParams.owner, dualListFilter.filterBy.filterParams.type, '',
dualListFilter.filterBy.filterParams.key, '', true)
.subscribe(resultList => {
this.fillDistributionTargets(dualListFilter.side, (resultList.workbaskets));
this.onRequest(true, dualListFilter.side);
});
}
ngOnDestroy(): void {
if (this.distributionTargetsSubscription) { this.distributionTargetsSubscription.unsubscribe(); }
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); }
if (this.workbasketFilterSubscription) { this.workbasketFilterSubscription.unsubscribe(); }
if (this.savingDistributionTargetsSubscription) { this.savingDistributionTargetsSubscription.unsubscribe(); }
if (this.orientationSubscription) { this.orientationSubscription.unsubscribe(); }
}
private init() {
this.onRequest(); this.onRequest();
if (!this.workbasket._links.distributionTargets) { if (!this.workbasket._links.distributionTargets) {
return; return;
} }
this.distributionTargetsSubscription = this.workbasketService.getWorkBasketsDistributionTargets(
this.workbasket._links.distributionTargets.href
).subscribe(
(distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource) => {
this.distributionTargetsSelectedResource = distributionTargetsSelectedResource;
this.distributionTargetsSelected = distributionTargetsSelectedResource.distributionTargets;
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
TaskanaQueryParameters.page = 1;
this.calculateNumberItemsList();
this.getWorkbaskets();
}
);
this.savingDistributionTargetsSubscription = this.savingWorkbaskets.triggeredDistributionTargetsSaving() this.store.dispatch(new GetWorkbasketDistributionTargets(this.workbasket._links.distributionTargets.href));
this.savingWorkbaskets.triggeredDistributionTargetsSaving()
.pipe(takeUntil(this.destroy$))
.subscribe((savingInformation: SavingInformation) => { .subscribe((savingInformation: SavingInformation) => {
if (this.action === ACTION.COPY) { if (this.action === ACTION.COPY) {
this.distributionTargetsSelectedResource._links.self.href = savingInformation.url; this.distributionTargetsSelectedResource._links.self.href = savingInformation.url;
@ -192,47 +129,25 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
} }
}); });
this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => { this.orientationService.getOrientation()
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.calculateNumberItemsList(); this.calculateNumberItemsList();
this.getWorkbaskets(); this.getWorkbaskets();
}); });
} }
private calculateNumberItemsList() { getWorkbaskets(side?: Side) {
if (this.panelBody) {
const cardHeight = 72;
const unusedHeight = 100;
this.cards = this.orientationService.calculateNumberItemsList(
this.panelBody.nativeElement.offsetHeight, cardHeight, unusedHeight, true
) + 1; // TODO: warum +1
}
}
private fillDistributionTargets(side: Side, workbaskets: WorkbasketSummary[]) {
this.distributionTargetsLeft = side === Side.LEFT ? workbaskets : this.distributionTargetsLeft;
this.distributionTargetsRight = side === Side.RIGHT ? workbaskets : this.distributionTargetsRight;
}
private getNextPage(side: Side) {
TaskanaQueryParameters.page += 1;
this.getWorkbaskets(side);
}
private getWorkbaskets(side?: Side) {
if (!this.distributionTargetsLeft) {
this.distributionTargetsLeft = [];
}
if (!this.distributionTargetsRight) {
this.distributionTargetsRight = [];
}
if (this.distributionTargetsSelected && !this.initialized) { if (this.distributionTargetsSelected && !this.initialized) {
this.initialized = true; this.initialized = true;
TaskanaQueryParameters.pageSize = this.cards + this.distributionTargetsSelected.length; TaskanaQueryParameters.pageSize = this.cards + this.distributionTargetsSelected.length;
} }
this.workbasketSubscription = this.workbasketService.getWorkBasketsSummary(true) // TODO: Implement this into NGXS
this.workbasketService.getWorkBasketsSummary(true)
.pipe(takeUntil(this.destroy$))
.subscribe( .subscribe(
(distributionTargetsAvailable: WorkbasketSummaryResource) => { (distributionTargetsAvailable: WorkbasketSummaryRepresentation) => {
if (TaskanaQueryParameters.page === 1) { if (TaskanaQueryParameters.page === 1) {
this.distributionTargetsLeft = []; this.distributionTargetsLeft = [];
this.page = distributionTargetsAvailable.page; this.page = distributionTargetsAvailable.page;
@ -251,17 +166,121 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
); );
} }
private setBadge() { performFilter(dualListFilter: any) {
this.fillDistributionTargets(dualListFilter.side, undefined);
this.onRequest(false, dualListFilter.side);
this.store.dispatch(new GetWorkbasketsSummary(true, '', '', '',
dualListFilter.filterBy.filterParams.name, dualListFilter.filterBy.filterParams.description, '',
dualListFilter.filterBy.filterParams.owner, dualListFilter.filterBy.filterParams.type, '',
dualListFilter.filterBy.filterParams.key, '', true)).subscribe((state: WorkbasketStateModel) => {
this.fillDistributionTargets(dualListFilter.side, state.paginatedWorkbasketsSummary.workbaskets);
this.onRequest(true, dualListFilter.side);
});
}
onSave() {
this.requestInProgressService.setRequestInProgress(true);
this.store.dispatch(new UpdateWorkbasketDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href,
this.getSeletedIds()
)).subscribe(() => {
this.requestInProgressService.setRequestInProgress(false);
return true;
}, error => {
this.requestInProgressService.setRequestInProgress(false);
return false;
});
/* TODO: OLD IMPLEMENTATION, KEPT HERE FOR REFERENCE
this.workbasketService.updateWorkBasketsDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href, this.getSeletedIds()
).subscribe(response => {
this.requestInProgressService.setRequestInProgress(false);
this.distributionTargetsSelected = response.distributionTargets;
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
this.distributionTargetsClone = Object.assign([], this.distributionTargetsLeft);
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', this.workbasket.name]])
);
return true;
},
error => {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
this.requestInProgressService.setRequestInProgress(false);
return false;
});
*/
return false;
}
moveDistributionTargets(side: number) {
if (side === Side.LEFT) {
const itemsLeft = this.distributionTargetsLeft.length;
const itemsRight = this.distributionTargetsRight.length;
const itemsSelected = this.getSelectedItems(this.distributionTargetsLeft);
this.distributionTargetsSelected = [...this.distributionTargetsSelected, ...itemsSelected];
this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected);
if (((itemsLeft - itemsSelected.length) <= TaskanaQueryParameters.pageSize) && ((itemsLeft + itemsRight) < this.page.totalElements)) {
this.getNextPage(side);
}
this.unselectItems(this.distributionTargetsSelected);
} else {
const itemsSelected = this.getSelectedItems(this.distributionTargetsRight);
this.distributionTargetsSelected = this.removeSelectedItems(this.distributionTargetsSelected, itemsSelected);
this.distributionTargetsRight = this.removeSelectedItems(this.distributionTargetsRight, itemsSelected);
this.distributionTargetsLeft = this.distributionTargetsLeft.concat(itemsSelected);
this.unselectItems(itemsSelected);
}
}
onClear() {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.distributionTargetsLeft = Object.assign([], this.distributionTargetsClone);
this.distributionTargetsRight = Object.assign([], this.distributionTargetsSelectedClone);
this.distributionTargetsSelected = Object.assign([], this.distributionTargetsSelectedClone);
}
calculateNumberItemsList() {
if (this.panelBody) {
const cardHeight = 72;
const unusedHeight = 100;
this.cards = this.orientationService.calculateNumberItemsList(
this.panelBody.nativeElement.offsetHeight, cardHeight, unusedHeight, true
) + 1; // TODO: warum +1
}
}
fillDistributionTargets(side: Side, workbaskets: WorkbasketSummary[]) {
this.distributionTargetsLeft = side === Side.LEFT ? workbaskets : this.distributionTargetsLeft;
this.distributionTargetsRight = side === Side.RIGHT ? workbaskets : this.distributionTargetsRight;
}
getNextPage(side: Side) {
TaskanaQueryParameters.page += 1;
this.getWorkbaskets(side);
}
setBadge() {
if (this.action === ACTION.COPY) { if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`; this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
} }
} }
private getSelectedItems(originList: any): Array<any> { getSelectedItems(originList: any): Array<any> {
return originList.filter((item: any) => (item.selected === true)); return originList.filter((item: any) => (item.selected === true));
} }
private removeSelectedItems(originList: any, selectedItemList) { unselectItems(originList: any): Array<any> {
// eslint-disable-next-line no-restricted-syntax
for (const item of originList) {
if (item.selected && item.selected === true) {
item.selected = false;
}
}
return originList;
}
removeSelectedItems(originList: any, selectedItemList) {
for (let index = originList.length - 1; index >= 0; index--) { for (let index = originList.length - 1; index >= 0; index--) {
if (selectedItemList.some(itemToRemove => (originList[index].workbasketId === itemToRemove.workbasketId))) { if (selectedItemList.some(itemToRemove => (originList[index].workbasketId === itemToRemove.workbasketId))) {
originList.splice(index, 1); originList.splice(index, 1);
@ -270,7 +289,7 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
return originList; return originList;
} }
private onRequest(finished: boolean = false, side?: Side) { onRequest(finished: boolean = false, side?: Side) {
this.loadingItems = false; this.loadingItems = false;
const inProgress = !finished; const inProgress = !finished;
switch (side) { switch (side) {
@ -284,7 +303,7 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
} }
} }
private getSeletedIds(): Array<string> { getSeletedIds(): Array<string> {
const distributionTargetsSelelected: Array<string> = []; const distributionTargetsSelelected: Array<string> = [];
this.distributionTargetsSelected.forEach(item => { this.distributionTargetsSelected.forEach(item => {
distributionTargetsSelelected.push(item.workbasketId); distributionTargetsSelelected.push(item.workbasketId);
@ -296,4 +315,9 @@ export class WorkbasketDistributionTargetsComponent implements OnChanges, OnDest
if (side === Side.LEFT && this.selectAllLeft) { this.selectAllLeft = false; } if (side === Side.LEFT && this.selectAllLeft) { this.selectAllLeft = false; }
if (side === Side.RIGHT && this.selectAllRight) { this.selectAllRight = false; } if (side === Side.RIGHT && this.selectAllRight) { this.selectAllRight = false; }
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
} }

View File

@ -1,28 +1,40 @@
<div id="dual-list-Left" class="dual-list list-left col-xs-12 col-md-5-6 container"> <div id="dual-list-Left" class="dual-list list-left col-xs-12 col-md-5-6 container">
<!-- ACTION TOOLBAR -->
<div class="action-toolbar">
<div class="row header"> <div class="row header">
<div class="col-xs-2"> <div class="col-xs-2">
<button (click)="allSelected = !allSelected; selectAll(allSelected);" class="btn btn-default btn-sm no-style" title="Select all"> <button (click)="allSelected = !allSelected; selectAll(allSelected);"
<span class="material-icons md-20 blue ">{{allSelected ? 'check_box': 'check_box_outline_blank'}}</span> class="btn btn-default btn-sm no-style" title="Select all">
<span class="material-icons md-20 blue ">{{allSelected ? 'check_box' : 'check_box_outline_blank'}}</span>
</button> </button>
</div> </div>
<div class="col-xs-7"> <div class="col-xs-7">
<h5>{{header}}</h5> <h5>{{header}}</h5>
</div> </div>
<div class="pull-right"> <div class="pull-right">
<button class="btn btn-default btn-sm" type="button" id="collapsedMenufilterWb" aria-expanded="false" (click)="changeToolbarState(!toolbarState)" <button class="btn btn-default btn-sm" type="button" id="collapsedMenufilterWb" aria-expanded="false"
(click)="changeToolbarState(!toolbarState)"
data-toggle="tooltip" title="Filter"> data-toggle="tooltip" title="Filter">
<span class="material-icons md-20 blue ">{{!toolbarState? 'search' : 'expand_less'}}</span> <span class="material-icons md-20 blue ">{{!toolbarState ? 'search' : 'expand_less'}}</span>
</button> </button>
</div> </div>
</div> </div>
<div [@toggleDown]="toolbarState"> <div [@toggleDown]="toolbarState">
<taskana-shared-filter (performFilter)="performAvailableFilter($event)"></taskana-shared-filter> <taskana-shared-filter (performFilter)="performAvailableFilter($event)"></taskana-shared-filter>
</div> </div>
<taskana-shared-spinner [isRunning]="requestInProgress" positionClass="centered-spinner" class="floating"></taskana-shared-spinner> <taskana-shared-spinner [isRunning]="requestInProgress" positionClass="centered-spinner"
<div infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50" (scrolled)="onScroll()" [scrollWindow]="false" class="infinite-scroll"> class="floating"></taskana-shared-spinner>
</div>
<!-- WORKBASKET LIST -->
<div infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50" (scrolled)="onScroll()"
[scrollWindow]="false" class="infinite-scroll">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item" *ngFor="let distributionTarget of distributionTargets | selectWorkbaskets: distributionTargetsSelected: side" <li class="list-group-item"
[class.selected]="distributionTarget.selected" type="text" (click)="distributionTarget.selected = !distributionTarget.selected"> *ngFor="let distributionTarget of distributionTargets | selectWorkbaskets: distributionTargetsSelected: side"
[class.selected]="distributionTarget.selected" type="text"
(click)="distributionTarget.selected = !distributionTarget.selected">
<div class="row"> <div class="row">
<dl class="col-xs-1"> <dl class="col-xs-1">
<taskana-administration-icon-type [type]="distributionTarget.type"></taskana-administration-icon-type> <taskana-administration-icon-type [type]="distributionTarget.type"></taskana-administration-icon-type>
@ -37,7 +49,8 @@
</div> </div>
</li> </li>
<li class="list-group-item" *ngIf="loadingItems"> <li class="list-group-item" *ngIf="loadingItems">
<taskana-shared-spinner [isRunning]="loadingItems" positionClass="centered-spinner" class="floating"></taskana-shared-spinner> <taskana-shared-spinner [isRunning]="loadingItems" positionClass="centered-spinner"
class="floating"></taskana-shared-spinner>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { Filter } from 'app/shared/models/filter'; import { Filter } from 'app/shared/models/filter';
import { expandDown } from 'theme/animations/expand.animation'; import { expandDown } from 'theme/animations/expand.animation';
@ -11,8 +11,8 @@ import { Side } from '../workbasket-distribution-targets/workbasket-distribution
animations: [expandDown] animations: [expandDown]
}) })
export class WorkbasketDualListComponent implements OnInit { export class WorkbasketDualListComponent implements OnInit {
@Input() distributionTargets: Array<WorkbasketSummary>; @Input() distributionTargets: WorkbasketSummary[];
@Input() distributionTargetsSelected: Array<WorkbasketSummary>; @Input() distributionTargetsSelected: WorkbasketSummary[];
@Output() performDualListFilter = new EventEmitter<{ filterBy: Filter, side: Side }>(); @Output() performDualListFilter = new EventEmitter<{ filterBy: Filter, side: Side }>();
@Input() requestInProgress = false; @Input() requestInProgress = false;
@Input() loadingItems ? = false; @Input() loadingItems ? = false;

View File

@ -5,7 +5,7 @@
<button type="button" (click)="onSubmit()" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary"> <button type="button" (click)="onSubmit()" data-toggle="tooltip" title="Save" class="btn btn-default btn-primary">
<span class="material-icons md-20">save</span> <span class="material-icons md-20">save</span>
</button> </button>
<button type="button" (click)="onClear()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default"> <button type="button" (click)="onUndo()" data-toggle="tooltip" title="Undo Changes" class="btn btn-default">
<span class="material-icons md-20 blue">undo</span> <span class="material-icons md-20 blue">undo</span>
</button> </button>
<button type="button" (click)="removeDistributionTargets()" data-toggle="tooltip" title="Remove workbasket as distribution target" <button type="button" (click)="removeDistributionTargets()" data-toggle="tooltip" title="Remove workbasket as distribution target"
@ -23,9 +23,11 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4> </h4>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<form #WorkbasketForm="ngForm"> <form #WorkbasketForm="ngForm">
<div class="col-md-6"> <div class="col-md-6">
<!-- KEY -->
<div class="form-group required"> <div class="form-group required">
<label for="wb-key" class="control-label">Key</label> <label for="wb-key" class="control-label">Key</label>
<input type="text" required #key="ngModel" class="form-control" id="wb-key" placeholder="Key" <input type="text" required #key="ngModel" class="form-control" id="wb-key" placeholder="Key"
@ -34,6 +36,8 @@
errorMessage="* Key is required"> errorMessage="* Key is required">
</taskana-shared-field-error-display> </taskana-shared-field-error-display>
</div> </div>
<!-- NAME -->
<div class="form-group required"> <div class="form-group required">
<label for="wb-name" class="control-label">Name</label> <label for="wb-name" class="control-label">Name</label>
<input type="text" required #name="ngModel" class="form-control" id="wb-name" placeholder="Name" <input type="text" required #name="ngModel" class="form-control" id="wb-name" placeholder="Name"
@ -42,9 +46,11 @@
errorMessage="* Name is required"> errorMessage="* Name is required">
</taskana-shared-field-error-display> </taskana-shared-field-error-display>
</div> </div>
<!-- OWNER -->
<div class="input-group form-group col-xs-12 required"> <div class="input-group form-group col-xs-12 required">
<label for="wb-owner" class="control-label ">Owner</label> <label for="wb-owner" class="control-label ">Owner</label>
<taskana-shared-type-ahead *ngIf="(workbasketsCustomisation$ | async)?.information?.lookupField else ownerInput" required #owner="ngModel" name="workbasket.owner" <taskana-shared-type-ahead *ngIf="lookupField else ownerInput" required #owner="ngModel" name="workbasket.owner"
[(ngModel)]="workbasket.owner" placeHolderMessage="* Owner is required" [validationValue]="this.toogleValidationMap.get('workbasket.owner')" [(ngModel)]="workbasket.owner" placeHolderMessage="* Owner is required" [validationValue]="this.toogleValidationMap.get('workbasket.owner')"
[displayError]="!isFieldValid('workbasket.owner')" width="100%"></taskana-shared-type-ahead> [displayError]="!isFieldValid('workbasket.owner')" width="100%"></taskana-shared-type-ahead>
<ng-template #ownerInput> <ng-template #ownerInput>
@ -55,11 +61,15 @@
</taskana-shared-field-error-display> </taskana-shared-field-error-display>
</ng-template> </ng-template>
</div> </div>
<!-- DOMAIN -->
<div class="form-group "> <div class="form-group ">
<label for="wb-domain" class="control-label">Domain</label> <label for="wb-domain" class="control-label">Domain</label>
<input type="text" #domain="ngModel" class="form-control" disabled id="wb-domain" placeholder="Domain" <input type="text" #domain="ngModel" class="form-control" disabled id="wb-domain" placeholder="Domain"
[(ngModel)]="workbasket.domain" name="workbasket.domain"> [(ngModel)]="workbasket.domain" name="workbasket.domain">
</div> </div>
<!-- TYPE & DESCRIPTION-->
<div class="row"> <div class="row">
<div class="form-group col-xs-4"> <div class="form-group col-xs-4">
<label class="control-label">Type</label> <label class="control-label">Type</label>
@ -97,7 +107,7 @@
<input type="text" class="form-control" id="wb-org-level-2" placeholder="OrgLevel 2" [(ngModel)]="workbasket.orgLevel2" <input type="text" class="form-control" id="wb-org-level-2" placeholder="OrgLevel 2" [(ngModel)]="workbasket.orgLevel2"
name="workbasket.orgLevel2"> name="workbasket.orgLevel2">
</div> </div>
<div class="form-group"> <div class="form-group" style="padding-top: 18px;">
<label for="wb-org-level-3" class="control-label">OrgLevel 3</label> <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" <input type="text" class="form-control" id="wb-org-level-3" placeholder="OrgLevel 3" [(ngModel)]="workbasket.orgLevel3"
name="workbasket.orgLevel3"> name="workbasket.orgLevel3">

View File

@ -4,20 +4,18 @@ import { FormsModule } from '@angular/forms';
import { AngularSvgIconModule } from 'angular-svg-icon'; import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http'; import { HttpClientModule } from '@angular/common/http';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { of } from 'rxjs';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Routes } from '@angular/router'; import { Routes } from '@angular/router';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { ICONTYPES } from 'app/shared/models/icon-types'; import { ICONTYPES } from 'app/shared/models/icon-types';
import { ACTION } from 'app/shared/models/action'; import { Links } from 'app/shared/models/links';
import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service'; import { SavingWorkbasketService } from 'app/administration/services/saving-workbaskets.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service'; import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { NgxsModule, Store } from '@ngxs/store'; import { NgxsModule } from '@ngxs/store';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { WorkbasketInformationComponent } from './workbasket-information.component'; import { WorkbasketInformationComponent } from './workbasket-information.component';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
@ -43,29 +41,50 @@ describe('WorkbasketInformationComponent', () => {
let requestInProgressService; let requestInProgressService;
let formsValidatorService; let formsValidatorService;
const storeSpy: jasmine.SpyObj<Store> = jasmine.createSpyObj('Store', ['select']);
const configure = (testBed: TestBed) => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
declarations: [WorkbasketInformationComponent, DummyDetailComponent], declarations: [WorkbasketInformationComponent, DummyDetailComponent],
imports: [FormsModule, AngularSvgIconModule, HttpClientModule, RouterTestingModule.withRoutes(routes), NgxsModule.forRoot()], imports: [FormsModule, AngularSvgIconModule, HttpClientModule, RouterTestingModule.withRoutes(routes),
NgxsModule.forRoot()],
providers: [WorkbasketService, NotificationService, SavingWorkbasketService, providers: [WorkbasketService, NotificationService, SavingWorkbasketService,
RequestInProgressService, FormsValidatorService, { provide: Store, useValue: storeSpy }] RequestInProgressService, FormsValidatorService]
}); });
}; };
function createWorkbasket(workbasketId?, created?, key?, domain?, type?, modified?, name?, description?,
owner?, custom1?, custom2?, custom3?, custom4?, orgLevel1?, orgLevel2?, orgLevel3?, orgLevel4?,
_links?: Links, markedForDeletion?: boolean) {
if (!type) {
// eslint-disable-next-line no-param-reassign
type = 'PERSONAL';
}
const workbasket: Workbasket = {
workbasketId,
created,
key,
domain,
type,
modified,
name,
description,
owner,
custom1,
custom2,
custom3,
custom4,
orgLevel1,
orgLevel2,
orgLevel3,
orgLevel4,
markedForDeletion,
_links
};
return workbasket;
}
beforeEach(done => { beforeEach(done => {
configureTests(configure).then(testBed => { configureTests(configure).then(testBed => {
storeSpy.select.and.callFake(selector => {
switch (selector) {
case EngineConfigurationSelectors.workbasketsCustomisation:
return of({ information: {} });
default:
return of();
}
});
fixture = testBed.createComponent(WorkbasketInformationComponent); fixture = testBed.createComponent(WorkbasketInformationComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement; debugElement = fixture.debugElement.nativeElement;
@ -83,6 +102,7 @@ describe('WorkbasketInformationComponent', () => {
}); });
afterEach(() => { afterEach(() => {
fixture.destroy();
document.body.removeChild(debugElement); document.body.removeChild(debugElement);
}); });
@ -90,21 +110,8 @@ describe('WorkbasketInformationComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should create a panel with heading and form with all fields', async(() => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC,
'modified', 'name', 'description', 'owner', 'custom1', 'custom2', 'custom3', 'custom4',
'orgLevel1', 'orgLevel2', 'orgLevel3', 'orgLevel4', null);
fixture.detectChanges();
expect(debugElement.querySelector('#wb-information')).toBeDefined();
expect(debugElement.querySelector('#wb-information > .panel-heading > h4').textContent.trim()).toBe('name');
expect(debugElement.querySelectorAll('#wb-information > .panel-body > form').length).toBe(1);
fixture.whenStable().then(() => {
expect(debugElement.querySelector('#wb-information > .panel-body > form > div > div > input ').value).toBe('keyModified');
});
}));
it('selectType should set workbasket.type to personal with 0 and group in other case', () => { it('selectType should set workbasket.type to personal with 0 and group in other case', () => {
component.workbasket = new Workbasket('id1'); component.workbasket = createWorkbasket('id1');
expect(component.workbasket.type).toEqual('PERSONAL'); expect(component.workbasket.type).toEqual('PERSONAL');
component.selectType(ICONTYPES.GROUP); component.selectType(ICONTYPES.GROUP);
expect(component.workbasket.type).toEqual('GROUP'); expect(component.workbasket.type).toEqual('GROUP');
@ -112,7 +119,7 @@ describe('WorkbasketInformationComponent', () => {
it('should create a copy of workbasket when workbasket is selected', () => { it('should create a copy of workbasket when workbasket is selected', () => {
expect(component.workbasketClone).toBeUndefined(); expect(component.workbasketClone).toBeUndefined();
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description', component.workbasket = createWorkbasket('id', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2', 'orgLevel3', 'orgLevel4'); 'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2', 'orgLevel3', 'orgLevel4');
component.ngOnChanges( component.ngOnChanges(
undefined undefined
@ -120,95 +127,4 @@ describe('WorkbasketInformationComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
expect(component.workbasket.workbasketId).toEqual(component.workbasketClone.workbasketId); expect(component.workbasket.workbasketId).toEqual(component.workbasketClone.workbasketId);
}); });
it('should reset requestInProgress after saving request is done', async(() => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', { self: { href: 'someUrl' } });
fixture.detectChanges();
spyOn(workbasketService, 'updateWorkbasket').and.returnValue(of(component.workbasket));
spyOn(workbasketService, 'triggerWorkBasketSaved').and.returnValue(of(component.workbasket));
component.onSubmit();
expect(component.requestInProgress).toBeFalsy();
}));
it('should trigger triggerWorkBasketSaved method after saving request is done', async(() => {
component.workbasket = new Workbasket('id', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', { self: { href: 'someurl' } });
spyOn(workbasketService, 'updateWorkbasket').and.returnValue(of(component.workbasket));
spyOn(workbasketService, 'triggerWorkBasketSaved').and.returnValue(of(component.workbasket));
fixture.detectChanges();
spyOn(formsValidatorService, 'validateFormAccess').and.returnValue(Promise.resolve(true));
component.onSubmit();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(workbasketService.triggerWorkBasketSaved).toHaveBeenCalled();
});
}));
it('should post a new workbasket when no workbasketId is defined and update workbasket', async(() => {
component.workbasket = new Workbasket(undefined, 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', {});
spyOn(workbasketService, 'createWorkbasket').and.returnValue(of(
new Workbasket('someNewId', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', {})
));
fixture.detectChanges();
spyOn(formsValidatorService, 'validateFormAccess').and.returnValue(Promise.resolve(true));
component.onSubmit();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(alertService.showToast).toHaveBeenCalled();
expect(component.workbasket.workbasketId).toBe('someNewId');
});
}));
it('should post a new workbasket, new distribution targets and new access '
+ 'items when no workbasketId is defined and action is copy', async(() => {
component.workbasket = new Workbasket(undefined, 'created', 'keyModified', 'domain', ICONTYPES.TOPIC,
'modified', 'name', 'description', 'owner', 'custom1', 'custom2',
'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', {});
component.action = ACTION.COPY;
spyOn(workbasketService, 'createWorkbasket').and.returnValue(of(
new Workbasket('someNewId', 'created', 'keyModified', 'domain', ICONTYPES.TOPIC, 'modified', 'name', 'description',
'owner', 'custom1', 'custom2', 'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', { distributionTargets: { href: 'someurl' }, accessItems: { href: 'someurl' } })
));
spyOn(savingWorkbasketService, 'triggerDistributionTargetSaving');
spyOn(savingWorkbasketService, 'triggerAccessItemsSaving');
fixture.detectChanges();
spyOn(formsValidatorService, 'validateFormAccess').and.returnValue(Promise.resolve(true));
component.onSubmit();
fixture.whenStable().then(() => {
fixture.detectChanges();
expect(alertService.showToast).toHaveBeenCalled();
expect(component.workbasket.workbasketId).toBe('someNewId');
expect(savingWorkbasketService.triggerDistributionTargetSaving).toHaveBeenCalled();
expect(savingWorkbasketService.triggerAccessItemsSaving).toHaveBeenCalled();
});
}));
it('should trigger requestInProgress service true before and requestInProgress false after remove a workbasket', () => {
component.workbasket = new Workbasket(undefined, 'created', 'keyModified', 'domain', ICONTYPES.TOPIC,
'modified', 'name', 'description', 'owner', 'custom1', 'custom2',
'custom3', 'custom4', 'orgLevel1', 'orgLevel2',
'orgLevel3', 'orgLevel4', { removeDistributionTargets: { href: 'someurl' } });
spyOn(workbasketService, 'removeDistributionTarget').and.returnValue(of(undefined));
const requestInProgressServiceSpy = spyOn(requestInProgressService, 'setRequestInProgress');
component.removeDistributionTargets();
expect(requestInProgressServiceSpy).toHaveBeenCalledWith(true);
workbasketService.removeDistributionTarget().subscribe(() => {
}, () => { }, () => {
expect(requestInProgressServiceSpy).toHaveBeenCalledWith(false);
});
});
}); });

View File

@ -6,9 +6,9 @@ import { Component,
SimpleChanges, SimpleChanges,
ViewChild } from '@angular/core'; ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subject, Subscription } from 'rxjs';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { Select } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { ICONTYPES } from 'app/shared/models/icon-types'; import { ICONTYPES } from 'app/shared/models/icon-types';
import { ACTION } from 'app/shared/models/action'; import { ACTION } from 'app/shared/models/action';
@ -19,13 +19,16 @@ import { SavingWorkbasketService, SavingInformation } from 'app/administration/s
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.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 { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { map } from 'rxjs/operators'; import { map, takeUntil } from 'rxjs/operators';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { CustomField, import { CustomField,
getCustomFields, getCustomFields,
WorkbasketsCustomisation } from '../../../shared/models/customisation'; WorkbasketsCustomisation } from '../../../shared/models/customisation';
import { CopyWorkbasket, MarkWorkbasketForDeletion,
RemoveDistributionTarget, SaveNewWorkbasket,
UpdateWorkbasket } from '../../../shared/store/workbasket-store/workbasket.actions';
@Component({ @Component({
selector: 'taskana-administration-workbasket-information', selector: 'taskana-administration-workbasket-information',
@ -37,24 +40,24 @@ implements OnInit, OnChanges, OnDestroy {
@Input() @Input()
workbasket: Workbasket; workbasket: Workbasket;
workbasketClone: Workbasket;
workbasketErrors;
@Input() @Input()
action: ACTION; action: ACTION;
@ViewChild('WorkbasketForm', { static: false })
workbasketForm: NgForm;
workbasketClone: Workbasket;
allTypes: Map<string, string>; allTypes: Map<string, string>;
requestInProgress = false; requestInProgress = false;
badgeMessage = ''; badgeMessage = '';
@Select(EngineConfigurationSelectors.workbasketsCustomisation) workbasketsCustomisation$: Observable<WorkbasketsCustomisation>;
customFields$: Observable<CustomField[]>;
toogleValidationMap = new Map<string, boolean>(); toogleValidationMap = new Map<string, boolean>();
lookupField = false;
private workbasketSubscription: Subscription; @Select(EngineConfigurationSelectors.workbasketsCustomisation)
private routeSubscription: Subscription; workbasketsCustomisation$: Observable<WorkbasketsCustomisation>;
@ViewChild('WorkbasketForm', { static: false })
workbasketForm: NgForm; customFields$: Observable<CustomField[]>;
destroy$ = new Subject<void>();
constructor( constructor(
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
@ -63,11 +66,12 @@ implements OnInit, OnChanges, OnDestroy {
private savingWorkbasket: SavingWorkbasketService, private savingWorkbasket: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService, private requestInProgressService: RequestInProgressService,
private formsValidatorService: FormsValidatorService, private formsValidatorService: FormsValidatorService,
private notificationService: NotificationService private notificationService: NotificationService,
private store: Store
) { ) {
} }
ngOnInit(): void { ngOnInit() {
this.allTypes = new Map([ this.allTypes = new Map([
['PERSONAL', 'Personal'], ['PERSONAL', 'Personal'],
['GROUP', 'Group'], ['GROUP', 'Group'],
@ -78,9 +82,16 @@ implements OnInit, OnChanges, OnDestroy {
map(customisation => customisation.information), map(customisation => customisation.information),
getCustomFields(customFieldCount) getCustomFields(customFieldCount)
); );
this.workbasketsCustomisation$
.pipe(takeUntil(this.destroy$))
.subscribe(workbasketsCustomization => {
if (workbasketsCustomization.information.owner) {
this.lookupField = workbasketsCustomization.information.owner.lookupField;
}
});
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges) {
this.workbasketClone = { ...this.workbasket }; this.workbasketClone = { ...this.workbasket };
if (this.action === ACTION.CREATE) { if (this.action === ACTION.CREATE) {
this.badgeMessage = 'Creating new workbasket'; this.badgeMessage = 'Creating new workbasket';
@ -108,7 +119,7 @@ implements OnInit, OnChanges, OnDestroy {
return this.formsValidatorService.isFieldValid(this.workbasketForm, field); return this.formsValidatorService.isFieldValid(this.workbasketForm, field);
} }
onClear() { onUndo() {
this.formsValidatorService.formSubmitAttempt = false; this.formsValidatorService.formSubmitAttempt = false;
this.notificationService.showToast(NOTIFICATION_TYPES.INFO_ALERT); this.notificationService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.workbasket = { ...this.workbasketClone }; this.workbasket = { ...this.workbasketClone };
@ -122,32 +133,11 @@ implements OnInit, OnChanges, OnDestroy {
} }
copyWorkbasket() { copyWorkbasket() {
this.router.navigate([{ outlets: { detail: ['copy-workbasket'] } }], { this.store.dispatch(new CopyWorkbasket(this.workbasket));
relativeTo: this.route.parent
});
} }
removeDistributionTargets() { removeDistributionTargets() {
this.requestInProgressService.setRequestInProgress(true); this.store.dispatch(new RemoveDistributionTarget(this.workbasket._links.removeDistributionTargets.href));
this.workbasketService
.removeDistributionTarget(
this.workbasket._links.removeDistributionTargets.href
)
.subscribe(
reponse => {
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_9,
new Map<string, string>([['workbasketId', this.workbasket.workbasketId]])
);
},
error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.REMOVE_ERR_2,
error,
new Map<String, String>([['workbasketId', this.workbasket.workbasketId]]));
this.requestInProgressService.setRequestInProgress(false);
}
);
} }
private onSave() { private onSave() {
@ -156,24 +146,11 @@ implements OnInit, OnChanges, OnDestroy {
this.postNewWorkbasket(); this.postNewWorkbasket();
return; return;
} }
this.store.dispatch(new UpdateWorkbasket(this.workbasket._links.self.href, this.workbasket))
this.workbasketSubscription = this.workbasketService .subscribe(state => {
.updateWorkbasket(this.workbasket._links.self.href, this.workbasket) this.requestInProgressService.setRequestInProgress(false);
.subscribe(
workbasketUpdated => {
this.afterRequest();
this.workbasket = workbasketUpdated;
this.workbasketClone = { ...this.workbasket }; this.workbasketClone = { ...this.workbasket };
this.notificationService.showToast( });
NOTIFICATION_TYPES.SUCCESS_ALERT_10,
new Map<string, string>([['workbasketKey', workbasketUpdated.key]])
);
},
error => {
this.afterRequest();
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_4, error);
}
);
} }
private beforeRequest() { private beforeRequest() {
@ -187,19 +164,9 @@ implements OnInit, OnChanges, OnDestroy {
private postNewWorkbasket() { private postNewWorkbasket() {
this.addDateToWorkbasket(); this.addDateToWorkbasket();
this.workbasketService.createWorkbasket(this.workbasket).subscribe( this.store.dispatch(new SaveNewWorkbasket(this.workbasket)).subscribe(
(workbasketUpdated: Workbasket) => { () => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_11,
new Map<string, string>([['workbasketKey', workbasketUpdated.key]])
);
this.workbasket = workbasketUpdated;
this.afterRequest(); this.afterRequest();
this.workbasketService.triggerWorkBasketSaved();
this.workbasketService.selectWorkBasket(this.workbasket.workbasketId);
this.router.navigate([`../${this.workbasket.workbasketId}`], {
relativeTo: this.route
});
if (this.action === ACTION.COPY) { if (this.action === ACTION.COPY) {
this.savingWorkbasket.triggerDistributionTargetSaving( this.savingWorkbasket.triggerDistributionTargetSaving(
new SavingInformation( new SavingInformation(
@ -214,10 +181,6 @@ implements OnInit, OnChanges, OnDestroy {
) )
); );
} }
},
error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.CREATE_ERR_2, error);
this.requestInProgressService.setRequestInProgress(false);
} }
); );
} }
@ -229,38 +192,18 @@ implements OnInit, OnChanges, OnDestroy {
} }
private onRemoveConfirmed() { private onRemoveConfirmed() {
this.requestInProgressService.setRequestInProgress(true); this.beforeRequest();
this.workbasketService this.store.dispatch(new MarkWorkbasketForDeletion(this.workbasket._links.self.href)).subscribe(() => {
.markWorkbasketForDeletion(this.workbasket._links.self.href) this.afterRequest();
.subscribe( });
response => {
this.requestInProgressService.setRequestInProgress(false);
this.workbasketService.triggerWorkBasketSaved();
if (response.status === 202) {
this.notificationService.triggerError(NOTIFICATION_TYPES.MARK_ERR,
undefined,
new Map<String, String>([['workbasketId', this.workbasket.workbasketId]]));
} else {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_12,
new Map<string, string>([['workbasketId', this.workbasket.workbasketId]])
);
}
this.router.navigate(['taskana/administration/workbaskets']);
}
);
}
ngOnDestroy() {
if (this.workbasketSubscription) {
this.workbasketSubscription.unsubscribe();
}
if (this.routeSubscription) {
this.routeSubscription.unsubscribe();
}
} }
getWorkbasketCustomProperty(custom: number) { getWorkbasketCustomProperty(custom: number) {
return `custom${custom}`; return `custom${custom}`;
} }
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
} }

View File

@ -9,8 +9,6 @@ import { HttpClientModule } from '@angular/common/http';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { SharedModule } from 'app/shared/shared.module'; import { SharedModule } from 'app/shared/shared.module';
import { AppModule } from 'app/app.module'; import { AppModule } from 'app/app.module';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { Links } from 'app/shared/models/links'; import { Links } from 'app/shared/models/links';
import { Filter } from 'app/shared/models/filter'; import { Filter } from 'app/shared/models/filter';
import { Sorting } from 'app/shared/models/sorting'; import { Sorting } from 'app/shared/models/sorting';
@ -23,6 +21,7 @@ import { WorkbasketDefinitionService } from 'app/administration/services/workbas
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { ImportExportService } from 'app/administration/services/import-export.service'; import { ImportExportService } from 'app/administration/services/import-export.service';
import { WorkbasketListToolbarComponent } from './workbasket-list-toolbar.component'; import { WorkbasketListToolbarComponent } from './workbasket-list-toolbar.component';
import { ICONTYPES } from '../../../shared/models/icon-types';
@Component({ @Component({
selector: 'taskana-dummy-detail', selector: 'taskana-dummy-detail',
@ -58,17 +57,20 @@ describe('WorkbasketListToolbarComponent', () => {
}); });
}; };
configureTests(configure).then(testBed => { configureTests(configure).then(testBed => {
fixture = testBed.createComponent(WorkbasketListToolbarComponent); fixture = TestBed.createComponent(WorkbasketListToolbarComponent);
workbasketService = testBed.get(WorkbasketService); workbasketService = TestBed.get(WorkbasketService);
router = testBed.get(Router); router = TestBed.get(Router);
spyOn(workbasketService, 'markWorkbasketForDeletion').and.returnValue(of('')); spyOn(workbasketService, 'markWorkbasketForDeletion').and.returnValue(of(''));
spyOn(workbasketService, 'triggerWorkBasketSaved'); spyOn(workbasketService, 'triggerWorkBasketSaved');
debugElement = fixture.debugElement.nativeElement; debugElement = fixture.debugElement.nativeElement;
component = fixture.componentInstance; component = fixture.componentInstance;
component.workbaskets = [ component.workbaskets = [{ workbasketId: '1',
new WorkbasketSummary('1', 'key1', 'NAME1', 'description 1', 'owner 1') key: 'key1',
]; name: 'NAME1',
description: 'description 1',
owner: 'owner 1',
type: ICONTYPES.PERSONAL }];
component.workbaskets[0].markedForDeletion = false; component.workbaskets[0].markedForDeletion = false;
fixture.detectChanges(); fixture.detectChanges();
@ -84,12 +86,6 @@ describe('WorkbasketListToolbarComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should navigate to new-workbasket when click on add new workbasket', () => {
const spy = spyOn(router, 'navigate');
component.addWorkbasket();
expect(spy.calls.first().args[0][0].outlets.detail[0]).toBe('new-workbasket');
});
it('should emit performSorting when sorting is triggered', () => { it('should emit performSorting when sorting is triggered', () => {
let sort: Sorting; let sort: Sorting;
const compareSort = new Sorting(); const compareSort = new Sorting();

View File

@ -3,14 +3,18 @@ import { ActivatedRoute, Router } from '@angular/router';
import { Sorting } from 'app/shared/models/sorting'; import { Sorting } from 'app/shared/models/sorting';
import { Filter } from 'app/shared/models/filter'; import { Filter } from 'app/shared/models/filter';
import { Subscription } from 'rxjs';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service'; import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { TaskanaType } from 'app/shared/models/taskana-type'; import { TaskanaType } from 'app/shared/models/taskana-type';
import { expandDown } from 'theme/animations/expand.animation'; import { expandDown } from 'theme/animations/expand.animation';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { Select, Store } from '@ngxs/store';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications'; import { Location } from '@angular/common';
import { Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ACTION } from '../../../shared/models/action';
import { CreateWorkbasket, SetActiveAction } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
@Component({ @Component({
selector: 'taskana-administration-workbasket-list-toolbar', selector: 'taskana-administration-workbasket-list-toolbar',
@ -23,7 +27,7 @@ export class WorkbasketListToolbarComponent implements OnInit {
@Input() workbasketDefaultSortBy: string; @Input() workbasketDefaultSortBy: string;
@Output() performSorting = new EventEmitter<Sorting>(); @Output() performSorting = new EventEmitter<Sorting>();
@Output() performFilter = new EventEmitter<Filter>(); @Output() performFilter = new EventEmitter<Filter>();
workbasketServiceSubscription: Subscription;
selectionToImport = TaskanaType.WORKBASKETS; selectionToImport = TaskanaType.WORKBASKETS;
sortingFields = new Map([['name', 'Name'], ['key', 'Key'], ['description', 'Description'], ['owner', 'Owner'], ['type', 'Type']]); sortingFields = new Map([['name', 'Name'], ['key', 'Key'], ['description', 'Description'], ['owner', 'Owner'], ['type', 'Type']]);
filteringTypes = new Map([['ALL', 'All'], ['PERSONAL', 'Personal'], ['GROUP', 'Group'], filteringTypes = new Map([['ALL', 'All'], ['PERSONAL', 'Personal'], ['GROUP', 'Group'],
@ -33,15 +37,27 @@ export class WorkbasketListToolbarComponent implements OnInit {
toolbarState = false; toolbarState = false;
filterType = TaskanaType.WORKBASKETS; filterType = TaskanaType.WORKBASKETS;
@Select(WorkbasketSelectors.workbasketActiveAction)
workbasketActiveAction$: Observable<ACTION>;
destroy$ = new Subject<void>();
action: ACTION;
constructor( constructor(
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private errors: NotificationService private store: Store,
private location: Location
) { ) {
} }
ngOnInit() { ngOnInit() {
this.workbasketActiveAction$
.pipe(takeUntil(this.destroy$))
.subscribe(action => {
this.action = action;
});
} }
sorting(sort: Sorting) { sorting(sort: Sorting) {
@ -53,7 +69,16 @@ export class WorkbasketListToolbarComponent implements OnInit {
} }
addWorkbasket() { addWorkbasket() {
this.workbasketService.selectWorkBasket(); // this.store.dispatch(new SetActiveAction(ACTION.CREATE));
this.router.navigate([{ outlets: { detail: ['new-workbasket'] } }], { relativeTo: this.route }); if (this.action !== ACTION.CREATE) {
this.store.dispatch(new CreateWorkbasket());
}
// this.workbasketService.selectWorkBasket();
// this.router.navigate([{ outlets: { detail: ['new-workbasket'] } }], { relativeTo: this.route });
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
} }
} }

View File

@ -1,14 +1,16 @@
<div class="footer-space-pagination-list"> <div class="footer-space-pagination-list">
<div #wbToolbar> <div #wbToolbar>
<taskana-administration-workbasket-list-toolbar [workbaskets]="workbaskets" (performFilter)="performFilter($event)" <taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)"
(performSorting)="performSorting($event)" (importSucessful)="refreshWorkbasketList()" [workbasketDefaultSortBy]="workbasketDefaultSortBy"></taskana-administration-workbasket-list-toolbar> (performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy">
</taskana-administration-workbasket-list-toolbar>
</div> </div>
<div *ngIf="(workbaskets && workbaskets.length > 0) else empty_workbaskets"> <div *ngIf="((workbasketsSummary$ | async) && (workbasketsSummary$ | async)?.length > 0) else empty_workbaskets">
<ul #wbList id="wb-list-container" class="list-group"> <ul #wbList id="wb-list-container" class="list-group">
<li class="list-group-item no-space"> <li class="list-group-item no-space">
<div class="row"></div> <div class="row"></div>
</li> </li>
<li class="list-group-item" *ngFor="let workbasket of workbaskets" [class.active]="workbasket.workbasketId == selectedId" <li class="list-group-item" *ngFor="let workbasket of (workbasketsSummary$ | async)"
[class.active]="workbasket.workbasketId == selectedId"
type="text" (click)="selectWorkbasket(workbasket.workbasketId)"> type="text" (click)="selectWorkbasket(workbasket.workbasketId)">
<div class="row"> <div class="row">
<dl class="col-xs-1"> <dl class="col-xs-1">
@ -36,5 +38,9 @@
</div> </div>
</ng-template> </ng-template>
</div> </div>
<taskana-shared-pagination [(page)]="workbasketsResource ? workbasketsResource.page : workbasketsResource" <taskana-shared-pagination
[type]="type" [numberOfItems]="workbaskets.length" (changePage)="changePage($event)"></taskana-shared-pagination> [page]="(workbasketsSummaryRepresentation$ | async) ? (workbasketsSummaryRepresentation$ | async)?.page : (workbasketsSummaryRepresentation$ | async)"
[type]="type"
[numberOfItems]="(workbasketsSummary$ | async)?.length"
(changePage)="changePage($event)">
</taskana-shared-pagination>

View File

@ -8,8 +8,8 @@ import { Routes } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { Filter } from 'app/shared/models/filter'; import { LinksWorkbasketSummary } from 'app/shared/models/links-workbasket-summary';
import { ImportExportComponent } from 'app/administration/components/import-export/import-export.component'; import { ImportExportComponent } from 'app/administration/components/import-export/import-export.component';
@ -20,6 +20,7 @@ import { OrientationService } from 'app/shared/services/orientation/orientation.
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { Page } from 'app/shared/models/page'; import { Page } from 'app/shared/models/page';
import { ImportExportService } from 'app/administration/services/import-export.service'; import { ImportExportService } from 'app/administration/services/import-export.service';
import { NgxsModule } from '@ngxs/store';
import { WorkbasketListToolbarComponent } from '../workbasket-list-toolbar/workbasket-list-toolbar.component'; import { WorkbasketListToolbarComponent } from '../workbasket-list-toolbar/workbasket-list-toolbar.component';
import { WorkbasketListComponent } from './workbasket-list.component'; import { WorkbasketListComponent } from './workbasket-list.component';
@ -31,7 +32,7 @@ class DummyDetailComponent {
} }
@Component({ @Component({
selector: 'taskana-shared-pagination', selector: 'taskana-pagination',
template: 'dummydetail' template: 'dummydetail'
}) })
class PaginationComponent { class PaginationComponent {
@ -44,13 +45,32 @@ class PaginationComponent {
@Output() changePage = new EventEmitter<any>(); @Output() changePage = new EventEmitter<any>();
} }
const workbasketSummaryResource: WorkbasketSummaryResource = new WorkbasketSummaryResource( function createWorkbasketSummary(workbasketId, key, name, domain, type, description, owner, custom1, custom2, custom3, custom4) {
new Array<WorkbasketSummary>( const workbasketSummary: WorkbasketSummary = {
new WorkbasketSummary('1', 'key1', 'NAME1', 'description 1', 'owner 1', '', '', 'PERSONAL', '', '', '', ''), workbasketId,
new WorkbasketSummary('2', 'key2', 'NAME2', 'description 2', 'owner 2', '', '', 'GROUP', '', '', '', '') key,
), name,
{} domain,
); type,
description,
owner,
custom1,
custom2,
custom3,
custom4
};
return workbasketSummary;
}
const workbasketSummaryResource: WorkbasketSummaryRepresentation = {
workbaskets: [
createWorkbasketSummary('1', 'key1', 'NAME1', '', 'PERSONAL',
'description 1', 'owner1', '', '', '', ''),
createWorkbasketSummary('2', 'key2', 'NAME2', '', 'PERSONAL',
'description 2', 'owner2', '', '', '', ''),
],
_links: new LinksWorkbasketSummary({ href: 'url' }),
page: {}
};
describe('WorkbasketListComponent', () => { describe('WorkbasketListComponent', () => {
let component: WorkbasketListComponent; let component: WorkbasketListComponent;
@ -71,12 +91,13 @@ describe('WorkbasketListComponent', () => {
WorkbasketListComponent, WorkbasketListComponent,
DummyDetailComponent, DummyDetailComponent,
WorkbasketListToolbarComponent, WorkbasketListToolbarComponent,
ImportExportComponent ImportExportComponent,
], ],
imports: [ imports: [
AngularSvgIconModule, AngularSvgIconModule,
HttpClientModule, HttpClientModule,
RouterTestingModule.withRoutes(routes) RouterTestingModule.withRoutes(routes),
NgxsModule.forRoot()
], ],
providers: [ providers: [
WorkbasketService, WorkbasketService,
@ -90,6 +111,13 @@ describe('WorkbasketListComponent', () => {
configureTests(configure).then(testBed => { configureTests(configure).then(testBed => {
fixture = TestBed.createComponent(WorkbasketListComponent); fixture = TestBed.createComponent(WorkbasketListComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
Object.defineProperty(component, 'workbasketsSummaryRepresentation$', { writable: true });
const page = {
workbaskets: [],
_links: {},
page: new Page(6, 3, 3, 1)
};
component.workbasketsSummaryRepresentation$ = of(page);
debugElement = fixture.debugElement.nativeElement; debugElement = fixture.debugElement.nativeElement;
workbasketService = TestBed.get(WorkbasketService); workbasketService = TestBed.get(WorkbasketService);
const orientationService = TestBed.get(OrientationService); const orientationService = TestBed.get(OrientationService);
@ -111,14 +139,6 @@ describe('WorkbasketListComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should call workbasketService.getWorkbasketsSummary method on init', () => {
component.ngOnInit();
expect(workbasketService.getWorkBasketsSummary).toHaveBeenCalled();
workbasketService.getWorkBasketsSummary().subscribe(value => {
expect(value).toBe(workbasketSummaryResource);
});
});
it('should have wb-action-toolbar, wb-search-bar, wb-list-container, wb-pagination,' it('should have wb-action-toolbar, wb-search-bar, wb-list-container, wb-pagination,'
+ ' collapsedMenufilterWb and taskana-filter created in the html', () => { + ' collapsedMenufilterWb and taskana-filter created in the html', () => {
expect(debugElement.querySelector('#wb-action-toolbar')).toBeDefined(); expect(debugElement.querySelector('#wb-action-toolbar')).toBeDefined();
@ -127,27 +147,6 @@ describe('WorkbasketListComponent', () => {
expect(debugElement.querySelector('#wb-list-container')).toBeDefined(); expect(debugElement.querySelector('#wb-list-container')).toBeDefined();
expect(debugElement.querySelector('#collapsedMenufilterWb')).toBeDefined(); expect(debugElement.querySelector('#collapsedMenufilterWb')).toBeDefined();
expect(debugElement.querySelector('taskana-filter')).toBeDefined(); expect(debugElement.querySelector('taskana-filter')).toBeDefined();
expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
});
// it('should have two workbasketsummary rows created with the second one selected.', fakeAsync(() => {
// tick(0);
// fixture.detectChanges();
// fixture.whenStable().then(() => {
// expect(debugElement.querySelectorAll('#wb-list-container > li').length).toBe(3);
// expect(debugElement.querySelectorAll('#wb-list-container > li')[1].getAttribute('class'))
// .toBe('list-group-item ng-star-inserted');
// expect(debugElement.querySelectorAll('#wb-list-container > li')[2].getAttribute('class'))
// .toBe('list-group-item ng-star-inserted active');
// })
//
// }));
it('should have two workbasketsummary rows created with two different icons: user and users', () => {
expect(debugElement.querySelectorAll('#wb-list-container > li')[1]
.querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/user.svg');
expect(debugElement.querySelectorAll('#wb-list-container > li')[2]
.querySelector('svg-icon').getAttribute('ng-reflect-src')).toBe('./assets/icons/users.svg');
}); });
it('should have rendered sort by: name, id, description, owner and type', () => { it('should have rendered sort by: name, id, description, owner and type', () => {
@ -157,18 +156,4 @@ describe('WorkbasketListComponent', () => {
expect(debugElement.querySelector('#sort-by-owner')).toBeDefined(); expect(debugElement.querySelector('#sort-by-owner')).toBeDefined();
expect(debugElement.querySelector('#sort-by-type')).toBeDefined(); expect(debugElement.querySelector('#sort-by-type')).toBeDefined();
}); });
it('should have performRequest with forced = true after performFilter is triggered', (() => {
const filter = new Filter({
name: 'someName',
owner: 'someOwner',
description: 'someDescription',
key: 'someKey',
type: 'PERSONAL'
});
component.performFilter(filter);
expect(workbasketSummarySpy.calls.all()[1].args).toEqual([true, 'name', 'asc',
'', 'someName', 'someDescription', '', 'someOwner', 'PERSONAL', '', 'someKey', '']);
}));
}); });

View File

@ -1,8 +1,7 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { Observable, pipe, Subject, Subscription } from 'rxjs';
import { Subscription } from 'rxjs';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary'; import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { Filter } from 'app/shared/models/filter'; import { Filter } from 'app/shared/models/filter';
import { Sorting } from 'app/shared/models/sorting'; import { Sorting } from 'app/shared/models/sorting';
@ -12,6 +11,12 @@ import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.ser
import { OrientationService } from 'app/shared/services/orientation/orientation.service'; import { OrientationService } from 'app/shared/services/orientation/orientation.service';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters'; import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { ImportExportService } from 'app/administration/services/import-export.service'; import { ImportExportService } from 'app/administration/services/import-export.service';
import { Actions, ofActionCompleted, ofActionDispatched, Select, Store } from '@ngxs/store';
import { takeUntil } from 'rxjs/operators';
import { DeselectWorkbasket, GetWorkbasketsSummary,
SelectWorkbasket } from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { Workbasket } from '../../../shared/models/workbasket';
@Component({ @Component({
selector: 'taskana-administration-workbasket-list', selector: 'taskana-administration-workbasket-list',
@ -20,15 +25,11 @@ import { ImportExportService } from 'app/administration/services/import-export.s
}) })
export class WorkbasketListComponent implements OnInit, OnDestroy { export class WorkbasketListComponent implements OnInit, OnDestroy {
selectedId = ''; selectedId = '';
workbasketsResource: WorkbasketSummaryResource;
workbaskets: Array<WorkbasketSummary> = [];
requestInProgress = false; requestInProgress = false;
pageSelected = 1; pageSelected = 1;
pageSize = 9; pageSize = 9;
type = 'workbaskets'; type = 'workbaskets';
cards: number = this.pageSize; cards: number = this.pageSize;
workbasketDefaultSortBy: string = 'name'; workbasketDefaultSortBy: string = 'name';
sort: Sorting = new Sorting(this.workbasketDefaultSortBy); sort: Sorting = new Sorting(this.workbasketDefaultSortBy);
filterBy: Filter = new Filter({ name: '', owner: '', type: '', description: '', key: '' }); filterBy: Filter = new Filter({ name: '', owner: '', type: '', description: '', key: '' });
@ -36,47 +37,75 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
@ViewChild('wbToolbar', { static: true }) @ViewChild('wbToolbar', { static: true })
private toolbarElement: ElementRef; private toolbarElement: ElementRef;
private workBasketSummarySubscription: Subscription; @Select(WorkbasketSelectors.workbasketsSummary)
private workbasketServiceSubscription: Subscription; workbasketsSummary$: Observable<WorkbasketSummary[]>;
private workbasketServiceSavedSubscription: Subscription;
private orientationSubscription: Subscription; @Select(WorkbasketSelectors.workbasketsSummaryRepresentation)
private importingExportingSubscription: Subscription; workbasketsSummaryRepresentation$: Observable<WorkbasketSummaryRepresentation>;
@Select(WorkbasketSelectors.selectedWorkbasket)
selectedWorkbasket$: Observable<Workbasket>;
destroy$ = new Subject<void>();
constructor( constructor(
private store: Store,
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
private router: Router,
private route: ActivatedRoute,
private orientationService: OrientationService, private orientationService: OrientationService,
private importExportService: ImportExportService private importExportService: ImportExportService,
private ngxsActions$: Actions,
) { ) {
this.ngxsActions$.pipe(ofActionDispatched(GetWorkbasketsSummary),
takeUntil(this.destroy$))
.subscribe(() => {
this.requestInProgress = true;
});
this.ngxsActions$.pipe(ofActionCompleted(GetWorkbasketsSummary),
takeUntil(this.destroy$))
.subscribe(() => {
this.requestInProgress = false;
});
} }
ngOnInit() { ngOnInit() {
this.requestInProgress = true; this.requestInProgress = true;
this.workbasketServiceSubscription = this.workbasketService.getSelectedWorkBasket().subscribe(workbasketIdSelected => {
// TODO should be done in a different way. this.selectedWorkbasket$.pipe(takeUntil(this.destroy$))
setTimeout(() => { .subscribe(selectedWorkbasket => {
this.selectedId = workbasketIdSelected; if (typeof selectedWorkbasket !== 'undefined') {
}, 0); this.selectedId = selectedWorkbasket.workbasketId;
} else {
this.selectedId = undefined;
}
}); });
TaskanaQueryParameters.page = this.pageSelected; TaskanaQueryParameters.page = this.pageSelected;
TaskanaQueryParameters.pageSize = this.pageSize; TaskanaQueryParameters.pageSize = this.pageSize;
this.workbasketServiceSavedSubscription = this.workbasketService.workbasketSavedTriggered().subscribe(value => { this.workbasketService.workbasketSavedTriggered()
.pipe(takeUntil(this.destroy$))
.subscribe(value => {
this.performRequest(); this.performRequest();
}); });
this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => {
this.orientationService.getOrientation()
.pipe(takeUntil(this.destroy$))
.subscribe((orientation: Orientation) => {
this.refreshWorkbasketList(); this.refreshWorkbasketList();
}); });
this.importingExportingSubscription = this.importExportService.getImportingFinished().subscribe((value: Boolean) => { this.importExportService.getImportingFinished()
.pipe(takeUntil(this.destroy$))
.subscribe((value: Boolean) => {
this.refreshWorkbasketList(); this.refreshWorkbasketList();
}); });
} }
selectWorkbasket(id: string) { selectWorkbasket(id: string) {
this.selectedId = id; if (this.selectedId === id) {
this.router.navigate([{ outlets: { detail: [this.selectedId] } }], { relativeTo: this.route }); this.store.dispatch(new DeselectWorkbasket());
} else {
this.store.dispatch(new SelectWorkbasket(id));
}
} }
performSorting(sort: Sorting) { performSorting(sort: Sorting) {
@ -102,37 +131,14 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
} }
private performRequest(): void { private performRequest(): void {
TaskanaQueryParameters.pageSize = this.cards; this.store.dispatch(new GetWorkbasketsSummary(true, this.sort.sortBy, this.sort.sortDirection, '',
this.requestInProgress = true;
this.workbaskets = [];
this.workbasketServiceSubscription = this.workbasketService.getWorkBasketsSummary(
true, this.sort.sortBy, this.sort.sortDirection, '',
this.filterBy.filterParams.name, this.filterBy.filterParams.description, '', this.filterBy.filterParams.owner, this.filterBy.filterParams.name, this.filterBy.filterParams.description, '', this.filterBy.filterParams.owner,
this.filterBy.filterParams.type, '', this.filterBy.filterParams.key, '' this.filterBy.filterParams.type, '', this.filterBy.filterParams.key, ''));
) TaskanaQueryParameters.pageSize = this.cards;
.subscribe(resultList => {
this.workbasketsResource = resultList;
this.workbaskets = resultList.workbaskets;
this.requestInProgress = false;
});
} }
ngOnDestroy() { ngOnDestroy() {
if (this.workBasketSummarySubscription) { this.destroy$.next();
this.workBasketSummarySubscription.unsubscribe(); this.destroy$.complete();
}
if (this.workbasketServiceSubscription) {
this.workbasketServiceSubscription.unsubscribe();
}
if (this.workbasketServiceSavedSubscription) {
this.workbasketServiceSavedSubscription.unsubscribe();
}
if (this.orientationSubscription) {
this.orientationSubscription.unsubscribe();
}
if (this.importingExportingSubscription) {
this.importingExportingSubscription.unsubscribe();
}
} }
} }

View File

@ -0,0 +1,17 @@
<div class="no-gutter">
<div class="col-xs-12 col-md-4 vertical-right-divider">
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
</div>
<div class="col-xs-12 col-md-8" *ngIf="showDetail; else showEmptyPage">
<taskana-administration-workbasket-details></taskana-administration-workbasket-details>
</div>
<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

@ -0,0 +1,75 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Routes } from '@angular/router';
import { NgxsModule } from '@ngxs/store';
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { Location } from '@angular/common';
import { take } from 'rxjs/operators';
import { debug } from 'util';
import { WorkbasketOverviewComponent } from './workbasket-overview.component';
@Component({
selector: 'taskana-dummy-detail',
template: 'dummydetail'
})
export class DummyDetailComponent {
}
@Component({
selector: 'taskana-workbasket-list',
template: 'dummylist'
})
export class DummyListComponent {
}
describe('WorkbasketOverviewComponent', () => {
let debugElement: any;
let component: WorkbasketOverviewComponent;
let fixture: ComponentFixture<WorkbasketOverviewComponent>;
const locationSpy: jasmine.SpyObj<Location> = jasmine.createSpyObj('Location', ['go']);
const routes: Routes = [
{ path: ':id', component: DummyDetailComponent }
];
beforeEach(async(() => {
TestBed.configureTestingModule(
{ imports: [
RouterTestingModule.withRoutes(routes),
NgxsModule.forRoot()
],
declarations: [
WorkbasketOverviewComponent,
DummyDetailComponent,
DummyListComponent],
providers: [
{ provide: Location, useValue: locationSpy },
],
schemas: [NO_ERRORS_SCHEMA] }
)
.compileComponents();
}));
afterEach(() => {
fixture.destroy();
});
beforeEach(() => {
fixture = TestBed.createComponent(WorkbasketOverviewComponent);
component = fixture.componentInstance;
debugElement = fixture.debugElement.nativeElement;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should not display detail component if showDetail is false', () => {
component.showDetail = false;
expect(debugElement.querySelector('taskana-administration-workbasket-details')).toBeNull();
});
it('should display detail component if showDetail is true', () => {
component.showDetail = true;
fixture.detectChanges();
expect(debugElement.querySelector('taskana-administration-workbasket-details')).toBeTruthy();
});
});

View File

@ -0,0 +1,57 @@
import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs';
import { ActivatedRoute } from '@angular/router';
import { takeUntil } from 'rxjs/operators';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { CreateWorkbasket,
SelectWorkbasket,
SetActiveAction } from '../../../shared/store/workbasket-store/workbasket.actions';
@Component({
selector: 'app-workbasket-overview',
templateUrl: './workbasket-overview.component.html',
styleUrls: ['./workbasket-overview.component.scss']
})
export class WorkbasketOverviewComponent implements OnInit {
showDetail = false;
@Select(WorkbasketSelectors.selectedWorkbasketAndAction) selectedWorkbasketAndAction$: Observable<any>;
destroy$ = new Subject<void>();
routerParams: any;
constructor(
private route: ActivatedRoute,
private store: Store
) {
}
ngOnInit() {
if (this.route.firstChild) {
this.route.firstChild.params
.pipe(takeUntil(this.destroy$))
.subscribe(params => {
this.routerParams = params;
if (this.routerParams.id) {
this.showDetail = true;
if (this.routerParams.id === 'new-workbasket') {
this.store.dispatch(new CreateWorkbasket());
} else {
this.store.dispatch(new SelectWorkbasket(this.routerParams.id));
}
}
});
}
this.selectedWorkbasketAndAction$
.pipe(takeUntil(this.destroy$))
.subscribe(state => {
this.showDetail = !!state.selectedWorkbasket || state.action === 1;
});
}
ngOnDestroy() {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -17,11 +17,10 @@
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.name" (keyup.enter)="search()" class="form-control input-sm" <input type="text" [(ngModel)]="filter.filterParams.name" (keyup.enter)="search()" class="form-control input-sm"
id="display-name-filter" placeholder="Filter name"> placeholder="Filter name">
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.key" (keyup.enter)="search()" class="form-control input-sm" <input type="text" [(ngModel)]="filter.filterParams.key" (keyup.enter)="search()" class="form-control input-sm" placeholder="Filter key">
id="display-key-filter" placeholder="Filter key">
</div> </div>
<button (click)="clear(); search()" type="button" class="btn btn-default btn-sm pull-right margin-right" <button (click)="clear(); search()" type="button" class="btn btn-default btn-sm pull-right margin-right"
data-toggle="tooltip" title="Clear"> data-toggle="tooltip" title="Clear">
@ -31,13 +30,13 @@
<div class="row"> <div class="row">
<div class="col-xs-8 col-xs-offset-2"> <div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.description" (keyup.enter)="search()" class="form-control input-sm" <input type="text" [(ngModel)]="filter.filterParams.description" (keyup.enter)="search()" class="form-control input-sm"
id="display-name-description" placeholder="Filter description"> placeholder="Filter description">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-8 col-xs-offset-2"> <div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.owner" (keyup.enter)="search()" class="form-control input-sm" <input type="text" [(ngModel)]="filter.filterParams.owner" (keyup.enter)="search()" class="form-control input-sm"
id="display-name-owner" placeholder="Filter owner"> placeholder="Filter owner">
</div> </div>
<button (click)="search()" type="button" class="btn btn-default btn-sm pull-right margin-right" data-toggle="tooltip" <button (click)="search()" type="button" class="btn btn-default btn-sm pull-right margin-right" data-toggle="tooltip"
title="Search"> title="Search">

View File

@ -16,6 +16,6 @@
<span class="footer pull-right" [hidden]="numberOfItems === 0"> <span class="footer pull-right" [hidden]="numberOfItems === 0">
<i [innerHTML]="getPagesTextToShow()"></i> <i [innerHTML]="getPagesTextToShow()"></i>
</span> </span>
<span class="footer pull-right" [hidden]="numberOfItems !== 0 && page.totalElements !== 0"> <span class="footer pull-right" [hidden]="numberOfItems !== 0 && page?.totalElements !== 0">
<i>Loading...</i> <i>Loading...</i>
</span> </span>

View File

@ -46,7 +46,6 @@ export class PaginationComponent implements OnChanges {
if (this.previousPageSelected !== page) { if (this.previousPageSelected !== page) {
this.changePage.emit(page); this.changePage.emit(page);
this.previousPageSelected = page; this.previousPageSelected = page;
this.page.number = page;
this.pageSelected = page; this.pageSelected = page;
} }
} }

View File

@ -19,7 +19,7 @@
{{dataSource.selected?.name}} {{dataSource.selected?.name}}
</label> </label>
</span> </span>
<div class="input-group"> <div class="input-group form-control">
<div>{{dataSource.selected?.accessId}}</div> <div>{{dataSource.selected?.accessId}}</div>
</div> </div>
</div> </div>
@ -33,11 +33,11 @@
<input #inputTypeAhead class="form-control input-text" [ngClass]="{'invalid': !dataSource.length && isRequired}" (blur)="typeaheadOnSelect({'item':dataSource.selected})" <input #inputTypeAhead class="form-control input-text" [ngClass]="{'invalid': !dataSource.length && isRequired}" (blur)="typeaheadOnSelect({'item':dataSource.selected})"
name="accessItem-{{index}}" [required]="isRequired ? 'required' : null" #accessItemName="ngModel" [(ngModel)]="value" name="accessItem-{{index}}" [required]="isRequired ? 'required' : null" #accessItemName="ngModel" [(ngModel)]="value"
[typeahead]="dataSource" typeaheadOptionField="name" [typeaheadItemTemplate]="customItemTemplate" [typeahead]="dataSource" typeaheadOptionField="name" [typeaheadItemTemplate]="customItemTemplate"
(typeaheadOnSelect)="typeaheadOnSelect($event, index)" [typeaheadScrollable]="true" (typeaheadOnSelect)="typeaheadOnSelect($event)" [typeaheadScrollable]="true"
[typeaheadOptionsInScrollableView]="typeaheadOptionsInScrollableView" [typeaheadMinLength]="typeaheadMinLength" [typeaheadOptionsInScrollableView]="typeaheadOptionsInScrollableView" [typeaheadMinLength]="typeaheadMinLength"
[typeaheadWaitMs]="typeaheadWaitMs" (typeaheadLoading)="changeTypeaheadLoading($event)" placeholder="{{displayError? placeHolderMessage: ''}}" [typeaheadWaitMs]="typeaheadWaitMs" (typeaheadLoading)="changeTypeaheadLoading($event)" placeholder="{{displayError? placeHolderMessage: ''}}"
[@validation]="validationValue"> [@validation]="validationValue">
<button *ngIf="!typeaheadLoading" type="button" title="search" class="btn rounded blue"> <button *ngIf="!typeaheadLoading" type="button" title="search" class="btn rounded blue search-button" >
<span class="material-icons md-20 blue">search</span> <span class="material-icons md-20 blue">search</span>
</button> </button>
<div *ngIf="typeaheadLoading" class="loading"> <div *ngIf="typeaheadLoading" class="loading">

View File

@ -5,55 +5,53 @@
height: 47px; height: 47px;
& label { & label {
margin-bottom: 0px; margin-bottom: 0px;
padding: 0px 12px; //font-style: italic;
font-style: italic; //color: grey;
overflow: hidden; overflow: hidden;
} }
&> div{ &> div{
&> div { &> div {
border-bottom: 1px solid $pallete-blue;
margin-top: 2px; margin-top: 2px;
padding-left: 12px; max-width: 175px;
min-width: 175px;
} }
} }
> div{ > div{
border-bottom: 1px solid $pallete-blue !important;
border-radius: 4px !important;
height: 32px;
text-overflow: ellipsis; text-overflow: ellipsis;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;
margin-right: 35px; margin-right: 35px;
padding-left: 12px;
} }
} }
.custom-form-control { .custom-form-control {
margin-top: -5px; margin-top: -5px;
height: 40px; max-height: 57px;
& .input-group{ & .input-group{
width: 100%; width: 100%;
} }
} }
.input-text { .input-text {
background: none; margin-top: 4px;
margin-bottom: 16px;
background: white;
box-shadow: none; box-shadow: none;
border: none; border-radius: 4px !important;
padding: 0px; //border-color: lightgray;
border-radius: 0px; border-bottom: 1px solid $pallete-blue !important;
min-width: 150px; min-width: 150px;
height: 28px; height: 32px;
border-bottom: 1px solid $pallete-blue; padding: 13px 0 13px 12px;
&:focus{
border-bottom: 1px solid $aquamarine;
}
padding-left: 12px;
padding-right: 35px;
} }
.field-label-wrapper{ .field-label-wrapper{
position: relative; position: relative;
height: 28px; height: 28px;
padding-left: 12px;
box-sizing: content-box; box-sizing: content-box;
overflow: hidden; overflow: hidden;
pointer-events: none; pointer-events: none;
@ -63,7 +61,7 @@
} }
.form-control:focus { .form-control:focus {
border-color: none; //border-color: unset;
box-shadow: none; box-shadow: none;
} }
@ -77,7 +75,11 @@
position: absolute; position: absolute;
top: 0px; top: 0px;
right: 0px; right: 0px;
padding-top: 0;
}
.search-button {
z-index: 999;
margin-top: 8px;
} }
::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */ ::placeholder { /* Chrome, Firefox, Opera, Safari 10.1+ */
@ -97,5 +99,5 @@
} }
.invalid { .invalid {
border-bottom: 1px solid $invalid; color: $invalid;
} }

View File

@ -1,5 +1,6 @@
export enum ACTION { export enum ACTION {
DEFAULT, DEFAULT,
CREATE, CREATE,
COPY COPY,
READ
} }

View File

@ -0,0 +1,10 @@
// Remnant from old design, needs to be removed, type reference should instead => models/Links.ts
export class LinksWorkbasketSummary {
constructor(
self?,
distributionTargets?,
accessItems?,
public allWorkbaskets?: { 'href': string }
) {
}
}

View File

@ -229,7 +229,7 @@ export const notifications = new Map<NOTIFICATION_TYPES, Pair>([
// taskdetails.component // taskdetails.component
[NOTIFICATION_TYPES.INFO_ALERT, new Pair( [NOTIFICATION_TYPES.INFO_ALERT, new Pair(
'', '',
'Refreshed selected classification' 'Information restored'
)], )],
// classification-details.component // classification-details.component
[NOTIFICATION_TYPES.SUCCESS_ALERT_4, new Pair( [NOTIFICATION_TYPES.SUCCESS_ALERT_4, new Pair(

View File

@ -0,0 +1,7 @@
import { Links } from './links';
import { WorkbasketAccessItems } from './workbasket-access-items';
export interface WorkbasketAccessItemsRepresentation {
accessItems: WorkbasketAccessItems[];
_links: Links;
}

View File

@ -1,9 +0,0 @@
import { Links } from './links';
import { WorkbasketAccessItems } from './workbasket-access-items';
export class WorkbasketAccessItemsResource {
constructor(
public accessItems: Array<WorkbasketAccessItems> = [],
public _links: Links = {}
) { }
}

View File

@ -1,30 +1,29 @@
import { Links } from './links'; import { Links } from './links';
export class WorkbasketAccessItems { export interface WorkbasketAccessItems {
constructor( accessItemId: string;
public accessItemId: string = '', workbasketId: string;
public workbasketId: string = '', workbasketKey: string;
public accessId: string = '', accessId: string;
public accessName: string = '', accessName: string;
public permRead: boolean = false, permRead: boolean;
public permOpen: boolean = false, permOpen: boolean;
public permAppend: boolean = false, permAppend: boolean;
public permTransfer: boolean = false, permTransfer: boolean;
public permDistribute: boolean = false, permDistribute: boolean;
public permCustom1: boolean = false, permCustom1: boolean;
public permCustom2: boolean = false, permCustom2: boolean;
public permCustom3: boolean = false, permCustom3: boolean;
public permCustom4: boolean = false, permCustom4: boolean;
public permCustom5: boolean = false, permCustom5: boolean;
public permCustom6: boolean = false, permCustom6: boolean;
public permCustom7: boolean = false, permCustom7: boolean;
public permCustom8: boolean = false, permCustom8: boolean;
public permCustom9: boolean = false, permCustom9: boolean;
public permCustom10: boolean = false, permCustom10: boolean;
public permCustom11: boolean = false, permCustom11: boolean;
public permCustom12: boolean = false, permCustom12: boolean;
public _links: Links = {} _links: Links;
) { }
} }
export const customFieldCount: number = 12; export const customFieldCount: number = 12;

View File

@ -1,11 +1,8 @@
import { WorkbasketAccessItems } from './workbasket-access-items'; import { WorkbasketAccessItems } from './workbasket-access-items';
import { Workbasket } from './workbasket'; import { Workbasket } from './workbasket';
export class WorkbasketDefinition { export interface WorkbasketDefinition {
constructor( distributionTargets: string[];
public distributionTargets: string[], workbasketAccessItems: WorkbasketAccessItems[];
public workbasketAccessItems: WorkbasketAccessItems[], workbasket: Workbasket;
public workbasket: Workbasket
) {
}
} }

View File

@ -1,10 +0,0 @@
import { WorkbasketSummary } from './workbasket-summary';
import { Links } from './links';
export class WorkbasketDistributionTargetsResource {
constructor(
public distributionTargets: Array<WorkbasketSummary> = [],
public _links: Links = null
) {
}
}

View File

@ -0,0 +1,7 @@
import { WorkbasketSummary } from './workbasket-summary';
import { Links } from './links';
export interface WorkbasketDistributionTargets {
distributionTargets: WorkbasketSummary[];
_links: Links;
}

View File

@ -0,0 +1,7 @@
import { Links } from './links';
import { Workbasket } from './workbasket';
export interface WorkbasketRepresentation {
workbaskets: Workbasket[];
_links: Links;
}

View File

@ -1,9 +0,0 @@
import { Links } from './links';
import { Workbasket } from './workbasket';
export class WorkbasketResource {
constructor(
public workbaskets: Array<Workbasket> = [],
public _links: Links = {}
) { }
}

View File

@ -0,0 +1,9 @@
import { Page } from 'app/shared/models/page';
import { WorkbasketSummary } from './workbasket-summary';
import { LinksWorkbasketSummary } from './links-workbasket-summary';
export interface WorkbasketSummaryRepresentation {
workbaskets: WorkbasketSummary[];
_links: LinksWorkbasketSummary;
page?: Page;
}

View File

@ -1,12 +0,0 @@
import { Page } from 'app/shared/models/page';
import { WorkbasketSummary } from './workbasket-summary';
import { Links } from './links';
export class WorkbasketSummaryResource {
constructor(
public workbaskets: Array<WorkbasketSummary> = [],
public _links: Links = {},
public page: Page = new Page()
) {
}
}

View File

@ -1,24 +1,20 @@
import { ICONTYPES } from './icon-types'; import { ICONTYPES } from './icon-types';
import { Page } from './page';
import { Links } from './links';
export class WorkbasketSummary { export interface WorkbasketSummary {
constructor( workbasketId?: string,
public workbasketId?: string, key?: string,
public key?: string, name?: string,
public name?: string, domain?: string,
public description?: string, type?: ICONTYPES,
public owner?: string, description?: string,
public modified?: string, owner?: string,
public domain?: string, custom1?: string,
public type: string = ICONTYPES.PERSONAL, custom2?: string,
public orgLevel1?: string, custom3?: string,
public orgLevel2?: string, custom4?: string,
public orgLevel3?: string, orgLevel1?: string,
public orgLevel4?: string, orgLevel2?: string,
public markedForDeletion: boolean = false, orgLevel3?: string,
public _links?: Links, orgLevel4?: string,
public page?: Page markedForDeletion?: boolean,
) {
}
} }

View File

@ -1,28 +1,26 @@
import { Links } from './links'; import { Links } from './links';
import { ICONTYPES } from './icon-types'; import { ICONTYPES } from './icon-types';
export class Workbasket { export interface Workbasket {
constructor( workbasketId?: string;
public workbasketId?: string, key?: string;
public created?: string, name?: string;
public key?: string, domain?: string;
public domain?: string, type?: ICONTYPES;
public type: ICONTYPES = ICONTYPES.PERSONAL, description?: string;
public modified?: string, owner?: string;
public name?: string, custom1?: string;
public description?: string, custom2?: string;
public owner?: string, custom3?: string;
public custom1?: string, custom4?: string;
public custom2?: string, orgLevel1?: string;
public custom3?: string, orgLevel2?: string;
public custom4?: string, orgLevel3?: string;
public orgLevel1?: string, orgLevel4?: string;
public orgLevel2?: string, markedForDeletion?: boolean;
public orgLevel3?: string, created?: string;
public orgLevel4?: string, modified?: string;
public _links: Links = {} _links?: Links;
) {
}
} }
export const customFieldCount: number = 4; export const customFieldCount: number = 4;

View File

@ -4,22 +4,22 @@ import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment'; import { environment } from 'environments/environment';
import { Workbasket } from 'app/shared/models/workbasket'; import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items'; import { WorkbasketAccessItems } from 'app/shared/models/workbasket-access-items';
import { WorkbasketSummaryResource } from 'app/shared/models/workbasket-summary-resource'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketAccessItemsResource } from 'app/shared/models/workbasket-access-items-resource'; import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation';
import { WorkbasketDistributionTargetsResource } from 'app/shared/models/workbasket-distribution-targets-resource'; import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-distribution-targets';
import { Direction } from 'app/shared/models/sorting'; import { Direction } from 'app/shared/models/sorting';
import { DomainService } from 'app/shared/services/domain/domain.service'; import { DomainService } from 'app/shared/services/domain/domain.service';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters'; import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { mergeMap, tap, catchError } from 'rxjs/operators'; import { mergeMap, tap, catchError } from 'rxjs/operators';
import { QueryParameters } from 'app/shared/models/query-parameters'; import { QueryParameters } from 'app/shared/models/query-parameters';
import { WorkbasketResource } from '../../models/workbasket-resource'; import { WorkbasketRepresentation } from '../../models/workbasket-representation';
@Injectable() @Injectable()
export class WorkbasketService { export class WorkbasketService {
public workBasketSelected = new Subject<string>(); public workBasketSelected = new Subject<string>();
public workBasketSaved = new Subject<number>(); public workBasketSaved = new Subject<number>();
private workbasketSummaryRef: Observable<WorkbasketSummaryResource> = new Observable(); private workbasketSummaryRef: Observable<WorkbasketSummaryRepresentation> = new Observable();
constructor( constructor(
private httpClient: HttpClient, private httpClient: HttpClient,
@ -47,7 +47,7 @@ export class WorkbasketService {
return this.domainService.getSelectedDomain() return this.domainService.getSelectedDomain()
.pipe(mergeMap(domain => { .pipe(mergeMap(domain => {
this.workbasketSummaryRef = this.httpClient.get<WorkbasketSummaryResource>( this.workbasketSummaryRef = this.httpClient.get<WorkbasketSummaryRepresentation>(
`${environment.taskanaRestUrl}/v1/workbaskets/${TaskanaQueryParameters `${environment.taskanaRestUrl}/v1/workbaskets/${TaskanaQueryParameters
.getQueryParameters(this.workbasketParameters(sortBy, order, name, nameLike, descLike, owner, ownerLike, .getQueryParameters(this.workbasketParameters(sortBy, order, name, nameLike, descLike, owner, ownerLike,
type, key, keyLike, requiredPermission, allPages, domain))}` type, key, keyLike, requiredPermission, allPages, domain))}`
@ -65,8 +65,8 @@ export class WorkbasketService {
} }
// GET // GET
getAllWorkBaskets(): Observable<WorkbasketResource> { getAllWorkBaskets(): Observable<WorkbasketRepresentation> {
return this.httpClient.get<WorkbasketResource>(`${environment.taskanaRestUrl}/v1/workbaskets?required-permission=OPEN`); return this.httpClient.get<WorkbasketRepresentation>(`${environment.taskanaRestUrl}/v1/workbaskets?required-permission=OPEN`);
} }
// POST // POST
@ -90,8 +90,8 @@ export class WorkbasketService {
} }
// GET // GET
getWorkBasketAccessItems(url: string): Observable<WorkbasketAccessItemsResource> { getWorkBasketAccessItems(url: string): Observable<WorkbasketAccessItemsRepresentation> {
return this.httpClient.get<WorkbasketAccessItemsResource>(url); return this.httpClient.get<WorkbasketAccessItemsRepresentation>(url);
} }
// POST // POST
@ -106,14 +106,14 @@ export class WorkbasketService {
} }
// GET // GET
getWorkBasketsDistributionTargets(url: string): Observable<WorkbasketDistributionTargetsResource> { getWorkBasketsDistributionTargets(url: string): Observable<WorkbasketDistributionTargets> {
return this.httpClient.get<WorkbasketDistributionTargetsResource>(url); return this.httpClient.get<WorkbasketDistributionTargets>(url);
} }
// PUT // PUT
updateWorkBasketsDistributionTargets(url: string, distributionTargetsIds: Array<string>): updateWorkBasketsDistributionTargets(url: string, distributionTargetsIds: Array<string>):
Observable<WorkbasketDistributionTargetsResource> { Observable<WorkbasketDistributionTargets> {
return this.httpClient.put<WorkbasketDistributionTargetsResource>(url, distributionTargetsIds); return this.httpClient.put<WorkbasketDistributionTargets>(url, distributionTargetsIds);
} }
// DELETE // DELETE

View File

@ -1,4 +1,5 @@
import { EngineConfigurationState } from './engine-configuration-store/engine-configuration.state'; import { EngineConfigurationState } from './engine-configuration-store/engine-configuration.state';
import { ClassificationState } from './classification-store/classification.state'; import { ClassificationState } from './classification-store/classification.state';
import { WorkbasketState } from './workbasket-store/workbasket.state';
export const STATES = [EngineConfigurationState, ClassificationState]; export const STATES = [EngineConfigurationState, ClassificationState, WorkbasketState];

View File

@ -0,0 +1,102 @@
import { Workbasket } from '../../models/workbasket';
import { TaskanaQueryParameters } from '../../util/query-parameters';
import { Direction } from '../../models/sorting';
import { ACTION } from '../../models/action';
import { WorkbasketAccessItems } from '../../models/workbasket-access-items';
// Workbasket List
export class GetWorkbasketsSummary {
static readonly type = '[Workbasket List] Get all workbaskets\' summary';
constructor(public forceRequest: boolean = false,
public sortBy: string = TaskanaQueryParameters.parameters.KEY,
public order: string = Direction.ASC,
public name?: string,
public nameLike?: string,
public descLike?: string,
public owner?: string,
public ownerLike?: string,
public type?: string,
public key?: string,
public keyLike?: string,
public requiredPermission?: string,
public allPages: boolean = false) {
}
}
export class SelectWorkbasket {
static readonly type = '[Workbasket] Select a workbasket';
constructor(public workbasketId: string) {
}
}
export class DeselectWorkbasket {
static readonly type = '[Workbasket] Deselect workbasket';
}
export class CreateWorkbasket {
static readonly type = '[Workbasket] Create new workbasket';
}
export class SetActiveAction {
static readonly type = '[Workbasket] Specify current action';
constructor(public action: ACTION) {
}
}
// Workbasket Information
export class SaveNewWorkbasket {
static readonly type = '[Workbasket] Save new workbasket';
constructor(public workbasket: Workbasket) {
}
}
export class CopyWorkbasket {
static readonly type = '[Workbasket] Copy selected workbasket';
constructor(public workbasket: Workbasket) {
}
}
export class UpdateWorkbasket {
static readonly type = '[Workbasket] Update a workbasket';
constructor(public url: string, public workbasket: Workbasket) {
}
}
export class RemoveDistributionTarget {
static readonly type = '[Workbasket] Remove distribution targets of selected workbasket';
constructor(public url: string) {
}
}
export class MarkWorkbasketForDeletion {
static readonly type = '[Workbasket] Mark selected workbasket for deletion';
constructor(public url: string) {
}
}
// Workbasket Access Items
export class GetWorkbasketAccessItems {
static readonly type = '[Workbasket] Get all workbasket access items';
constructor(public url: string) {
}
}
export class UpdateWorkbasketAccessItems {
static readonly type = '[Workbasket] Update selected workbasket\'s access items';
constructor(public url: string, public workbasketAccessItems: WorkbasketAccessItems[]) {
}
}
// Workbasket Distribution Targets
export class GetWorkbasketDistributionTargets {
static readonly type = '[Workbasket] Get all workbasket distribution targets';
constructor(public url: string) {
}
}
export class UpdateWorkbasketDistributionTargets {
static readonly type = '[Workbasket] Update workbasket distribution targets';
constructor(public url: string, public distributionTargetsIds: string[]) {
}
}

View File

@ -0,0 +1,56 @@
import { Selector } from '@ngxs/store';
import { WorkbasketState, WorkbasketStateModel } from './workbasket.state';
import { WorkbasketSummary } from '../../models/workbasket-summary';
import { WorkbasketSummaryRepresentation } from '../../models/workbasket-summary-representation';
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';
export class WorkbasketSelectors {
// Workbasket
@Selector([WorkbasketState])
static selectedWorkbasket(state: WorkbasketStateModel): Workbasket {
return { ...state.selectedWorkbasket };
}
@Selector([WorkbasketState])
static workbasketsSummary(state: WorkbasketStateModel): WorkbasketSummary[] {
return state.paginatedWorkbasketsSummary.workbaskets;
}
@Selector([WorkbasketState])
static workbasketsSummaryRepresentation(state: WorkbasketStateModel): WorkbasketSummaryRepresentation {
return state.paginatedWorkbasketsSummary;
}
@Selector([WorkbasketState])
static workbasketActiveAction(state: WorkbasketStateModel): ACTION {
return state.action;
}
@Selector([WorkbasketState])
static selectedWorkbasketAndAction(state: WorkbasketStateModel): WorkbasketAndAction {
return {
selectedWorkbasket: state.selectedWorkbasket,
action: state.action
};
}
// Workbasket Access Items
@Selector([WorkbasketState])
static workbasketAccessItems(state: WorkbasketStateModel): WorkbasketAccessItemsRepresentation {
return state.workbasketAccessItems;
}
// Workbasket Distribution Targets
@Selector([WorkbasketState])
static workbasketDistributionTargets(state: WorkbasketStateModel): WorkbasketDistributionTargets {
return state.workbasketDistributionTargets;
}
}
export interface WorkbasketAndAction {
selectedWorkbasket: Workbasket,
action: ACTION
}

View File

@ -0,0 +1,261 @@
import { Action, NgxsAfterBootstrap, State, StateContext } from '@ngxs/store';
import { take, tap } from 'rxjs/operators';
import { Observable, of } from 'rxjs';
import { Location } from '@angular/common';
import { WorkbasketService } from '../../services/workbasket/workbasket.service';
import { Workbasket } from '../../models/workbasket';
import { CopyWorkbasket,
CreateWorkbasket,
DeselectWorkbasket,
GetWorkbasketAccessItems,
GetWorkbasketDistributionTargets,
GetWorkbasketsSummary,
MarkWorkbasketForDeletion,
RemoveDistributionTarget,
SaveNewWorkbasket,
SelectWorkbasket,
SetActiveAction,
UpdateWorkbasket, UpdateWorkbasketAccessItems,
UpdateWorkbasketDistributionTargets } from './workbasket.actions';
import { WorkbasketSummaryRepresentation } from '../../models/workbasket-summary-representation';
import { ACTION } from '../../models/action';
import { DomainService } from '../../services/domain/domain.service';
import { NOTIFICATION_TYPES } from '../../models/notifications';
import { NotificationService } from '../../services/notifications/notification.service';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { WorkbasketDistributionTargets } from '../../models/workbasket-distribution-targets';
import { WorkbasketSummary } from '../../models/workbasket-summary';
class InitializeStore {
static readonly type = '[Workbasket] Initializing state';
}
@State<WorkbasketStateModel>({ name: 'workbasket' })
export class WorkbasketState implements NgxsAfterBootstrap {
constructor(
private workbasketService: WorkbasketService,
private domainService: DomainService,
private location: Location,
private notificationService: NotificationService
) {
}
@Action(GetWorkbasketsSummary)
getWorkbasketsSummary(ctx: StateContext<WorkbasketStateModel>, action: GetWorkbasketsSummary): Observable<any> {
return this.workbasketService.getWorkBasketsSummary(action.forceRequest,
action.sortBy, action.order, action.name, action.nameLike, action.descLike, action.owner, action.ownerLike,
action.type, action.key, action.keyLike, action.requiredPermission, action.allPages).pipe(
take(1), tap(paginatedWorkbasketsSummary => {
ctx.patchState(
{ paginatedWorkbasketsSummary }
);
})
);
}
@Action(SelectWorkbasket)
selectWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: SelectWorkbasket): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, `workbaskets/(detail:${action.workbasketId})`));
const id = action.workbasketId;
if (typeof id !== 'undefined') {
return this.workbasketService.getWorkBasket(id).pipe(take(1), tap(
selectedWorkbasket => {
ctx.patchState({
selectedWorkbasket,
action: ACTION.READ
});
}
));
}
return of(null);
}
@Action(DeselectWorkbasket)
deselectWorkbasket(ctx: StateContext<WorkbasketStateModel>): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets'));
ctx.patchState({
selectedWorkbasket: undefined,
action: ACTION.READ
});
return of(null);
}
@Action(CreateWorkbasket)
createWorkbasket(ctx: StateContext<WorkbasketStateModel>): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets/(detail:new-workbasket)'));
ctx.patchState({
selectedWorkbasket: undefined,
action: ACTION.CREATE
});
return of(null);
}
@Action(SetActiveAction)
setActiveAction(ctx: StateContext<WorkbasketStateModel>, action: SetActiveAction): Observable<any> {
ctx.patchState({ action: action.action });
return of(null);
}
@Action(SaveNewWorkbasket)
saveNewWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: SaveNewWorkbasket): Observable<any> {
return this.workbasketService.createWorkbasket(action.workbasket).pipe(take(1), tap(
workbasketUpdated => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_11,
new Map<string, string>([['workbasketKey', workbasketUpdated.key]])
);
this.selectWorkbasket(ctx, workbasketUpdated.workbasketId);
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets'));
}, error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.CREATE_ERR_2, error);
}
));
}
@Action(CopyWorkbasket)
copyWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: CopyWorkbasket): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets/(detail:new-workbasket)'));
ctx.patchState({
action: ACTION.COPY
});
return of(null);
}
@Action(UpdateWorkbasket)
updateWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: UpdateWorkbasket): Observable<any> {
return this.workbasketService.updateWorkbasket(action.url, action.workbasket).pipe(take(1), tap(
updatedWorkbasket => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_10,
new Map<string, string>([['workbasketKey', updatedWorkbasket.key]])
);
const paginatedWorkbasketSummary = { ...ctx.getState().paginatedWorkbasketsSummary };
paginatedWorkbasketSummary.workbaskets = updateWorkbasketSummaryRepresentation(
paginatedWorkbasketSummary.workbaskets, action.workbasket
);
ctx.patchState({
selectedWorkbasket: updatedWorkbasket,
paginatedWorkbasketsSummary: paginatedWorkbasketSummary
});
}, error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_4, error);
}
));
}
@Action(RemoveDistributionTarget)
removeDistributionTarget(ctx: StateContext<WorkbasketStateModel>, action: RemoveDistributionTarget): Observable<any> {
return this.workbasketService.removeDistributionTarget(action.url).pipe(take(1), tap(
() => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_9,
new Map<string, string>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
}, error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.REMOVE_ERR_2,
error,
new Map<String, String>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]]));
}
));
}
@Action(MarkWorkbasketForDeletion)
deleteWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: MarkWorkbasketForDeletion): Observable<any> {
return this.workbasketService.markWorkbasketForDeletion(action.url).pipe(take(1), tap(
response => {
if (response.status === 202) {
this.notificationService.triggerError(NOTIFICATION_TYPES.MARK_ERR,
undefined,
new Map<String, String>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]]));
} else {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_12,
new Map<string, string>([['workbasketId', ctx.getState().selectedWorkbasket.workbasketId]])
);
}
}
));
}
@Action(GetWorkbasketAccessItems)
getWorkbasketAccessItems(ctx: StateContext<WorkbasketStateModel>, action: GetWorkbasketAccessItems): Observable<any> {
return this.workbasketService.getWorkBasketAccessItems(action.url).pipe(take(1), tap(
workbasketAccessItemsRepresentation => {
ctx.patchState({
workbasketAccessItems: workbasketAccessItemsRepresentation
});
}
));
}
@Action(UpdateWorkbasketAccessItems)
updateWorkbasketAccessItems(ctx: StateContext<WorkbasketStateModel>,
action: UpdateWorkbasketAccessItems): Observable<any> {
return this.workbasketService.updateWorkBasketAccessItem(action.url, action.workbasketAccessItems)
.pipe(take(1), tap(workbasketAccessItems => {
ctx.patchState({
workbasketAccessItems
});
this.notificationService.showToast(NOTIFICATION_TYPES.SUCCESS_ALERT_7,
new Map<string, string>([['workbasketKey', ctx.getState().selectedWorkbasket.key]]));
}, error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_2, error);
}));
}
@Action(GetWorkbasketDistributionTargets)
getWorkbasketDistributionTargets(ctx: StateContext<WorkbasketStateModel>, action: GetWorkbasketDistributionTargets): Observable<any> {
return this.workbasketService.getWorkBasketsDistributionTargets(action.url).pipe(take(1), tap(
workbasketDistributionTargets => {
ctx.patchState({
workbasketDistributionTargets
});
}
));
}
@Action(UpdateWorkbasketDistributionTargets)
updateWorkbasketDistributionTargets(
ctx: StateContext<WorkbasketStateModel>,
action: UpdateWorkbasketDistributionTargets
): Observable<any> {
return this.workbasketService.updateWorkBasketsDistributionTargets(action.url, action.distributionTargetsIds).pipe(take(1), tap(
updatedWorkbasketsDistributionTargets => {
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', ctx.getState().selectedWorkbasket.name]])
);
}, error => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
}
));
}
ngxsAfterBootstrap(ctx?: StateContext<any>): void {
ctx.dispatch(new InitializeStore());
}
}
function updateWorkbasketSummaryRepresentation(
workbasketsSummary: WorkbasketSummary[],
selectedWorkbasket: Workbasket
) {
return workbasketsSummary.map(w => {
if (w.workbasketId === selectedWorkbasket.workbasketId) {
const workbasketSummary: WorkbasketSummary = selectedWorkbasket;
return workbasketSummary;
}
return w;
});
}
export interface WorkbasketStateModel {
paginatedWorkbasketsSummary: WorkbasketSummaryRepresentation,
selectedWorkbasket: Workbasket,
action: ACTION,
workbasketAccessItems: WorkbasketAccessItemsRepresentation,
workbasketDistributionTargets: WorkbasketDistributionTargets
}