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;
@ -46,6 +50,11 @@ export class SpinnerComponent {
if (this.isModal) { $(this.modal.nativeElement).modal('toggle'); }
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,6 +17,9 @@ 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>(
@ -26,9 +30,12 @@ const workbasketSummaryResource: WorkbasketSummaryResource = new WorkbasketSumma
@Component({
selector: 'taskana-filter',
template: ''
})
export class FilterComponent {
@Input()
target: string;
}
@ -41,7 +48,7 @@ describe('DistributionTargetsComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule, HttpModule, JsonpModule],
declarations: [DistributionTargetsComponent, SpinnerComponent, GeneralMessageModalComponent, FilterComponent, SelectWorkBasketPipe, IconTypeComponent],
declarations: [DistributionTargetsComponent, SpinnerComponent, GeneralMessageModalComponent, FilterComponent, SelectWorkBasketPipe, IconTypeComponent, DualListComponent],
providers: [WorkbasketService, AlertService]
})
.compileComponents();
@ -54,11 +61,16 @@ describe('DistributionTargetsComponent', () => {
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' })))
{
'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 WorkbasketSummaryResource(
{ 'workbaskets': new Array<WorkbasketSummary>(new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' })))
return Observable.of(new WorkbasketDistributionTargetsResource(
{ 'distributionTargets': new Array<WorkbasketSummary>(new WorkbasketSummary('id2', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' })))
})
fixture.detectChanges();
@ -67,4 +79,66 @@ describe('DistributionTargetsComponent', () => {
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.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.distributionTargetsResource = distributionTargetsAvailable;
this.distributionTargetsLeft = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.requestInProgressLeft = false;
this.requestInProgressRight = false;
this.distributionTargetsClone = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.onRequest(undefined, true);
});
})
}
selectAll(side: number, selected: boolean) {
if (side === 0) {
this.distributionTargetsLeft.forEach((element: any) => {
element.selected = selected;
});
}
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);
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;
}
performSelectedFilter(filterBy: FilterModel) {
this.filterBy = filterBy;
this.performFilter(1);
)
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();

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;
}
}
}