TSK-647 - [UI] Add pagination or infite scrolling to distribution targets lists in order to avoid loading the whole workbasket list.

This commit is contained in:
Jose Ignacio Recuerda Cambil 2018-11-06 14:36:55 +01:00 committed by Martin Rojas Miguel Angel
parent f31964d771
commit aa250c926d
21 changed files with 957 additions and 855 deletions

1394
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -35,6 +35,7 @@
"material-design-icons": "3.0.1", "material-design-icons": "3.0.1",
"ng2-charts": "1.6.0", "ng2-charts": "1.6.0",
"ngx-bootstrap": "3.0.1", "ngx-bootstrap": "3.0.1",
"ngx-infinite-scroll": "6.0.1",
"node-sass": "4.9.2", "node-sass": "4.9.2",
"popper.js": "1.14.3", "popper.js": "1.14.3",
"rxjs": "6.2.2", "rxjs": "6.2.2",

View File

@ -6,6 +6,7 @@ import {AngularSvgIconModule} from 'angular-svg-icon';
import {AlertModule, TypeaheadModule} from 'ngx-bootstrap'; import {AlertModule, TypeaheadModule} from 'ngx-bootstrap';
import {SharedModule} from 'app/shared/shared.module'; import {SharedModule} from 'app/shared/shared.module';
import {AdministrationRoutingModule} from './administration-routing.module'; import {AdministrationRoutingModule} from './administration-routing.module';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
/** /**
* Components * Components
*/ */
@ -39,7 +40,8 @@ const MODULES = [
AlertModule, AlertModule,
SharedModule, SharedModule,
AdministrationRoutingModule, AdministrationRoutingModule,
TypeaheadModule TypeaheadModule,
InfiniteScrollModule
]; ];
const DECLARATIONS = [ const DECLARATIONS = [

View File

@ -12,10 +12,13 @@
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span> <span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4> </h4>
</div> </div>
<div class="panel-body"> <div #panelBody class="panel-body">
<taskana-dual-list id="dual-list-Left" header="Available distribution targets" [(distributionTargets)]="distributionTargetsLeft" <div class="dual-list list-left col-xs-12 col-md-5-6 container">
[distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" [side]="side.LEFT" <taskana-dual-list #dualListLeft id="dual-list-Left" header="Available distribution targets" [(distributionTargets)]="distributionTargetsLeft"
[requestInProgress]="requestInProgressLeft" class="dual-list list-left col-xs-12 col-md-5-6 container"></taskana-dual-list> [distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" (scrolling)="onScroll($event)" [side]="side.LEFT"
[requestInProgress]="requestInProgressLeft" [loadingItems]="loadingItems" [(allSelected)]="selectAllLeft"></taskana-dual-list>
</div>
<div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top"> <div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top">
<button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight" <button (click)="moveDistributionTargets(side.LEFT)" [disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-right" data-toggle="tooltip" title="Move to selected distribution targets"> class="btn btn-default move-right" data-toggle="tooltip" title="Move to selected distribution targets">
@ -36,8 +39,10 @@
<span class="material-icons md-20 blue">expand_less</span> <span class="material-icons md-20 blue">expand_less</span>
</button> </button>
</div> </div>
<taskana-dual-list id="dual-list-right" header="Selected distribution targets" [(distributionTargets)]="distributionTargetsRight" <div class="dual-list list-right col-xs-12 col-md-5-6 container">
[distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" <taskana-dual-list #dualListRight id="dual-list-right" header="Selected distribution targets" [(distributionTargets)]="distributionTargetsRight"
[requestInProgress]="requestInProgressRight" [side]="side.RIGHT" class="dual-list list-right col-xs-12 col-md-5-6 container"></taskana-dual-list> [distributionTargetsSelected]="distributionTargetsSelected" (performDualListFilter)="performFilter($event)" [requestInProgress]="requestInProgressRight"
[side]="side.RIGHT" [(allSelected)]="selectAllRight"></taskana-dual-list>
</div>
</div> </div>
</div> </div>

View File

@ -22,6 +22,7 @@ import { DualListComponent } from './dual-list/dual-list.component';
import { DistributionTargetsComponent, Side } from './distribution-targets.component'; import { DistributionTargetsComponent, Side } from './distribution-targets.component';
import { LinksWorkbasketSummary } from 'app/models/links-workbasket-summary'; import { LinksWorkbasketSummary } from 'app/models/links-workbasket-summary';
import { configureTests } from 'app/app.test.configuration'; import { configureTests } from 'app/app.test.configuration';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
describe('DistributionTargetsComponent', () => { describe('DistributionTargetsComponent', () => {
let component: DistributionTargetsComponent; let component: DistributionTargetsComponent;
@ -33,7 +34,7 @@ describe('DistributionTargetsComponent', () => {
beforeEach(done => { beforeEach(done => {
const configure = (testBed: TestBed) => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
imports: [AngularSvgIconModule, HttpClientModule], imports: [AngularSvgIconModule, HttpClientModule, InfiniteScrollModule],
declarations: [DistributionTargetsComponent, DualListComponent], declarations: [DistributionTargetsComponent, DualListComponent],
providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService, RequestInProgressService, providers: [WorkbasketService, AlertService, SavingWorkbasketService, ErrorModalService, RequestInProgressService,
] ]
@ -72,10 +73,10 @@ describe('DistributionTargetsComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should clone distribution target selected on init', () => { it('should clone distribution target selected on init', () => {
expect(component.distributionTargetsClone).toBeDefined(); expect(component.distributionTargetsClone).toBeDefined();
}); });
it('should clone distribution target left and distribution target right lists on init', () => { it('should clone distribution target left and distribution target right lists on init', () => {
expect(component.distributionTargetsLeft).toBeDefined(); expect(component.distributionTargetsLeft).toBeDefined();
expect(component.distributionTargetsRight).toBeDefined(); expect(component.distributionTargetsRight).toBeDefined();
@ -92,6 +93,7 @@ describe('DistributionTargetsComponent', () => {
}) })
expect(repeteadElemens).toBeFalsy(); expect(repeteadElemens).toBeFalsy();
}); });
it('should filter left list and keep selected elements as selected', () => { it('should filter left list and keep selected elements as selected', () => {
component.performFilter({ filterBy: new FilterModel({ component.performFilter({ filterBy: new FilterModel({
name: 'someName', owner: 'someOwner', description: 'someDescription', key: 'someKey'}), side: Side.LEFT }); name: 'someName', owner: 'someOwner', description: 'someDescription', key: 'someKey'}), side: Side.LEFT });
@ -103,6 +105,7 @@ describe('DistributionTargetsComponent', () => {
expect(component.distributionTargetsRight.length).toBe(1); expect(component.distributionTargetsRight.length).toBe(1);
expect(component.distributionTargetsRight[0].workbasketId).toBe('id2'); expect(component.distributionTargetsRight[0].workbasketId).toBe('id2');
}); });
it('should reset distribution target and distribution target selected on reset', () => { it('should reset distribution target and distribution target selected on reset', () => {
component.distributionTargetsLeft.push( component.distributionTargetsLeft.push(
new WorkbasketSummary('id4', '', '', '', '', '', '', '', '', '', '', '', false, new Links({ 'href': 'someurl' }))); new WorkbasketSummary('id4', '', '', '', '', '', '', '', '', '', '', '', false, new Links({ 'href': 'someurl' })));

View File

@ -1,4 +1,4 @@
import { Component, Input, OnDestroy, SimpleChanges, OnChanges } from '@angular/core'; import { Component, Input, OnDestroy, SimpleChanges, OnChanges, ViewChild, ElementRef } from '@angular/core';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { Workbasket } from 'app/models/workbasket'; import { Workbasket } from 'app/models/workbasket';
@ -14,6 +14,10 @@ import { AlertService } from 'app/services/alert/alert.service';
import { SavingWorkbasketService, SavingInformation } from 'app/administration/services/saving-workbaskets/saving-workbaskets.service'; import { SavingWorkbasketService, SavingInformation } from 'app/administration/services/saving-workbaskets/saving-workbaskets.service';
import { ErrorModalService } from 'app/services/errorModal/error-modal.service'; import { ErrorModalService } from 'app/services/errorModal/error-modal.service';
import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service'; import { RequestInProgressService } from 'app/services/requestInProgress/request-in-progress.service';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { Page } from 'app/models/page';
import { OrientationService } from 'app/services/orientation/orientation.service';
import { Orientation } from 'app/models/orientation';
export enum Side { export enum Side {
LEFT, LEFT,
@ -38,6 +42,7 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
workbasketSubscription: Subscription; workbasketSubscription: Subscription;
workbasketFilterSubscription: Subscription; workbasketFilterSubscription: Subscription;
savingDistributionTargetsSubscription: Subscription; savingDistributionTargetsSubscription: Subscription;
orientationSubscription: Subscription;
distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource; distributionTargetsSelectedResource: WorkbasketDistributionTargetsResource;
distributionTargetsLeft: Array<WorkbasketSummary>; distributionTargetsLeft: Array<WorkbasketSummary>;
@ -48,16 +53,25 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
requestInProgressLeft = false; requestInProgressLeft = false;
requestInProgressRight = false; requestInProgressRight = false;
loadingItems = false;
modalErrorMessage: string; modalErrorMessage: string;
side = Side; side = Side;
private initialized = false; private initialized = false;
page: Page;
cards: number;
selectAllLeft = false;
selectAllRight = false;
@ViewChild('panelBody')
private panelBody: ElementRef;
constructor( constructor(
private workbasketService: WorkbasketService, private workbasketService: WorkbasketService,
private alertService: AlertService, private alertService: AlertService,
private savingWorkbaskets: SavingWorkbasketService, private savingWorkbaskets: SavingWorkbasketService,
private errorModalService: ErrorModalService, private errorModalService: ErrorModalService,
private requestInProgressService: RequestInProgressService) { } private requestInProgressService: RequestInProgressService,
private orientationService: OrientationService) { }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') { if (!this.initialized && changes.active && changes.active.currentValue === 'distributionTargets') {
@ -67,52 +81,31 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
this.setBadge(); this.setBadge();
} }
} }
onScroll(side: Side) {
private init() { if (side === this.side.LEFT && this.page.totalPages > TaskanaQueryParameters.page) {
this.initialized = true; this.loadingItems = true;
this.onRequest(undefined); this.getNextPage(side);
if (!this.workbasket._links.distributionTargets) {
return;
} }
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(true,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, true).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);
});
});
this.savingDistributionTargetsSubscription = this.savingWorkbaskets.triggeredDistributionTargetsSaving()
.subscribe((savingInformation: SavingInformation) => {
if (this.action === ACTION.COPY) {
this.distributionTargetsSelectedResource._links.self.href = savingInformation.url;
this.onSave();
}
});
} }
moveDistributionTargets(side: number) { moveDistributionTargets(side: number) {
if (side === Side.LEFT) { if (side === Side.LEFT) {
const itemsLeft = this.distributionTargetsLeft.length;
const itemsRight = this.distributionTargetsRight.length;
const itemsSelected = this.getSelectedItems(this.distributionTargetsLeft, this.distributionTargetsRight) const itemsSelected = this.getSelectedItems(this.distributionTargetsLeft, this.distributionTargetsRight)
this.distributionTargetsSelected = this.distributionTargetsSelected.concat(itemsSelected); this.distributionTargetsSelected = this.distributionTargetsSelected.concat(itemsSelected);
this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected); this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected);
if (((itemsLeft - itemsSelected.length) <= TaskanaQueryParameters.pageSize) && ((itemsLeft + itemsRight) < this.page.totalElements)) {
this.getNextPage(side);
}
} else { } else {
const itemsSelected = this.getSelectedItems(this.distributionTargetsRight, this.distributionTargetsLeft); const itemsSelected = this.getSelectedItems(this.distributionTargetsRight, this.distributionTargetsLeft);
this.distributionTargetsSelected = this.removeSeletedItems(this.distributionTargetsSelected, itemsSelected); this.distributionTargetsSelected = this.removeSeletedItems(this.distributionTargetsSelected, itemsSelected);
this.distributionTargetsRight = this.removeSeletedItems(this.distributionTargetsRight, itemsSelected); this.distributionTargetsRight = this.removeSeletedItems(this.distributionTargetsRight, itemsSelected);
this.distributionTargetsLeft = this.distributionTargetsLeft.concat(itemsSelected); this.distributionTargetsLeft = this.distributionTargetsLeft.concat(itemsSelected);
} }
this.uncheckSelectAll(side);
} }
onSave() { onSave() {
@ -158,6 +151,90 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
}); });
} }
ngOnDestroy(): void {
if (this.distributionTargetsSubscription) { this.distributionTargetsSubscription.unsubscribe(); }
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); }
if (this.workbasketFilterSubscription) { this.workbasketFilterSubscription.unsubscribe(); }
if (this.savingDistributionTargetsSubscription) { this.savingDistributionTargetsSubscription.unsubscribe(); }
if (this.orientationSubscription) { this.orientationSubscription.unsubscribe(); }
}
private init() {
this.onRequest(undefined);
if (!this.workbasket._links.distributionTargets) {
return;
}
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);
TaskanaQueryParameters.page = 1;
this.calculateNumberItemsList();
this.getWorkbaskets();
});
this.savingDistributionTargetsSubscription = this.savingWorkbaskets.triggeredDistributionTargetsSaving()
.subscribe((savingInformation: SavingInformation) => {
if (this.action === ACTION.COPY) {
this.distributionTargetsSelectedResource._links.self.href = savingInformation.url;
this.onSave();
}
});
this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => {
this.calculateNumberItemsList();
this.getWorkbaskets();
});
}
private calculateNumberItemsList() {
if (this.panelBody) {
const cardHeight = 72;
this.cards = this.orientationService.calculateNumberItemsList(this.panelBody.nativeElement.offsetHeight, cardHeight, 100, true) + 1;
}
}
private getNextPage(side: Side) {
TaskanaQueryParameters.page = TaskanaQueryParameters.page + 1;
this.getWorkbaskets(side);
}
private getWorkbaskets(side?: Side) {
if (!this.distributionTargetsLeft) {
this.distributionTargetsLeft = [];
}
if (!this.distributionTargetsRight) {
this.distributionTargetsRight = [];
}
if (this.distributionTargetsSelected && !this.initialized) {
this.initialized = true;
TaskanaQueryParameters.pageSize = this.cards + this.distributionTargetsSelected.length;
}
this.workbasketSubscription = this.workbasketService.getWorkBasketsSummary(true,
undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined,
undefined, undefined, undefined, false).subscribe(
(distributionTargetsAvailable: WorkbasketSummaryResource) => {
if (TaskanaQueryParameters.page === 1) {
this.distributionTargetsLeft = [];
this.page = distributionTargetsAvailable.page;
}
if (side === this.side.LEFT) {
this.distributionTargetsLeft.push(...distributionTargetsAvailable._embedded.workbaskets);
} else if (side === this.side.RIGHT) {
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
} else {
this.distributionTargetsLeft.push(...distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
this.distributionTargetsClone = Object.assign([], distributionTargetsAvailable._embedded.workbaskets);
}
this.onRequest(undefined, true);
});
}
private setBadge() { private setBadge() {
if (this.action === ACTION.COPY) { if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`; this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
@ -178,6 +255,9 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
} }
private onRequest(side: Side = undefined, finished: boolean = false) { private onRequest(side: Side = undefined, finished: boolean = false) {
if (this.loadingItems) {
this.loadingItems = false;
}
if (finished) { if (finished) {
side === undefined ? (this.requestInProgressLeft = false, this.requestInProgressRight = false) : side === undefined ? (this.requestInProgressLeft = false, this.requestInProgressRight = false) :
side === Side.LEFT ? this.requestInProgressLeft = false : this.requestInProgressRight = false; side === Side.LEFT ? this.requestInProgressLeft = false : this.requestInProgressRight = false;
@ -195,13 +275,8 @@ export class DistributionTargetsComponent implements OnChanges, OnDestroy {
return distributionTargetsSelelected; return distributionTargetsSelelected;
} }
ngOnDestroy(): void { private uncheckSelectAll(side: number) {
if (this.distributionTargetsSubscription) { this.distributionTargetsSubscription.unsubscribe(); } if (side === Side.LEFT && this.selectAllLeft) { this.selectAllLeft = false; }
if (this.workbasketSubscription) { this.workbasketSubscription.unsubscribe(); } if (side === Side.RIGHT && this.selectAllRight) { this.selectAllRight = false; }
if (this.workbasketFilterSubscription) { this.workbasketFilterSubscription.unsubscribe(); }
if (this.savingDistributionTargetsSubscription) { this.savingDistributionTargetsSubscription.unsubscribe(); }
} }
} }

View File

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

View File

@ -8,14 +8,14 @@ $selected-item: #e3f3f5;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05);
& .row { & .row {
padding: 3px 0px 0px 3px; padding: 0px 0px 0px 3px;
} }
& .row:first { & .row:first {
border-top: 1px solid #ddd; border-top: 1px solid #ddd;
} }
& div.pull-right { & div.pull-right {
margin-right: 18px; margin-right: 17px;
} }
& >.list-group { & >.list-group {
@ -29,13 +29,28 @@ $selected-item: #e3f3f5;
} }
overflow-x: hidden; overflow-x: hidden;
overflow-y: auto; overflow-y: hidden;
@media screen and (max-width: 991px){ @media screen and (max-width: 991px){
max-height: 33vh; height: calc((100vh - 241px) / 2);
min-height: 120px;
margin-bottom: 0;
} }
max-height: 75vh; max-height: calc(100vh - 194px);
margin-bottom: 2px;
}
.infinite-scroll {
overflow-y: scroll;
height: calc(100vh - 233px);
@media screen and (max-width: 991px){
height: calc((100vh - 315px) / 2);
min-height: 83px;
}
}
.header {
margin: 2px -15px 1px -15px;
} }
.list-group { .list-group {
@ -96,4 +111,3 @@ li.list-group-item:hover, {
text-decoration: none; text-decoration: none;
background-color: #f5f5f5; background-color: #f5f5f5;
} }

View File

@ -13,19 +13,19 @@ import { expandDown } from 'app/shared/animations/expand.animation';
export class DualListComponent implements OnInit { export class DualListComponent implements OnInit {
@Input() distributionTargets: Array<WorkbasketSummary>; @Input() distributionTargets: Array<WorkbasketSummary>;
@Output() distributionTargetsChange = new EventEmitter<Array<WorkbasketSummary>>();
@Input() distributionTargetsSelected: Array<WorkbasketSummary>; @Input() distributionTargetsSelected: Array<WorkbasketSummary>;
@Output() performDualListFilter = new EventEmitter<{ filterBy: FilterModel, side: Side }>(); @Output() performDualListFilter = new EventEmitter<{ filterBy: FilterModel, side: Side }>();
@Input() requestInProgress = false; @Input() requestInProgress = false;
@Input() loadingItems ? = false;
@Input() side: Side; @Input() side: Side;
@Input() header: string; @Input() header: string;
@Output() scrolling = new EventEmitter<Side>();
@Input() allSelected;
@Output() allSelectedChange = new EventEmitter<boolean>();
sideNumber = 0; sideNumber = 0;
toggleDtl = false;
toolbarState = false; toolbarState = false;
constructor() { }
ngOnInit() { ngOnInit() {
this.sideNumber = this.side === Side.LEFT ? 0 : 1; this.sideNumber = this.side === Side.LEFT ? 0 : 1;
} }
@ -34,9 +34,19 @@ export class DualListComponent implements OnInit {
this.distributionTargets.forEach((element: any) => { this.distributionTargets.forEach((element: any) => {
element.selected = selected; element.selected = selected;
}); });
this.allSelectedChange.emit(this.allSelected);
}
onScroll() {
this.scrolling.emit(this.side);
} }
performAvailableFilter(filterModel: FilterModel) { performAvailableFilter(filterModel: FilterModel) {
this.performDualListFilter.emit({ filterBy: filterModel, side: this.side }); this.performDualListFilter.emit({ filterBy: filterModel, side: this.side });
} }
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
} }

View File

@ -30,6 +30,7 @@ import { WorkbasketInformationComponent } from './information/workbasket-informa
import { AccessItemsComponent } from './access-items/access-items.component'; import { AccessItemsComponent } from './access-items/access-items.component';
import { DistributionTargetsComponent } from './distribution-targets/distribution-targets.component'; import { DistributionTargetsComponent } from './distribution-targets/distribution-targets.component';
import { DualListComponent } from './distribution-targets//dual-list/dual-list.component'; import { DualListComponent } from './distribution-targets//dual-list/dual-list.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
@Component({ @Component({
selector: 'taskana-dummy-detail', selector: 'taskana-dummy-detail',
@ -55,7 +56,8 @@ describe('WorkbasketDetailsComponent', () => {
beforeEach(done => { beforeEach(done => {
const configure = (testBed: TestBed) => { const configure = (testBed: TestBed) => {
testBed.configureTestingModule({ testBed.configureTestingModule({
imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule], imports: [RouterTestingModule.withRoutes(routes), FormsModule, AngularSvgIconModule, HttpClientModule, ReactiveFormsModule,
InfiniteScrollModule],
declarations: [WorkbasketDetailsComponent, WorkbasketInformationComponent, declarations: [WorkbasketDetailsComponent, WorkbasketInformationComponent,
AccessItemsComponent, AccessItemsComponent,
DistributionTargetsComponent, DualListComponent, DummyDetailComponent], DistributionTargetsComponent, DualListComponent, DummyDetailComponent],

View File

@ -37,4 +37,4 @@
</ng-template> </ng-template>
</div> </div>
<taskana-pagination [(page)]="workbasketsResource !== undefined ? workbasketsResource.page : workbasketsResource" <taskana-pagination [(page)]="workbasketsResource !== undefined ? workbasketsResource.page : workbasketsResource"
[type]="type" (changePage)="changePage($event)"></taskana-pagination> [type]="type" [numberOfItems]="workbaskets.length" (changePage)="changePage($event)"></taskana-pagination>

View File

@ -27,6 +27,7 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
pageSelected = 1; pageSelected = 1;
pageSize = 9; pageSize = 9;
type = 'workbaskets'; type = 'workbaskets';
cards: number = this.pageSize;
sort: SortingModel = new SortingModel(); sort: SortingModel = new SortingModel();
filterBy: FilterModel = new FilterModel({name: '', owner: '', type: '', description: '', key: ''}); filterBy: FilterModel = new FilterModel({name: '', owner: '', type: '', description: '', key: ''});
@ -62,7 +63,7 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
}); });
this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => { this.orientationSubscription = this.orientationService.getOrientation().subscribe((orientation: Orientation) => {
this.refreshWorkbasketList(); this.refreshWorkbasketList();
}) });
} }
selectWorkbasket(id: string) { selectWorkbasket(id: string) {
@ -86,16 +87,13 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
} }
refreshWorkbasketList() { refreshWorkbasketList() {
const toolbarSize = this.toolbarElement.nativeElement.offsetHeight; this.cards = this.orientationService.calculateNumberItemsList(
const cardHeight = 72; window.innerHeight, 72, 170 + this.toolbarElement.nativeElement.offsetHeight, false);
const unusedHeight = 145;
const totalHeight = window.innerHeight;
const cards = Math.round((totalHeight - (unusedHeight + toolbarSize)) / cardHeight);
cards > 0 ? TaskanaQueryParameters.pageSize = cards : TaskanaQueryParameters.pageSize = 1;
this.performRequest(); this.performRequest();
} }
private performRequest(): void { private performRequest(): void {
TaskanaQueryParameters.pageSize = this.cards;
this.requestInProgress = true; this.requestInProgress = true;
this.workbaskets = []; this.workbaskets = [];
this.workbasketServiceSubscription = this.workbasketService.getWorkBasketsSummary( this.workbasketServiceSubscription = this.workbasketService.getWorkBasketsSummary(

View File

@ -1,6 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Orientation } from 'app/models/orientation'; import { Orientation } from 'app/models/orientation';
import { BehaviorSubject , Observable } from 'rxjs'; import { BehaviorSubject , Observable } from 'rxjs';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
@Injectable() @Injectable()
export class OrientationService { export class OrientationService {
@ -33,6 +34,13 @@ export class OrientationService {
} }
} }
calculateNumberItemsList(heightContainer: number, cardHeight: number, unusedHeight: number, doubleList = false): number {
let cards = Math.round((heightContainer - unusedHeight) / cardHeight);
if (doubleList && window.innerWidth < 992) { cards = Math.floor(cards / 2); }
cards > 0 ? TaskanaQueryParameters.pageSize = cards : TaskanaQueryParameters.pageSize = 1;
return cards;
}
private detectOrientation(): Orientation { private detectOrientation(): Orientation {
if (window.innerHeight > window.innerWidth) { if (window.innerHeight > window.innerWidth) {
return Orientation.portrait; return Orientation.portrait;

View File

@ -2,7 +2,7 @@
<div *ngIf="filterTypeIsWorkbasket(); else tasktype"> <div *ngIf="filterTypeIsWorkbasket(); else tasktype">
<div class="row"> <div class="row">
<div class="dropdown col-xs-2"> <div class="dropdown col-xs-2">
<button class="btn btn-default" data-toggle="dropdown" type="button" id="dropdownMenufilter" data-toggle="dropdown" <button class="btn btn-default btn-sm" data-toggle="dropdown" type="button" id="dropdownMenufilter" data-toggle="dropdown"
aria-haspopup="true" aria-expanded="true"> aria-haspopup="true" aria-expanded="true">
<taskana-icon-type [type]="filter.filterParams?.type"></taskana-icon-type> <taskana-icon-type [type]="filter.filterParams?.type"></taskana-icon-type>
</button> </button>
@ -16,30 +16,30 @@
</ul> </ul>
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.name" (keyup.enter)="search()" class="form-control" id="display-name-filter" <input type="text" [(ngModel)]="filter.filterParams.name" (keyup.enter)="search()" class="form-control input-sm" id="display-name-filter"
placeholder="Filter name"> placeholder="Filter name">
</div> </div>
<div class="col-xs-4"> <div class="col-xs-4">
<input type="text" [(ngModel)]="filter.filterParams.key" (keyup.enter)="search()" class="form-control" id="display-key-filter" <input type="text" [(ngModel)]="filter.filterParams.key" (keyup.enter)="search()" class="form-control input-sm" id="display-key-filter"
placeholder="Filter key"> placeholder="Filter key">
</div> </div>
<button (click)="clear(); search()" type="button" class="btn btn-default pull-right margin-right" <button (click)="clear(); search()" type="button" class="btn btn-default btn-sm pull-right margin-right"
data-toggle="tooltip" title="Clear"> data-toggle="tooltip" title="Clear">
<span class="material-icons md-20 blue">clear</span> <span class="material-icons md-20 blue">clear</span>
</button> </button>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-8 col-xs-offset-2"> <div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.description" (keyup.enter)="search()" class="form-control" <input type="text" [(ngModel)]="filter.filterParams.description" (keyup.enter)="search()" class="form-control input-sm"
id="display-name-description" placeholder="Filter description"> id="display-name-description" placeholder="Filter description">
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-8 col-xs-offset-2"> <div class="col-xs-8 col-xs-offset-2">
<input type="text" [(ngModel)]="filter.filterParams.owner" (keyup.enter)="search()" class="form-control" id="display-name-owner" <input type="text" [(ngModel)]="filter.filterParams.owner" (keyup.enter)="search()" class="form-control input-sm" id="display-name-owner"
placeholder="Filter owner"> placeholder="Filter owner">
</div> </div>
<button (click)="search()" type="button" class="btn btn-default pull-right margin-right" data-toggle="tooltip" <button (click)="search()" type="button" class="btn btn-default btn-sm pull-right margin-right" data-toggle="tooltip"
title="Search"> title="Search">
<span class="material-icons md-20 blue ">search</span> <span class="material-icons md-20 blue ">search</span>
</button> </button>

View File

@ -3,8 +3,7 @@
<a (click)="changeToPage(1)" aria-label="First"> <a (click)="changeToPage(1)" aria-label="First">
First</a> First</a>
</li> </li>
<li *ngFor="let pageNumber of page?.totalPages | <li *ngFor="let pageNumber of page?.totalPages | spreadNumber: page?.number: maxPagesAvailable">
spreadNumber: page?.number: maxPagesAvailable: page?.totalPages">
<a *ngIf="pageNumber + 1 !== page?.number" (click)="changeToPage(pageNumber+1)">{{pageNumber + 1}}</a> <a *ngIf="pageNumber + 1 !== page?.number" (click)="changeToPage(pageNumber+1)">{{pageNumber + 1}}</a>
<a *ngIf="pageNumber + 1 === page?.number" class="pagination"> <a *ngIf="pageNumber + 1 === page?.number" class="pagination">
<input [(ngModel)]="pageSelected" (keyup.enter)="changeToPage(pageSelected)" type="text" (blur)="changeToPage(pageSelected)"> <input [(ngModel)]="pageSelected" (keyup.enter)="changeToPage(pageSelected)" type="text" (blur)="changeToPage(pageSelected)">

View File

@ -31,6 +31,6 @@ ul.pagination{
} }
.footer{ .footer{
margin: 5px 5px 0 0; margin: 0 5px 0 0;
color: $blue; color: $blue;
} }

View File

@ -97,19 +97,22 @@ describe('PaginationComponent', () => {
it('should getPagesTextToShow return 7 of 13 with size < totalElements', () => { it('should getPagesTextToShow return 7 of 13 with size < totalElements', () => {
component.page = new Page(7, 13, 3, 2); component.page = new Page(7, 13, 3, 2);
component.type = 'workbaskets'; component.type = 'workbaskets';
expect(component.getPagesTextToShow()).toBe('7 of 13 workbaskets'); component.numberOfItems = 5;
expect(component.getPagesTextToShow()).toBe(component.numberOfItems.toString().concat(' of 13 workbaskets'));
}); });
it('should getPagesTextToShow return 6 of 6 with size > totalElements', () => { it('should getPagesTextToShow return 6 of 6 with size > totalElements', () => {
component.page = new Page(7, 6, 3, 2); component.page = new Page(7, 6, 3, 2);
component.type = 'tasks'; component.type = 'tasks';
expect(component.getPagesTextToShow()).toBe('6 of 6 tasks'); component.numberOfItems = 6;
expect(component.getPagesTextToShow()).toBe(component.numberOfItems.toString().concat(' of 6 tasks'));
}); });
it('should getPagesTextToShow return of with totalElements = 0', () => { it('should getPagesTextToShow return of with totalElements = 0', () => {
component.page = new Page(7, 0, 0, 0); component.page = new Page(7, 0, 0, 0);
component.type = 'workbaskets'; component.type = 'workbaskets';
expect(component.getPagesTextToShow()).toBe('0 of 0 workbaskets'); component.numberOfItems = 0;
expect(component.getPagesTextToShow()).toBe(component.numberOfItems.toString().concat(' of 0 workbaskets'));
}); });
}); });

View File

@ -14,7 +14,7 @@ import { Page } from 'app/models/page';
templateUrl: './pagination.component.html', templateUrl: './pagination.component.html',
styleUrls: ['./pagination.component.scss'] styleUrls: ['./pagination.component.scss']
}) })
export class PaginationComponent implements OnInit, OnChanges { export class PaginationComponent implements OnChanges {
@Input() @Input()
page: Page; page: Page;
@Input() @Input()
@ -23,6 +23,8 @@ export class PaginationComponent implements OnInit, OnChanges {
workbasketsResourceChange = new EventEmitter<Page>(); workbasketsResourceChange = new EventEmitter<Page>();
@Output() @Output()
changePage = new EventEmitter<number>(); changePage = new EventEmitter<number>();
@Input()
numberOfItems: number;
previousPageSelected = 1; previousPageSelected = 1;
pageSelected = 1; pageSelected = 1;
maxPagesAvailable = 8; maxPagesAvailable = 8;
@ -30,13 +32,11 @@ export class PaginationComponent implements OnInit, OnChanges {
constructor() {} constructor() {}
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
if (changes.page.currentValue !== undefined) { if (changes.page && changes.page.currentValue !== undefined) {
this.pageSelected = changes.page.currentValue.number; this.pageSelected = changes.page.currentValue.number;
} }
} }
ngOnInit() {}
changeToPage(page) { changeToPage(page) {
if (page < 1) { if (page < 1) {
page = this.pageSelected = 1; page = this.pageSelected = 1;
@ -47,6 +47,8 @@ export class PaginationComponent implements OnInit, OnChanges {
if (this.previousPageSelected !== page) { if (this.previousPageSelected !== page) {
this.changePage.emit(page); this.changePage.emit(page);
this.previousPageSelected = page; this.previousPageSelected = page;
this.page.number = page;
this.pageSelected = page;
} }
} }
@ -54,14 +56,7 @@ export class PaginationComponent implements OnInit, OnChanges {
if (!this.page) { if (!this.page) {
return ''; return '';
} }
let text = this.page.totalElements + ''; const text = this.numberOfItems + '';
if (
this.page &&
this.page.totalElements &&
this.page.totalElements >= this.page.size
) {
text = this.page.size + '';
}
return `${text} of ${this.page.totalElements} ${this.type}`; return `${text} of ${this.page.totalElements} ${this.type}`;
} }
} }

View File

@ -1,4 +1,5 @@
import { Pipe, PipeTransform } from '@angular/core'; import { Pipe, PipeTransform } from '@angular/core';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
@Pipe({ name: 'selectWorkbaskets' }) @Pipe({ name: 'selectWorkbaskets' })
export class SelectWorkBasketPipe implements PipeTransform { export class SelectWorkBasketPipe implements PipeTransform {
@ -18,6 +19,9 @@ export class SelectWorkBasketPipe implements PipeTransform {
originArray.splice(index, 1); originArray.splice(index, 1);
} }
} }
if (originArray.length > TaskanaQueryParameters.pageSize) {
originArray.slice(0, TaskanaQueryParameters.pageSize);
}
returnArray = originArray; returnArray = originArray;
return returnArray; return returnArray;
} }

View File

@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'spreadNumber' }) @Pipe({ name: 'spreadNumber' })
export class SpreadNumberPipe implements PipeTransform { export class SpreadNumberPipe implements PipeTransform {
transform(value: number, currentIndex: number, maxArrayElements: number, maxPageNumber: number): number[] { transform(maxPageNumber: number, currentIndex: number, maxArrayElements: number): number[] {
const returnArray = new Array(); const returnArray = new Array();
if (maxPageNumber <= 5) { if (maxPageNumber <= 5) {
for (let i = 0; i < maxPageNumber; i++) { for (let i = 0; i < maxPageNumber; i++) {

View File

@ -44,5 +44,5 @@
</ng-template> </ng-template>
</div> </div>
</div> </div>
<taskana-pagination *ngIf="tasks && tasks.length > 0" [(page)]="tasksPageInformation" [type]="type" (changePage)="changePage($event)"></taskana-pagination> <taskana-pagination [numberOfItems]="tasks.length" *ngIf="tasks && tasks.length > 0" [(page)]="tasksPageInformation" [type]="type" (changePage)="changePage($event)"></taskana-pagination>
<taskana-code></taskana-code> <taskana-code></taskana-code>