diff --git a/admin/src/app/app.module.ts b/admin/src/app/app.module.ts index da96bfb60..97bcb1b39 100644 --- a/admin/src/app/app.module.ts +++ b/admin/src/app/app.module.ts @@ -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 diff --git a/admin/src/app/model/workbasket-distribution-targets-resource.ts b/admin/src/app/model/workbasket-distribution-targets-resource.ts new file mode 100644 index 000000000..14811ecb9 --- /dev/null +++ b/admin/src/app/model/workbasket-distribution-targets-resource.ts @@ -0,0 +1,7 @@ +import { WorkbasketSummary } from './workbasket-summary'; +import { Links } from './links'; + +export class WorkbasketDistributionTargetsResource { + constructor(public _embedded: {'distributionTargets': Array } = {'distributionTargets': []}, public _links: Links = null) { + } +} \ No newline at end of file diff --git a/admin/src/app/model/workbasketSummary.ts b/admin/src/app/model/workbasketSummary.ts new file mode 100644 index 000000000..d04c0541e --- /dev/null +++ b/admin/src/app/model/workbasketSummary.ts @@ -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 = undefined){} +} \ No newline at end of file diff --git a/admin/src/app/pipes/seleted-workbasket.pipe.ts b/admin/src/app/pipes/seleted-workbasket.pipe.ts index 1924b3ead..db214837c 100644 --- a/admin/src/app/pipes/seleted-workbasket.pipe.ts +++ b/admin/src/app/pipes/seleted-workbasket.pipe.ts @@ -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; } - -} \ No newline at end of file +} diff --git a/admin/src/app/services/workbasket.service.ts b/admin/src/app/services/workbasket.service.ts index da791e69f..e95161fac 100644 --- a/admin/src/app/services/workbasket.service.ts +++ b/admin/src/app/services/workbasket.service.ts @@ -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 { - return this.httpClient.get(url, this.httpOptions); + getWorkBasketsDistributionTargets(url: string): Observable { + return this.httpClient.get(url, this.httpOptions); + } + + // PUT + updateWorkBasketsDistributionTargets(url: string, distributionTargetsIds :Array): Observable { + return this.httpClient.put(url, distributionTargetsIds, this.httpOptions); } diff --git a/admin/src/app/shared/filter/filter.component.scss b/admin/src/app/shared/filter/filter.component.scss index ec8bf3314..48c0a54d9 100644 --- a/admin/src/app/shared/filter/filter.component.scss +++ b/admin/src/app/shared/filter/filter.component.scss @@ -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; } \ No newline at end of file diff --git a/admin/src/app/shared/general-message-modal/general-message-modal.component.scss b/admin/src/app/shared/general-message-modal/general-message-modal.component.scss index 511584002..e4ac5cd9c 100644 --- a/admin/src/app/shared/general-message-modal/general-message-modal.component.scss +++ b/admin/src/app/shared/general-message-modal/general-message-modal.component.scss @@ -1 +1 @@ -.word-break{word-break: break-all; } \ No newline at end of file +.word-break{word-break: break-word; } \ No newline at end of file diff --git a/admin/src/app/shared/general-message-modal/general-message-modal.component.ts b/admin/src/app/shared/general-message-modal/general-message-modal.component.ts index fcaf5d920..a8dbfc8f3 100644 --- a/admin/src/app/shared/general-message-modal/general-message-modal.component.ts +++ b/admin/src/app/shared/general-message-modal/general-message-modal.component.ts @@ -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({ @@ -7,9 +7,9 @@ declare var $: any; styleUrls: ['./general-message-modal.component.scss'] }) export class GeneralMessageModalComponent implements OnChanges { - - @Input() - message: string = ''; + + @Input() message: string; + @Output() messageChange = new EventEmitter(); @Input() title: string = ''; @@ -29,7 +29,8 @@ export class GeneralMessageModalComponent implements OnChanges { } removeMessage() { - this.message = undefined; + this.message = ''; + this.messageChange.emit(this.message); } } diff --git a/admin/src/app/shared/spinner/spinner.component.ts b/admin/src/app/shared/spinner/spinner.component.ts index 58ba2d789..6c65f3e55 100644 --- a/admin/src/app/shared/spinner/spinner.component.ts +++ b/admin/src/app/shared/spinner/spinner.component.ts @@ -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) { @@ -36,7 +38,9 @@ export class SpinnerComponent { @Input() positionClass: string = undefined; - + + @Output() + requestTimeoutExceeded = new EventEmitter() @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 { diff --git a/admin/src/app/workbasket/details/access-items/access-items.component.html b/admin/src/app/workbasket/details/access-items/access-items.component.html index d49123012..d1a7c4a30 100644 --- a/admin/src/app/workbasket/details/access-items/access-items.component.html +++ b/admin/src/app/workbasket/details/access-items/access-items.component.html @@ -1,5 +1,5 @@ - +
@@ -58,7 +58,7 @@ - + diff --git a/admin/src/app/workbasket/details/access-items/access-items.component.scss b/admin/src/app/workbasket/details/access-items/access-items.component.scss index 333bcdc1f..6f3248ece 100644 --- a/admin/src/app/workbasket/details/access-items/access-items.component.scss +++ b/admin/src/app/workbasket/details/access-items/access-items.component.scss @@ -16,4 +16,4 @@ td { &.has-changes { border-bottom: 1px solid #f0ad4e;; } -} \ No newline at end of file +} diff --git a/admin/src/app/workbasket/details/access-items/access-items.component.ts b/admin/src/app/workbasket/details/access-items/access-items.component.ts index 8bef081c5..9ef50adc8 100644 --- a/admin/src/app/workbasket/details/access-items/access-items.component.ts +++ b/admin/src/app/workbasket/details/access-items/access-items.component.ts @@ -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`)); diff --git a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.html b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.html index b6a1e1558..53290c2fe 100644 --- a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.html +++ b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.html @@ -1,101 +1,34 @@ - - + +
- +

{{workbasket.name}}

-
-
-
- -
-
-
Available distribution targets
-
- -
- -
    - -
  • -
    -
    -
    - -
    -
    -
    -
    {{distributionTarget.name}} ({{distributionTarget.key}})
    -
    {{distributionTarget.description}}
    -
    {{distributionTarget.owner}}  
    -
    -
    -
  • -
-
+ -
-
-
- -
-
-
Selected distribution targets
-
- -
- -
    - -
      -
    • -
      -
      -
      - -
      -
      -
      -
      {{distributionTarget.name}} ({{distributionTarget.key}})
      -
      {{distributionTarget.description}}
      -
      {{distributionTarget.owner}}  
      -
      -
      -
    • -
    -
-
+
\ No newline at end of file diff --git a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.scss b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.scss index 9329bb8ce..56ab9bb4b 100644 --- a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.scss +++ b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.scss @@ -1,13 +1,4 @@ -.list-group { - margin-top: 8px; -} - -.list-left li, -.list-right li { - cursor: pointer; -} - .button-margin-top { margin-top: 100px } @@ -16,28 +7,9 @@ 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){ width: 45.82%; } -} \ No newline at end of file +} diff --git a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.spec.ts b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.spec.ts index 45951b219..582f9df8f 100644 --- a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.spec.ts +++ b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.spec.ts @@ -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( - new WorkbasketSummary("1", "key1", "NAME1", "description 1", "owner 1", "", "", "PERSONAL", "", "", "", ""), - new WorkbasketSummary("2", "key2", "NAME2", "description 2", "owner 2", "", "", "GROUP", "", "", "", "")) + 'workbaskets': new Array( + 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; - let workbasketService; - let workbasket = new Workbasket('1', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }, { 'href': 'someurl' }, { 'href': 'someurl' })); + let component: DistributionTargetsComponent; + let fixture: ComponentFixture; + 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(new WorkbasketSummary('id1', '', '', '', '', '', '', '', '', '', '', '', new Links({ 'href': 'someurl' }))) }, new Links({ 'href': 'someurl' }))) - }) - spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => { - return Observable.of(new WorkbasketSummaryResource( - { 'workbaskets': new Array(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( + 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(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( + 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( + 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); + + }); }); diff --git a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.ts b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.ts index c65f583a7..569a2e2b3 100644 --- a/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.ts +++ b/admin/src/app/workbasket/details/distribution-targets/distribution-targets.component.ts @@ -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 = []; - distributionTargetsRight: Array = []; - distributionTargetsSelected: Array = []; + + distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource; + distributionTargetsLeft: Array; + distributionTargetsRight: Array; + distributionTargetsSelected: Array; + distributionTargetsClone: Array; + distributionTargetsSelectedClone: Array; - 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 { 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 { + let distributionTargetsSelelected: Array = []; + this.distributionTargetsSelected.forEach(element => { + distributionTargetsSelelected.push(element.workbasketId); + }) + return distributionTargetsSelelected; } private ngOnDestroy(): void { diff --git a/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.html b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.html new file mode 100644 index 000000000..b89907b50 --- /dev/null +++ b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.html @@ -0,0 +1,38 @@ +
+
+
+ +
+
+
Available distribution targets
+
+
+ +
+
+ + +
    +
  • +
    +
    +
    + +
    +
    +
    +
    {{distributionTarget.name}} ({{distributionTarget.key}})
    +
    {{distributionTarget.description}}
    +
    {{distributionTarget.owner}}  
    +
    +
    +
  • +
+
\ No newline at end of file diff --git a/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.scss b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.scss new file mode 100644 index 000000000..cf6fb5b6e --- /dev/null +++ b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.scss @@ -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; +} diff --git a/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.ts b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.ts new file mode 100644 index 000000000..3d1f1cb14 --- /dev/null +++ b/admin/src/app/workbasket/details/distribution-targets/dual-list/dual-list.component.ts @@ -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; + @Output() distributionTargetsChange = new EventEmitter>(); + @Input() distributionTargetsSelected: Array; + @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 }); + } + +} diff --git a/admin/src/app/workbasket/details/information/workbasket-information.component.html b/admin/src/app/workbasket/details/information/workbasket-information.component.html index 7cfc6b1c7..f05c608fd 100644 --- a/admin/src/app/workbasket/details/information/workbasket-information.component.html +++ b/admin/src/app/workbasket/details/information/workbasket-information.component.html @@ -1,5 +1,5 @@ - +
diff --git a/admin/src/app/workbasket/details/workbasket-details.component.html b/admin/src/app/workbasket/details/workbasket-details.component.html index 1a5f45efb..f736120a8 100644 --- a/admin/src/app/workbasket/details/workbasket-details.component.html +++ b/admin/src/app/workbasket/details/workbasket-details.component.html @@ -1,7 +1,7 @@
-
+
-
+
diff --git a/admin/src/app/workbasket/details/workbasket-details.component.scss b/admin/src/app/workbasket/details/workbasket-details.component.scss index d8ee75cf4..54ec032ce 100644 --- a/admin/src/app/workbasket/details/workbasket-details.component.scss +++ b/admin/src/app/workbasket/details/workbasket-details.component.scss @@ -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; -} \ No newline at end of file diff --git a/admin/src/app/workbasket/details/workbasket-details.component.spec.ts b/admin/src/app/workbasket/details/workbasket-details.component.spec.ts index 4e3a4c91b..44c4dfcdc 100644 --- a/admin/src/app/workbasket/details/workbasket-details.component.spec.ts +++ b/admin/src/app/workbasket/details/workbasket-details.component.spec.ts @@ -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()}, new Links({'href': 'url'})) )}) - spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => { return Observable.of(new WorkbasketSummaryResource( {'workbaskets': new Array()}, new Links({'href': 'url'})) ) }) + spyOn(workbasketService, 'getWorkBasketAccessItems').and.callFake(() => { return Observable.of(new WorkbasketAccessItemsResource({ 'accessItems': new Array() }, new Links({ 'href': 'url' }))) }) + spyOn(workbasketService, 'getWorkBasketsDistributionTargets').and.callFake(() => { return Observable.of(new WorkbasketSummaryResource({ 'workbaskets': new Array() }, new Links({ 'href': 'url' }))) }) }); diff --git a/admin/src/app/workbasket/list/workbasket-list.component.scss b/admin/src/app/workbasket/list/workbasket-list.component.scss index d9c16c0c9..c625e191d 100644 --- a/admin/src/app/workbasket/list/workbasket-list.component.scss +++ b/admin/src/app/workbasket/list/workbasket-list.component.scss @@ -17,5 +17,6 @@ a > label{ } .tab-align{ + border-bottom: 1px solid #ddd; padding-bottom: 12px; } diff --git a/admin/src/assets/_site.scss b/admin/src/assets/_site.scss index 36528caa0..6f6d88b84 100644 --- a/admin/src/assets/_site.scss +++ b/admin/src/assets/_site.scss @@ -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; -} \ No newline at end of file + 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; + } + } +}