TSK-201 Add Distribution targets saving feature.

This commit is contained in:
Martin Rojas Miguel Angel 2018-03-15 16:50:23 +01:00 committed by Holger Hagen
parent 5de0cd5e09
commit 5778ac75c0
25 changed files with 461 additions and 243 deletions

View File

@ -20,6 +20,7 @@ import { WorkbasketListComponent } from './workbasket/list/workbasket-list.compo
import { WorkbasketDetailsComponent } from './workbasket/details/workbasket-details.component';
import { WorkbasketInformationComponent } from './workbasket/details/information/workbasket-information.component';
import { DistributionTargetsComponent } from './workbasket/details/distribution-targets/distribution-targets.component';
import { DualListComponent } from './workbasket/details/distribution-targets/dual-list/dual-list.component';
import { AccessItemsComponent } from './workbasket/details/access-items/access-items.component';
import { NoAccessComponent } from './workbasket/noAccess/no-access.component';
import { SpinnerComponent } from './shared/spinner/spinner.component';
@ -77,6 +78,7 @@ const DECLARATIONS = [
GeneralMessageModalComponent,
DistributionTargetsComponent,
SortComponent,
DualListComponent,
MapValuesPipe,
RemoveNoneTypePipe,
SelectWorkBasketPipe

View File

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

View File

@ -0,0 +1,18 @@
import {Links} from './links';
export class WorkbasketSummary {
constructor(
public workbasketId: string,
public key: string,
public name: string,
public description: string,
public owner: string,
public modified: string,
public domain: string,
public type: string,
public orgLevel1: string,
public orgLevel2: string,
public orgLevel3: string,
public orgLevel4: string,
public links: Array<Links> = undefined){}
}

View File

@ -4,7 +4,7 @@ import { Pipe, PipeTransform } from '@angular/core';
export class SelectWorkBasketPipe implements PipeTransform {
transform(originArray: any, selectionArray: any, arg1: any): Object[] {
let returnArray = [];
if (!originArray) {
if (!originArray || !selectionArray) {
return returnArray;
}
@ -17,5 +17,4 @@ export class SelectWorkBasketPipe implements PipeTransform {
returnArray = originArray;
return returnArray;
}
}

View File

@ -10,6 +10,7 @@ import { Subject } from 'rxjs/Subject';
import { map } from 'rxjs/operator/map';
import { WorkbasketSummaryResource } from '../model/workbasket-summary-resource';
import { WorkbasketAccessItemsResource } from '../model/workbasket-access-items-resource';
import { WorkbasketDistributionTargetsResource } from '../model/workbasket-distribution-targets-resource';
@Injectable()
export class WorkbasketService {
@ -100,8 +101,13 @@ export class WorkbasketService {
this.httpOptions);
}
// GET
getWorkBasketsDistributionTargets(url: string): Observable<WorkbasketSummaryResource> {
return this.httpClient.get<WorkbasketSummaryResource>(url, this.httpOptions);
getWorkBasketsDistributionTargets(url: string): Observable<WorkbasketDistributionTargetsResource> {
return this.httpClient.get<WorkbasketDistributionTargetsResource>(url, this.httpOptions);
}
// PUT
updateWorkBasketsDistributionTargets(url: string, distributionTargetsIds :Array<string>): Observable<WorkbasketDistributionTargetsResource> {
return this.httpClient.put<WorkbasketDistributionTargetsResource>(url, distributionTargetsIds, this.httpOptions);
}

View File

@ -5,11 +5,12 @@
margin-left: 15px;
}
.list-group-search {
padding: 10px 15px;
}
.btn-users-list {
border: 0px solid transparent;
/* this was 1px earlier */
}
.list-group-search {
padding: 10px 15px;
margin-top: 12px;
border-top: 1px solid #ddd;
}

View File

@ -1 +1 @@
.word-break{word-break: break-all; }
.word-break{word-break: break-word; }

View File

@ -1,4 +1,4 @@
import { Component, OnInit, Input, ViewChild, OnChanges, SimpleChanges } from '@angular/core';
import { Component, OnInit, Input, ViewChild, OnChanges, SimpleChanges, Output, EventEmitter, DoCheck } from '@angular/core';
declare var $: any;
@Component({
@ -8,8 +8,8 @@ declare var $: any;
})
export class GeneralMessageModalComponent implements OnChanges {
@Input()
message: string = '';
@Input() message: string;
@Output() messageChange = new EventEmitter<string>();
@Input()
title: string = '';
@ -29,7 +29,8 @@ export class GeneralMessageModalComponent implements OnChanges {
}
removeMessage() {
this.message = undefined;
this.message = '';
this.messageChange.emit(this.message);
}
}

View File

@ -1,4 +1,4 @@
import { Component, Input, ElementRef } from '@angular/core';
import { Component, Input, ElementRef, Output, EventEmitter } from '@angular/core';
import { ViewChild } from '@angular/core';
declare var $: any;
@ -9,11 +9,13 @@ declare var $: any;
})
export class SpinnerComponent {
private currentTimeout: any;
private requestTimeout: any;
private maxRequestTimeout: number = 10000;
isDelayedRunning: boolean = false;
@Input()
delay: number = 100;
delay: number = 200;
@Input()
set isRunning(value: boolean) {
@ -37,6 +39,8 @@ export class SpinnerComponent {
@Input()
positionClass: string = undefined;
@Output()
requestTimeoutExceeded = new EventEmitter<string>()
@ViewChild('spinnerModal')
private modal;
@ -44,8 +48,13 @@ export class SpinnerComponent {
private runSpinner(value) {
this.currentTimeout = setTimeout(() => {
if (this.isModal) { $(this.modal.nativeElement).modal('toggle'); }
this.isDelayedRunning = value;
this.cancelTimeout();
this.isDelayedRunning = value;
this.cancelTimeout();
this.requestTimeout = setTimeout(() => {
this.requestTimeoutExceeded.emit('There was an error with your request, please make sure you have internet connection');
this.cancelTimeout();
this.isRunning = false;
},this.maxRequestTimeout);
}, this.delay);
}
private closeModal() {
@ -57,7 +66,9 @@ export class SpinnerComponent {
private cancelTimeout(): void {
clearTimeout(this.currentTimeout);
clearTimeout(this.requestTimeout);
this.currentTimeout = undefined;
this.requestTimeout = undefined;
}
ngOnDestroy(): any {

View File

@ -1,5 +1,5 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<taskana-general-message-modal *ngIf="modalErrorMessage" [message]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<taskana-general-message-modal *ngIf="modalErrorMessage" [(message)]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<div *ngIf="workbasket && accessItems" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
@ -58,7 +58,7 @@
<td [ngClass]="{'has-changes': (accessItemsClone[index].permAppend !== accessItem.permAppend)}">
<input type="checkbox" name="accessItem.permAppend-{{index}}" [(ngModel)]="accessItem.permAppend">
</td>
<td ngClass="{{(accessItemsClone[index].permTransfer != accessItem.permTransfer)? 'has-changes': 'pepe'}}">
<td [ngClass]="{'has-changes': (accessItemsClone[index].permTransfer !== accessItem.permTransfer)}">
<input type="checkbox" name="accessItem.permTransfer-{{index}}" [(ngModel)]="accessItem.permTransfer">
</td>
<td [ngClass]="{'has-changes': (accessItemsClone[index].permDistribute !== accessItem.permDistribute)}">

View File

@ -36,7 +36,7 @@ export class AccessItemsComponent implements OnInit {
ngOnInit() {
this.accessItemsubscription = this.workbasketService.getWorkBasketAccessItems(this.workbasket._links.accessItems.href).subscribe( (accessItemsResource: WorkbasketAccessItemsResource) =>{
this.accessItemsResource = accessItemsResource;
this.accessItems = accessItemsResource._embedded.accessItems;
this.accessItems = accessItemsResource._embedded?accessItemsResource._embedded.accessItems: [];
this.accessItemsClone = this.cloneAccessItems(this.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(this.accessItems);
})
@ -61,7 +61,7 @@ export class AccessItemsComponent implements OnInit {
onSave(): boolean {
this.requestInProgress = true;
this.workbasketService.updateWorkBasketAccessItem(this.accessItemsResource._links.self.href + '/', this.accessItems).subscribe(response =>{
this.workbasketService.updateWorkBasketAccessItem(this.accessItemsResource._links.self.href , this.accessItems).subscribe(response =>{
this.accessItemsClone = this.cloneAccessItems(this.accessItems);
this.accessItemsResetClone = this.cloneAccessItems(this.accessItems);
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Workbasket ${this.workbasket.name} Access items were saved successfully`));

View File

@ -1,101 +1,34 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<taskana-general-message-modal *ngIf="modalErrorMessage" [message]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<taskana-spinner [isRunning]="requestInProgress" isModal="true" (requestTimeoutExceeded)="requestTimeoutExceeded($event)"
class="centered-horizontally floating"></taskana-spinner>
<taskana-general-message-modal *ngIf="modalErrorMessage" [(message)]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button type="button" (click)="onSave()" class="btn btn-default btn-primary">Save</button>
<button type="button" (click)="clear()" class="btn btn-default">Undo changes</button>
<button type="button" (click)="onClear()" class="btn btn-default">Undo changes</button>
</div>
<h4 class="panel-header">{{workbasket.name}}</h4>
</div>
<div class="panel-body">
<div id="dual-list-Left" class="dual-list list-left col-xs-12 col-md-5-6 container">
<div class="row">
<div class="col-xs-2">
<button (click)="toggleDtl = !toggleDtl; selectAll(0, toggleDtl);" class="btn btn-default selector" title="select all">
<span aria-hidden="true" class="glyphicon blue {{toggleDtl? 'glyphicon-check': 'glyphicon-unchecked'}}"></span>
</button>
</div>
<div class="col-xs-8">
<h5>Available distribution targets</h5>
</div>
<button class="btn btn-default pull-right collapsed" type="button" id="collapsedMenufilterWbDta" data-toggle="collapse" data-target="#wb-dta-filter-bar"
aria-expanded="false">
<span class="glyphicon glyphicon-filter blue"></span>
</button>
</div>
<taskana-filter target="wb-dta-filter-bar" (performFilter)="performAvailableFilter($event)"></taskana-filter>
<ul class="list-group dual-list-group">
<taskana-spinner [isRunning]="requestInProgressLeft" [isModal]="modalSpinner" positionClass="centered-spinner" class="centered-horizontally floating"></taskana-spinner>
<li class="list-group-item" *ngFor="let distributionTarget of distributionTargetsLeft | selectWorkbaskets: distributionTargetsSelected: 0"
[class.selected]="distributionTarget.selected" type="text" (click)="distributionTarget.selected = !distributionTarget.selected">
<div class="row">
<dl class="col-xs-1">
<dt>
<taskana-icon-type class="vertical-align" [type]="distributionTarget.type"></taskana-icon-type>
</dt>
</dl>
<dl class="col-xs-10">
<dt>{{distributionTarget.name}} ({{distributionTarget.key}}) </dt>
<dd>{{distributionTarget.description}}</dd>
<dd>{{distributionTarget.owner}} &nbsp;</dd>
</dl>
</div>
</li>
</ul>
</div>
<taskana-dual-list id="dual-list-Left" [(distributionTargets)]="distributionTargetsLeft" [distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)" [side]="side.LEFT" [requestInProgress]="requestInProgressLeft" class="dual-list list-left col-xs-12 col-md-5-6 container"></taskana-dual-list>
<div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top">
<button (click)="moveDistributionTargets(0)" [disabled] = "requestInProgressLeft || requestInProgressRight" class="btn btn-default move-right">
<button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight" class="btn btn-default move-right">
<span class="glyphicon glyphicon-chevron-right blue"></span>
</button>
<button (click)="moveDistributionTargets(1)" [disabled] = "requestInProgressLeft || requestInProgressRight" class="btn btn-default move-left">
<button (click)="moveDistributionTargets(side.RIGHT)" [disabled]="requestInProgressLeft || requestInProgressRight" class="btn btn-default move-left">
<span class="glyphicon glyphicon-chevron-left blue"></span>
</button>
</div>
<div class="hidden visible-xs visible-sm col-xs-12 list-arrows text-center">
<button (click)="moveDistributionTargets(0)" [disabled] = "requestInProgressLeft || requestInProgressRight" class="btn btn-default move-down">
<button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight" class="btn btn-default move-down">
<span class="glyphicon glyphicon-chevron-down blue"></span>
</button>
<button (click)="moveDistributionTargets(1)" [disabled] = "requestInProgressLeft || requestInProgressRight" class="btn btn-default move-up">
<button (click)="moveDistributionTargets(side.RIGHT)" [disabled]="requestInProgressLeft || requestInProgressRight" class="btn btn-default move-up">
<span class="glyphicon glyphicon-chevron-up blue"></span>
</button>
</div>
<div id="dual-list-right" class="dual-list list-right col-xs-12 col-md-5-6 container">
<div class="row">
<div class="col-xs-2">
<button (click)="toggleDtr = !toggleDtr; selectAll(1, toggleDtr);" class="btn btn-default selector" title="select all">
<span aria-hidden="true" class="glyphicon blue {{toggleDtr? 'glyphicon-check': 'glyphicon-unchecked'}}"></span>
</button>
</div>
<div class="col-xs-8">
<h5>Selected distribution targets</h5>
</div>
<button class="btn btn-default pull-right collapsed" type="button" id="collapsedMenufilterWbDta" data-toggle="collapse" data-target="#wb-dts-filter-bar"
aria-expanded="false">
<span class="glyphicon glyphicon-filter blue"></span>
</button>
</div>
<taskana-filter target="wb-dts-filter-bar" (performFilter)="performSelectedFilter($event)"></taskana-filter>
<ul class="list-group dual-list-group">
<taskana-spinner [isRunning]="requestInProgressRight" [isModal]="modalSpinner" positionClass="centered-spinner" class="centered-horizontally floating"></taskana-spinner>
<ul class="list-group dual-list-group">
<li class="list-group-item" *ngFor="let distributionTarget of distributionTargetsRight | selectWorkbaskets: distributionTargetsSelected: 1"
[class.selected]="distributionTarget.select" type="text" (click)="distributionTarget.select = !distributionTarget.select">
<div class="row">
<dl class="col-xs-1">
<dt>
<taskana-icon-type class="vertical-align" [type]="distributionTarget.type"></taskana-icon-type>
</dt>
</dl>
<dl class="col-xs-10">
<dt>{{distributionTarget.name}} ({{distributionTarget.key}}) </dt>
<dd>{{distributionTarget.description}}</dd>
<dd>{{distributionTarget.owner}} &nbsp;</dd>
</dl>
</div>
</li>
</ul>
</ul>
</div>
<taskana-dual-list id="dual-list-right" [(distributionTargets)]="distributionTargetsRight" [distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)" [requestInProgress]="requestInProgressRight" [side]="side.RIGHT" class="dual-list list-right col-xs-12 col-md-5-6 container"></taskana-dual-list>
</div>
</div>

View File

@ -1,13 +1,4 @@
.list-group {
margin-top: 8px;
}
.list-left li,
.list-right li {
cursor: pointer;
}
.button-margin-top {
margin-top: 100px
}
@ -16,25 +7,6 @@
margin: 10px 0px;
}
.dual-list {
min-height: 20px;
padding: 5px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
& .row {
padding: 10px 15px;
}
@media screen and (max-width: 991px){
max-height: calc(50vh - 150px);
}
max-height: calc(100vh - 250px);
overflow: hidden;
overflow-y: scroll;
}
.col-md-5-6 {
@media (min-width: 992px){

View File

@ -1,9 +1,10 @@
import { Input } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AngularSvgIconModule } from 'angular-svg-icon';
import { HttpClientModule } from '@angular/common/http';
import { HttpModule, JsonpModule } from '@angular/http';
import { DistributionTargetsComponent } from './distribution-targets.component';
import { DistributionTargetsComponent, Side } from './distribution-targets.component';
import { SpinnerComponent } from '../../../shared/spinner/spinner.component';
import { GeneralMessageModalComponent } from '../../../shared/general-message-modal/general-message-modal.component';
import { IconTypeComponent } from '../../../shared/type-icon/icon-type.component';
@ -16,55 +17,128 @@ import { WorkbasketService } from '../../../services/workbasket.service';
import { AlertService } from '../../../services/alert.service';
import { Observable } from 'rxjs/Observable';
import { Workbasket } from '../../../model/workbasket';
import { WorkbasketDistributionTargetsResource } from '../../../model/workbasket-distribution-targets-resource';
import { FilterModel } from '../../../shared/filter/filter.component';
import { DualListComponent } from './dual-list/dual-list.component';
const workbasketSummaryResource: WorkbasketSummaryResource = new WorkbasketSummaryResource({
'workbaskets': new Array<WorkbasketSummary>(
new WorkbasketSummary("1", "key1", "NAME1", "description 1", "owner 1", "", "", "PERSONAL", "", "", "", ""),
new WorkbasketSummary("2", "key2", "NAME2", "description 2", "owner 2", "", "", "GROUP", "", "", "", ""))
'workbaskets': new Array<WorkbasketSummary>(
new WorkbasketSummary("1", "key1", "NAME1", "description 1", "owner 1", "", "", "PERSONAL", "", "", "", ""),
new WorkbasketSummary("2", "key2", "NAME2", "description 2", "owner 2", "", "", "GROUP", "", "", "", ""))
}, new Links({ 'href': 'url' }));
@Component({
selector: 'taskana-filter',
template: ''
selector: 'taskana-filter',
template: ''
})
export class FilterComponent {
@Input()
target: string;
}
describe('DistributionTargetsComponent', () => {
let component: DistributionTargetsComponent;
let fixture: ComponentFixture<DistributionTargetsComponent>;
let workbasketService;
let workbasket = new Workbasket('1', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }, { 'href': 'someurl' }, { 'href': 'someurl' }));
let component: DistributionTargetsComponent;
let fixture: ComponentFixture<DistributionTargetsComponent>;
let workbasketService;
let workbasket = new Workbasket('1', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }, { 'href': 'someurl' }, { 'href': 'someurl' }));
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, HttpModule, JsonpModule],
declarations: [DistributionTargetsComponent, SpinnerComponent, GeneralMessageModalComponent, FilterComponent, SelectWorkBasketPipe, IconTypeComponent],
providers: [WorkbasketService, AlertService]
})
.compileComponents();
}));
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, HttpModule, JsonpModule],
declarations: [DistributionTargetsComponent, SpinnerComponent, GeneralMessageModalComponent, FilterComponent, SelectWorkBasketPipe, IconTypeComponent, DualListComponent],
providers: [WorkbasketService, AlertService]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DistributionTargetsComponent);
component = fixture.componentInstance;
component.workbasket = workbasket;
workbasketService = TestBed.get(WorkbasketService);
spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => {
return Observable.of(new WorkbasketSummaryResource(
{ 'workbaskets': new Array<WorkbasketSummary>(new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' })))
})
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => {
return Observable.of(new WorkbasketSummaryResource(
{ 'workbaskets': new Array<WorkbasketSummary>(new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' })))
})
beforeEach(() => {
fixture = TestBed.createComponent(DistributionTargetsComponent);
component = fixture.componentInstance;
component.workbasket = workbasket;
workbasketService = TestBed.get(WorkbasketService);
spyOn(workbasketService, 'getWorkBasketsSummary').and.callFake(() => {
return Observable.of(new WorkbasketSummaryResource(
{
'workbaskets': new Array<WorkbasketSummary>(
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })),
new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })),
new WorkbasketSummary('id3', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })))
}, new Links({ 'href': 'someurl' })))
})
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => {
return Observable.of(new WorkbasketDistributionTargetsResource(
{ 'distributionTargets': new Array<WorkbasketSummary>(new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' })))
})
fixture.detectChanges();
});
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should clone distribution target selected on init', () => {
expect(component.distributionTargetsClone).toBeDefined();
});
it('should clone distribution target left and distribution target right lists on init', () => {
expect(component.distributionTargetsLeft).toBeDefined();
expect(component.distributionTargetsRight).toBeDefined();
});
it('should have two list with differents elements onInit', () => {
let repeteadElemens = false;
expect(component.distributionTargetsLeft.length).toBe(2);
expect(component.distributionTargetsRight.length).toBe(1);
component.distributionTargetsLeft.forEach(leftElement => {
component.distributionTargetsRight.forEach(rightElement => {
if (leftElement.workbasketId === rightElement.workbasketId) repeteadElemens = true;
})
})
expect(repeteadElemens).toBeFalsy();
});
it('should filter left list and keep selected elements as selected', () => {
component.performFilter({filterBy:new FilterModel(), side: Side.LEFT});
component.distributionTargetsLeft = new Array<WorkbasketSummary>(
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))
)
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', () => {
component.distributionTargetsLeft.push(new WorkbasketSummary('id4', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })));
component.distributionTargetsRight.push(new WorkbasketSummary('id5', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })));
expect(component.distributionTargetsLeft.length).toBe(3);
expect(component.distributionTargetsRight.length).toBe(2);
component.onClear();
fixture.detectChanges();
expect(component.distributionTargetsLeft.length).toBe(2);
expect(component.distributionTargetsRight.length).toBe(1)
});
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(() => {
return Observable.of(new WorkbasketDistributionTargetsResource(
{
'distributionTargets': new Array<WorkbasketSummary>(
new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })),
new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' })))
}, new Links({ 'href': 'someurl' })))
})
component.onSave();
fixture.detectChanges();
expect(component.distributionTargetsSelected.length).toBe(2);
expect(component.distributionTargetsSelectedClone.length).toBe(2);
expect(component.distributionTargetsLeft.length).toBe(1);
});
});

View File

@ -3,6 +3,7 @@ import { Workbasket } from '../../../model/workbasket';
import { WorkbasketSummary } from '../../../model/workbasket-summary';
import { WorkbasketAccessItems } from '../../../model/workbasket-access-items';
import { FilterModel } from '../../../shared/filter/filter.component'
import { TREE_ACTIONS, KEYS, IActionMapping, ITreeOptions } from 'angular-tree-component';
import { WorkbasketService } from '../../../services/workbasket.service';
import { AlertService, AlertModel, AlertType } from '../../../services/alert.service';
@ -10,7 +11,12 @@ import { AlertService, AlertModel, AlertType } from '../../../services/alert.ser
import { Subscription } from 'rxjs';
import { element } from 'protractor';
import { WorkbasketSummaryResource } from '../../../model/workbasket-summary-resource';
import { WorkbasketDistributionTargetsResource } from '../../../model/workbasket-distribution-targets-resource';
export enum Side {
LEFT,
RIGHT
}
@Component({
selector: 'taskana-workbaskets-distribution-targets',
templateUrl: './distribution-targets.component.html',
@ -24,54 +30,45 @@ export class DistributionTargetsComponent implements OnInit {
distributionTargetsSubscription: Subscription;
workbasketSubscription: Subscription;
workbasketFilterSubscription: Subscription;
distributionTargetsResource: WorkbasketSummaryResource;
distributionTargetsLeft: Array<WorkbasketSummary> = [];
distributionTargetsRight: Array<WorkbasketSummary> = [];
distributionTargetsSelected: Array<WorkbasketSummary> = [];
distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource;
distributionTargetsLeft: Array<WorkbasketSummary>;
distributionTargetsRight: Array<WorkbasketSummary>;
distributionTargetsSelected: Array<WorkbasketSummary>;
distributionTargetsClone: Array<WorkbasketSummary>;
distributionTargetsSelectedClone: Array<WorkbasketSummary>;
filterBy: FilterModel = new FilterModel();
requestInProgress: boolean = false;
requestInProgressLeft: boolean = false;
requestInProgressRight: boolean = false;
modalErrorMessage: string;
side = Side;
constructor(private workbasketService: WorkbasketService) { }
constructor(private workbasketService: WorkbasketService, private alertService: AlertService) { }
ngOnInit() {
this.requestInProgressLeft = true;
this.requestInProgressRight = true;
this.distributionTargetsSubscription = this.workbasketService.getWorkBasketsDistributionTargets(this.workbasket._links.distributionTargets.href).subscribe((distributionTargetsSelectedResource: WorkbasketSummaryResource) => {
this.distributionTargetsSelected = distributionTargetsSelectedResource._embedded ? distributionTargetsSelectedResource._embedded.workbaskets :[];
this.workbasketSubscription = this.workbasketService.getWorkBasketsSummary().subscribe((distributionTargetsAvailable: WorkbasketSummaryResource) => {
this.distributionTargetsResource = distributionTargetsAvailable;
this.distributionTargetsLeft = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.requestInProgressLeft = false;
this.requestInProgressRight = false;
});
})
}
selectAll(side: number, selected: boolean) {
if (side === 0) {
this.distributionTargetsLeft.forEach((element: any) => {
element.selected = selected;
this.onRequest(undefined);
this.distributionTargetsSubscription = this.workbasketService.getWorkBasketsDistributionTargets(this.workbasket._links.distributionTargets.href).subscribe((distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource) => {
this.distributionTargetsSelectedResource = distributionTargetsSelectedResource;
this.distributionTargetsSelected = distributionTargetsSelectedResource._embedded ? distributionTargetsSelectedResource._embedded.distributionTargets : [];
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
this.workbasketSubscription = this.workbasketService.getWorkBasketsSummary().subscribe((distributionTargetsAvailable: WorkbasketSummaryResource) => {
this.distributionTargetsLeft = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsClone = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.onRequest(undefined, true);
});
}
else if (side === 1) {
this.distributionTargetsRight.forEach((element: any) => {
element.selected = selected;
});
}
});
}
moveDistributionTargets(side: number) {
if (side === 0) {
if (side === Side.LEFT) {
let itemsSelected = this.getSelectedItems(this.distributionTargetsLeft, this.distributionTargetsRight)
this.distributionTargetsSelected = this.distributionTargetsSelected.concat(itemsSelected);
this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected);
}
else if (side === 1) {
else {
let itemsSelected = this.getSelectedItems(this.distributionTargetsRight, this.distributionTargetsLeft);
this.distributionTargetsSelected = this.removeSeletedItems(this.distributionTargetsSelected, itemsSelected);
this.distributionTargetsRight = this.removeSeletedItems(this.distributionTargetsRight, itemsSelected);
@ -79,29 +76,51 @@ export class DistributionTargetsComponent implements OnInit {
}
}
performAvailableFilter(filterBy: FilterModel) {
this.filterBy = filterBy;
this.performFilter(0);
}
performSelectedFilter(filterBy: FilterModel) {
this.filterBy = filterBy;
this.performFilter(1);
onSave() {
this.requestInProgress = true;
this.workbasketService.updateWorkBasketsDistributionTargets(this.distributionTargetsSelectedResource._links.self.href, this.getSeletedIds()).subscribe(response => {
this.requestInProgress = false;
this.distributionTargetsSelected = response._embedded ? response._embedded.distributionTargets : [];
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
this.distributionTargetsClone = Object.assign([], this.distributionTargetsLeft);
this.alertService.triggerAlert(new AlertModel(AlertType.SUCCESS, `Workbasket ${this.workbasket.name} Access items were saved successfully`));
return true;
},
error => {
this.modalErrorMessage = error.message;
this.requestInProgress = false;
return false;
}
)
return false;
}
private performFilter(listType: number) {
onClear() {
this.alertService.triggerAlert(new AlertModel(AlertType.INFO, 'Reset edited fields'))
this.distributionTargetsLeft = Object.assign([], this.distributionTargetsClone);
this.distributionTargetsRight = Object.assign([], this.distributionTargetsSelectedClone);
this.distributionTargetsSelected = Object.assign([], this.distributionTargetsSelectedClone);
}
listType ? this.distributionTargetsRight = undefined : this.distributionTargetsLeft = undefined;
listType ? this.requestInProgressRight = true : this.requestInProgressLeft = true;
requestTimeoutExceeded(message: string) {
this.modalErrorMessage = message;
}
performFilter(dualListFilter: any) {
dualListFilter.side === Side.RIGHT ? this.distributionTargetsRight = undefined : this.distributionTargetsLeft = undefined;
this.onRequest(dualListFilter.side, false);
this.workbasketFilterSubscription = this.workbasketService.getWorkBasketsSummary(true, undefined, undefined, undefined,
this.filterBy.name, this.filterBy.description, undefined, this.filterBy.owner,
this.filterBy.type, undefined, this.filterBy.key).subscribe((resultList: WorkbasketSummaryResource) => {
listType ? this.distributionTargetsRight = resultList._embedded.workbaskets : this.distributionTargetsLeft = resultList._embedded.workbaskets;
listType ? this.requestInProgressRight = false : this.requestInProgressLeft = false;
dualListFilter.filterBy.name, dualListFilter.filterBy.description, undefined, dualListFilter.filterBy.owner,
dualListFilter.filterBy.type, undefined, dualListFilter.filterBy.key).subscribe(resultList => {
(dualListFilter.side === Side.RIGHT) ?
this.distributionTargetsRight = (resultList._embedded ? resultList._embedded.workbaskets : []) :
this.distributionTargetsLeft = (resultList._embedded ? resultList._embedded.workbaskets : []);
this.onRequest(dualListFilter.side, true);
});
}
private getSelectedItems(originList: any, destinationList: any): Array<any> {
return originList.filter((element: any) => { return (element.selected === true) });
}
@ -113,7 +132,24 @@ export class DistributionTargetsComponent implements OnInit {
}
}
return originList;
}
private onRequest(side: Side = undefined, finished: boolean = false) {
if (finished) {
side === undefined ? (this.requestInProgressLeft = false, this.requestInProgressRight = false) :
side === Side.LEFT ? this.requestInProgressLeft = false : this.requestInProgressRight = false;
return;
}
side === undefined ? (this.requestInProgressLeft = true, this.requestInProgressRight = true) :
side === Side.LEFT ? this.requestInProgressLeft = true : this.requestInProgressRight = true;
}
private getSeletedIds(): Array<string> {
let distributionTargetsSelelected: Array<string> = [];
this.distributionTargetsSelected.forEach(element => {
distributionTargetsSelelected.push(element.workbasketId);
})
return distributionTargetsSelelected;
}
private ngOnDestroy(): void {

View File

@ -0,0 +1,38 @@
<div id="dual-list-Left" class="dual-list list-left col-xs-12 col-md-5-6 container">
<div class="row">
<div class="col-xs-2">
<button (click)="toggleDtl = !toggleDtl; selectAll(toggleDtl);" class="btn btn-default no-style" title="select all">
<span aria-hidden="true" class="glyphicon blue {{toggleDtl? 'glyphicon-check': 'glyphicon-unchecked'}}"></span>
</button>
</div>
<div class="col-xs-7">
<h5>Available distribution targets</h5>
</div>
<div class="pull-right">
<button class="btn btn-default collapsed" type="button" id="collapsedMenufilterWbDta" data-toggle="collapse"
[attr.data-target]="'#wb-dta-filter-bar-' + sideNumber"
aria-expanded="false">
<span class="glyphicon glyphicon-filter blue"></span>
</button>
</div>
</div>
<taskana-filter target="wb-dta-filter-bar-{{sideNumber}}" (performFilter)="performAvailableFilter($event)"></taskana-filter>
<taskana-spinner [isRunning]="requestInProgress" positionClass="centered-spinner" class="centered-horizontally floating"></taskana-spinner>
<ul class="list-group ">
<li class="list-group-item" *ngFor="let distributionTarget of distributionTargets | selectWorkbaskets: distributionTargetsSelected: side"
[class.selected]="distributionTarget.selected" type="text" (click)="distributionTarget.selected = !distributionTarget.selected">
<div class="row">
<dl class="col-xs-1">
<dt>
<taskana-icon-type class="vertical-align" [type]="distributionTarget.type"></taskana-icon-type>
</dt>
</dl>
<dl class="col-xs-10">
<dt>{{distributionTarget.name}} ({{distributionTarget.key}}) </dt>
<dd>{{distributionTarget.description}}</dd>
<dd>{{distributionTarget.owner}} &nbsp;</dd>
</dl>
</div>
</li>
</ul>
</div>

View File

@ -0,0 +1,60 @@
.dual-list {
min-height: 300px;
padding: 0px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
& .row {
padding: 3px 0px 0px 3px;
}
& .row:first {
border-top: 1px solid #ddd;
}
& div.pull-right {
margin-right: 18px;
}
& >.list-group {
margin-bottom: 0px;
margin-top: 0px;
}
& > .list-group > li {
border-left: none;
border-right: none;
}
overflow-x: hidden;
overflow-y: scroll;
@media screen and (max-width: 991px){
max-height: 38vh;
}
max-height: 78vh;
}
.list-group {
margin-top: 8px;
}
ul>li {
&:first-child.list-group-item.selected {
border-color: #ddd;
}
&.list-group-item.selected {
background-color: #e9f2fc;
}
}
.list-left li {
cursor: pointer;
}
button.no-style{
background: none;
border:none;
}

View File

@ -0,0 +1,40 @@
import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core';
import { WorkbasketSummary } from '../../../../model/workbasket-summary';
import { FilterModel } from '../../../../shared/filter/filter.component';
import { filter } from 'rxjs/operators';
import { Side } from '../distribution-targets.component';
@Component({
selector: 'taskana-dual-list',
templateUrl: './dual-list.component.html',
styleUrls: ['./dual-list.component.scss']
})
export class DualListComponent implements OnInit {
constructor() { }
ngOnInit() {
this.sideNumber = this.side === Side.LEFT ? 0 : 1;
}
@Input() distributionTargets: Array<WorkbasketSummary>;
@Output() distributionTargetsChange = new EventEmitter<Array<WorkbasketSummary>>();
@Input() distributionTargetsSelected: Array<WorkbasketSummary>;
@Output() performDualListFilter = new EventEmitter<{ filterBy: FilterModel, side: Side }>();
@Input() requestInProgress: boolean = false;
@Input() side: Side;
sideNumber: number = 0;
selectAll(selected: boolean) {
this.distributionTargets.forEach((element: any) => {
element.selected = selected;
});
}
performAvailableFilter(filterModel: FilterModel) {
this.performDualListFilter.emit({ filterBy: filterModel, side: this.side });
}
}

View File

@ -1,5 +1,5 @@
<taskana-spinner [isRunning]="requestInProgress" [isModal]="modalSpinner" class="centered-horizontally floating"></taskana-spinner>
<taskana-general-message-modal *ngIf="modalErrorMessage" [message]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<taskana-general-message-modal *ngIf="modalErrorMessage" [(message)]="modalErrorMessage" [title]="modalTitle" error="true"></taskana-general-message-modal>
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">

View File

@ -1,7 +1,7 @@
<div class="container-scrollable" >
<taskana-spinner [isRunning]="requestInProgress" class = "centered-horizontally"></taskana-spinner>
<app-no-access *ngIf="!requestInProgress && (!hasPermission || !workbasket && selectedId)" ></app-no-access>
<div id ="workbasket-details" class="workbasket-details" *ngIf="workbasket && !requestInProgress">
<div id ="workbasket-details" *ngIf="workbasket && !requestInProgress">
<ul class="nav nav-tabs" role="tablist">
<li *ngIf="showDetail" class="visible-xs visible-sm hidden">
<a (click) = "backClicked()"><span class="glyphicon glyphicon-chevron-left" aria-hidden="true"></span>Back</a>
@ -9,14 +9,14 @@
<li role="presentation" class="active">
<a href="#work-baskets" aria-controls="work baskets" role="tab" data-toggle="tab" aria-expanded="true">Information</a>
</li>
<li role="presentation" class="inactive">
<li role="presentation" class="">
<a href="#access-items" aria-controls="Acccess" role="tab" data-toggle="tab" aria-expanded="true">Access</a>
</li>
<li role="presentation" class="inactive">
<li role="presentation" class="">
<a href="#distribution-targets" aria-controls="distribution targets" role="tab" data-toggle="tab" aria-expanded="true">Distribution targets</a>
</li>
</ul>
<div class="tab-content detail-tab-content">
<div class="tab-content">
<div role="tabpanel" class="tab-pane active" id="work-baskets">
<workbasket-information [workbasket]="workbasket"></workbasket-information>
</div>

View File

@ -1,8 +1,9 @@
.nav.nav-tabs {
& > li {
& > a {
min-height: 56px;
padding-top: 17px;
min-height: 52px;
padding-top: 15px;
border-radius: 0px;
&.has-changes{
border-bottom: solid #f0ad4e;
}
@ -12,11 +13,12 @@
border-left: none;
}
}
& > li.active > a {
border-top: 3px solid #479ea9;
padding-top: 13px;
background-color: #f5f5f5;
}
& > p{
margin: 0px;
}
}
.workbasket-details{
margin-top:1px;
}

View File

@ -1,10 +1,11 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { async, ComponentFixture, TestBed, } from '@angular/core/testing';
import { WorkbasketDetailsComponent } from './workbasket-details.component';
import { NoAccessComponent } from '../noAccess/no-access.component';
import { WorkbasketInformationComponent } from './information/workbasket-information.component';
import { AccessItemsComponent } from './access-items/access-items.component';
import { DistributionTargetsComponent } from './distribution-targets/distribution-targets.component';
import { DualListComponent } from './distribution-targets//dual-list/dual-list.component';
import { Workbasket } from 'app/model/workbasket';
import { Observable } from 'rxjs/Observable';
import { SpinnerComponent } from '../../shared/spinner/spinner.component';
@ -37,6 +38,8 @@ import { WorkbasketAccessItemsResource } from '../../model/workbasket-access-ite
})
export class FilterComponent {
@Input()
target: string;
}
@ -52,7 +55,7 @@ describe('WorkbasketDetailsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [RouterTestingModule, FormsModule, AngularSvgIconModule, HttpClientModule, HttpModule],
declarations: [WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, AlertComponent, GeneralMessageModalComponent, AccessItemsComponent, DistributionTargetsComponent, FilterComponent, SelectWorkBasketPipe],
declarations: [WorkbasketDetailsComponent, NoAccessComponent, WorkbasketInformationComponent, SpinnerComponent, IconTypeComponent, MapValuesPipe, RemoveNoneTypePipe, AlertComponent, GeneralMessageModalComponent, AccessItemsComponent, DistributionTargetsComponent, FilterComponent, DualListComponent, SelectWorkBasketPipe],
providers: [WorkbasketService, MasterAndDetailService, PermissionService, AlertService]
})
.compileComponents();
@ -73,8 +76,8 @@ describe('WorkbasketDetailsComponent', () => {
})
spyOn(workbasketService, 'getWorkBasket').and.callFake(() => { return Observable.of(workbasket) })
spyOn(workbasketService, 'getWorkBasketAccessItems').and.callFake(() => { return Observable.of(new WorkbasketAccessItemsResource( {'accessItems': new Array<WorkbasketAccessItems>()}, new Links({'href': 'url'})) )})
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => { return Observable.of(new WorkbasketSummaryResource( {'workbaskets': new Array<WorkbasketSummary>()}, new Links({'href': 'url'})) ) })
spyOn(workbasketService, 'getWorkBasketAccessItems').and.callFake(() => { return Observable.of(new WorkbasketAccessItemsResource({ 'accessItems': new Array<WorkbasketAccessItems>() }, new Links({ 'href': 'url' }))) })
spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => { return Observable.of(new WorkbasketSummaryResource({ 'workbaskets': new Array<WorkbasketSummary>() }, new Links({ 'href': 'url' }))) })
});

View File

@ -17,5 +17,6 @@ a > label{
}
.tab-align{
border-bottom: 1px solid #ddd;
padding-bottom: 12px;
}

View File

@ -146,14 +146,6 @@ li > div.row > dl {
color: red;
}
.detail-tab-content {
margin-top: 20px;
}
.col-xs-9.mod-col-9 {
width: 74%;
padding-right: 0px;
}
.user-select {
margin-left: 2px;
@ -225,5 +217,27 @@ li > div.row > dl {
}
.centered-spinner {
margin-top: 100px;
margin-top: 30px;
margin-bottom: 30px;
}
.list-group-item {
padding: 5px 15px;
}
.dual-list > taskana-filter >.list-group-search {
margin-top: 4px;
}
workbasket-information, taskana-workbasket-access-items, taskana-workbaskets-distribution-targets {
&> .panel{
border: none;
box-shadow: none;
margin-bottom: 0px;
&> .panel-body {
height: 84vh;
max-height: 84vh;
overflow-y: scroll;
}
}
}