TSK-1475: Hide mechanism workbasket list (#1369)

* TSK-1475: implement toggle button that resizes the width of workbasket list

* TSK-1475: update pagination component view when workbasket list is collapsed

* TSK-1475: optimize workbasket list toolbar when workbasket list is collapsed

* TSK-1475: fix broken jest tests
This commit is contained in:
Chi Nguyen 2020-12-14 09:25:48 +01:00 committed by GitHub
parent 5ac12037bc
commit 3c067c55e3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 185 additions and 63 deletions

View File

@ -10,7 +10,7 @@
</button> </button>
<!-- IMPORT EXPORT --> <!-- IMPORT EXPORT -->
<taskana-administration-import-export [currentSelection]="selectionToImport" [parentComponent]="'workbaskets'"></taskana-administration-import-export> <taskana-administration-import-export *ngIf="workbasketListExpanded" [currentSelection]="selectionToImport" [parentComponent]="'workbaskets'"></taskana-administration-import-export>
<span class="workbasket-details__spacer" style="flex: 1 1 auto"> </span> <span class="workbasket-details__spacer" style="flex: 1 1 auto"> </span>
@ -29,9 +29,10 @@
</button> </button>
<div class="filter__filter-component-wrapper"> <div class="filter__filter-component-wrapper">
<taskana-shared-filter [isExpanded]="isExpanded" (performFilter)="filtering($event)"></taskana-shared-filter> <taskana-shared-filter [isExpanded]="isExpanded"
(performFilter)="filtering($event)"
component="workbasket-list"
(inputComponent)="setComponent($event)"></taskana-shared-filter>
</div> </div>
</div> </div>
<taskana-shared-filter *ngIf="showFilter" (performFilter)="filtering($event)" component="workbasket-list" (inputComponent)="setComponent($event)"></taskana-shared-filter>
</div> </div>

View File

@ -4,6 +4,7 @@
padding: 16px 16px 20px; padding: 16px 16px 20px;
flex-wrap: wrap; flex-wrap: wrap;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
overflow: hidden;
} }
.workbasket-list-toolbar__action-toolbar { .workbasket-list-toolbar__action-toolbar {

View File

@ -19,6 +19,7 @@ import { WorkbasketService } from '../../../shared/services/workbasket/workbaske
styleUrls: ['./workbasket-list-toolbar.component.scss'] styleUrls: ['./workbasket-list-toolbar.component.scss']
}) })
export class WorkbasketListToolbarComponent implements OnInit { export class WorkbasketListToolbarComponent implements OnInit {
@Input() workbasketListExpanded: boolean = true;
@Input() workbaskets: Array<WorkbasketSummary>; @Input() workbaskets: Array<WorkbasketSummary>;
@Input() workbasketDefaultSortBy: string; @Input() workbasketDefaultSortBy: string;
@Output() performSorting = new EventEmitter<Sorting>(); @Output() performSorting = new EventEmitter<Sorting>();

View File

@ -2,7 +2,7 @@
<!-- TOOLBAR --> <!-- TOOLBAR -->
<section #wbToolbar class="workbasket-list__toolbar"> <section #wbToolbar class="workbasket-list__toolbar">
<taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)" <taskana-administration-workbasket-list-toolbar [workbaskets]="workbasketsSummary$ | async" (performFilter)="performFilter($event)"
(performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy"> (performSorting)="performSorting($event)" [workbasketDefaultSortBy]="workbasketDefaultSortBy" [workbasketListExpanded]="expanded">
</taskana-administration-workbasket-list-toolbar> </taskana-administration-workbasket-list-toolbar>
</section> </section>
@ -16,7 +16,7 @@
[value]="workbasket.workbasketId"> [value]="workbasket.workbasketId">
<div class="workbaskets-item__wrapper"> <div class="workbaskets-item__wrapper">
<div class="workbaskets-item__icon"> <div class="workbaskets-item__icon" *ngIf="expanded">
<taskana-administration-icon-type [type]="workbasket.type" size="large" tooltip="true" [selected]="workbasket.workbasketId === selectedId"></taskana-administration-icon-type> <taskana-administration-icon-type [type]="workbasket.type" size="large" tooltip="true" [selected]="workbasket.workbasketId === selectedId"></taskana-administration-icon-type>
</div> </div>
@ -24,7 +24,7 @@
<p> <p>
<b>{{workbasket.name}}</b>, <i>{{workbasket.key}} </i> <b>{{workbasket.name}}</b>, <i>{{workbasket.key}} </i>
</p> </p>
<p>{{workbasket.description}} &nbsp;</p> <p style="max-height: 20px; overflow: hidden; text-overflow: ellipsis">{{workbasket.description}} &nbsp;</p>
<p>{{workbasket.owner}} &nbsp;</p> <p>{{workbasket.owner}} &nbsp;</p>
</div> </div>
@ -53,6 +53,7 @@
<taskana-shared-pagination <taskana-shared-pagination
[page]="(workbasketsSummaryRepresentation$ | async) ? (workbasketsSummaryRepresentation$ | async)?.page : (workbasketsSummaryRepresentation$ | async)" [page]="(workbasketsSummaryRepresentation$ | async) ? (workbasketsSummaryRepresentation$ | async)?.page : (workbasketsSummaryRepresentation$ | async)"
[type]="type" [type]="type"
[expanded]="expanded"
[numberOfItems]="(workbasketsSummary$ | async)?.length" [numberOfItems]="(workbasketsSummary$ | async)?.length"
(changePage)="changePage($event)"> (changePage)="changePage($event)">
</taskana-shared-pagination> </taskana-shared-pagination>

View File

@ -12,6 +12,7 @@
} }
.mat-list-item { .mat-list-item {
height: 90px !important; height: 90px !important;
overflow: hidden;
} }
.mat-list-single-selected-option { .mat-list-single-selected-option {
background-color: $green !important; background-color: $green !important;
@ -25,6 +26,7 @@
} }
.workbaskets-item__info { .workbaskets-item__info {
display: block; display: block;
overflow: hidden;
padding: 8px 0; padding: 8px 0;
} }
p { p {

View File

@ -73,6 +73,7 @@ const requestInProgressServiceSpy = jest.fn().mockImplementation(
class WorkbasketListToolbarStub { class WorkbasketListToolbarStub {
@Input() workbaskets: Array<WorkbasketSummary>; @Input() workbaskets: Array<WorkbasketSummary>;
@Input() workbasketDefaultSortBy: string; @Input() workbasketDefaultSortBy: string;
@Input() workbasketListExpanded: boolean;
@Output() performSorting = new EventEmitter<Sorting>(); @Output() performSorting = new EventEmitter<Sorting>();
@Output() performFilter = new EventEmitter<Filter>(); @Output() performFilter = new EventEmitter<Filter>();
} }
@ -87,6 +88,7 @@ class IconTypeStub {
class PaginationStub { class PaginationStub {
@Input() page: Page; @Input() page: Page;
@Input() type: String; @Input() type: String;
@Input() expanded: boolean;
@Output() changePage = new EventEmitter<number>(); @Output() changePage = new EventEmitter<number>();
@Input() numberOfItems: number; @Input() numberOfItems: number;
} }

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { Component, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation'; import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
@ -40,6 +40,7 @@ export class WorkbasketListComponent implements OnInit, OnDestroy {
filterBy: Filter = new Filter({ name: '', owner: '', type: '', description: '', key: '' }); filterBy: Filter = new Filter({ name: '', owner: '', type: '', description: '', key: '' });
requestInProgress: boolean; requestInProgress: boolean;
requestInProgressLocal = false; requestInProgressLocal = false;
@Input() expanded: boolean;
@ViewChild('wbToolbar', { static: true }) @ViewChild('wbToolbar', { static: true })
private toolbarElement: ElementRef; private toolbarElement: ElementRef;

View File

@ -1,11 +1,22 @@
<div class="workbasket-overview"> <div class="workbasket-overview">
<taskana-administration-workbasket-list></taskana-administration-workbasket-list>
<div class="vertical-right-divider"></div> <div class="workbasket-overview__list" #workbasketList>
<taskana-administration-workbasket-details <taskana-administration-workbasket-list [expanded]="expanded"></taskana-administration-workbasket-list>
*ngIf="showDetail; else showEmptyPage"></taskana-administration-workbasket-details> </div>
<div class="vertical-right-divider">
<span class="workbasket-overview__toggle-view-button" (click)="toggleWidth()" #toggleButton>
<mat-icon class="md-36" *ngIf="expanded">chevron_left</mat-icon>
<mat-icon class="md-36" *ngIf="!expanded">chevron_right</mat-icon>
</span>
</div>
<div class="workbasket-overview__details" *ngIf="showDetail; else showEmptyPage">
<taskana-administration-workbasket-details></taskana-administration-workbasket-details>
</div>
<ng-template #showEmptyPage> <ng-template #showEmptyPage>
<div class="hidden-xs hidden-sm col-md-8 container-no-items"> <div class="workbasket-overview__empty-page">
<div class="center-block no-detail"> <div class="center-block no-detail">
<h3 class="grey">Select a workbasket</h3> <h3 class="grey">Select a workbasket</h3>
<svg-icon class="img-responsive empty-icon" src="./assets/icons/wb-empty.svg"></svg-icon> <svg-icon class="img-responsive empty-icon" src="./assets/icons/wb-empty.svg"></svg-icon>

View File

@ -4,10 +4,33 @@
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
align-items: stretch; align-items: stretch;
} position: relative;
taskana-administration-workbasket-list {
min-width: 500px; &__toggle-view-button {
} width: 40px;
taskana-administration-workbasket-details { height: 40px;
flex-grow: 1; border-radius: 999px;
border: 1px solid #cecece;
background-color: white;
position: absolute;
cursor: pointer;
top: 50%;
left: 480px;
z-index: 10;
transition: 0.2s ease-out;
}
&__list {
min-width: 500px;
width: 500px;
transition: 0.2s ease-out;
}
&__details {
flex-grow: 1;
}
&__empty-page {
flex-grow: 1;
display: flex;
justify-content: center;
align-items: center;
}
} }

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkbasketOverviewComponent } from './workbasket-overview.component'; import { WorkbasketOverviewComponent } from './workbasket-overview.component';
import { Component, DebugElement } from '@angular/core'; import { Component, DebugElement, Input } from '@angular/core';
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store'; import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state'; import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
@ -17,6 +17,7 @@ import { StartupService } from '../../../shared/services/startup/startup.service
import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service'; import { TaskanaEngineService } from '../../../shared/services/taskana-engine/taskana-engine.service';
import { WindowRefService } from '../../../shared/services/window/window.service'; import { WindowRefService } from '../../../shared/services/window/window.service';
import { workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store'; import { workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store';
import { MatIconModule } from '@angular/material/icon';
const showDialogFn = jest.fn().mockReturnValue(true); const showDialogFn = jest.fn().mockReturnValue(true);
const NotificationServiceSpy = jest.fn().mockImplementation( const NotificationServiceSpy = jest.fn().mockImplementation(
@ -47,7 +48,9 @@ const mockActivatedRouteNoParams = {
}; };
@Component({ selector: 'taskana-administration-workbasket-list', template: '' }) @Component({ selector: 'taskana-administration-workbasket-list', template: '' })
class WorkbasketListStub {} class WorkbasketListStub {
@Input() expanded: boolean;
}
@Component({ selector: 'taskana-administration-workbasket-details', template: '' }) @Component({ selector: 'taskana-administration-workbasket-details', template: '' })
class WorkbasketDetailsStub {} class WorkbasketDetailsStub {}
@ -64,7 +67,12 @@ describe('WorkbasketOverviewComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([]), NgxsModule.forRoot([WorkbasketState])], imports: [
MatIconModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub], declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub],
providers: [ providers: [
WorkbasketService, WorkbasketService,
@ -124,7 +132,12 @@ describe('WorkbasketOverviewComponent Alternative Params ID', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([]), NgxsModule.forRoot([WorkbasketState])], imports: [
MatIconModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub], declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub],
providers: [ providers: [
WorkbasketService, WorkbasketService,
@ -162,7 +175,12 @@ describe('WorkbasketOverviewComponent No Params', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([]), NgxsModule.forRoot([WorkbasketState])], imports: [
MatIconModule,
HttpClientTestingModule,
RouterTestingModule.withRoutes([]),
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub], declarations: [WorkbasketOverviewComponent, WorkbasketListStub, WorkbasketDetailsStub, SvgIconStub],
providers: [ providers: [
WorkbasketService, WorkbasketService,

View File

@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, ElementRef, OnInit, ViewChild } from '@angular/core';
import { Select, Store } from '@ngxs/store'; import { Select, Store } from '@ngxs/store';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
@ -19,6 +19,10 @@ export class WorkbasketOverviewComponent implements OnInit {
@Select(WorkbasketSelectors.selectedWorkbasket) selectedWorkbasket$: Observable<Workbasket>; @Select(WorkbasketSelectors.selectedWorkbasket) selectedWorkbasket$: Observable<Workbasket>;
destroy$ = new Subject<void>(); destroy$ = new Subject<void>();
routerParams: any; routerParams: any;
expanded = true;
@ViewChild('workbasketList') workbasketList: ElementRef;
@ViewChild('toggleButton') toggleButton: ElementRef;
constructor(private route: ActivatedRoute, private store: Store) {} constructor(private route: ActivatedRoute, private store: Store) {}
@ -52,6 +56,20 @@ export class WorkbasketOverviewComponent implements OnInit {
}); });
} }
toggleWidth() {
if (this.workbasketList.nativeElement.offsetWidth === 250) {
this.expanded = true;
this.workbasketList.nativeElement.style.width = '500px';
this.workbasketList.nativeElement.style.minWidth = '500px';
this.toggleButton.nativeElement.style.left = '480px';
} else {
this.expanded = false;
this.workbasketList.nativeElement.style.width = '250px';
this.workbasketList.nativeElement.style.minWidth = '250px';
this.toggleButton.nativeElement.style.left = '230px';
}
}
ngOnDestroy() { ngOnDestroy() {
this.destroy$.next(); this.destroy$.next();
this.destroy$.complete(); this.destroy$.complete();

View File

@ -1,14 +1,18 @@
<div class="pagination"> <div class="pagination" #pagination>
<mat-paginator [length]="page?.totalElements" [pageIndex]="pageSelected - 1 " hidePageSize="true" [pageSize]="page?.size" (page)="changeToPage($event)" showFirstLastButtons="true"></mat-paginator> <mat-paginator class="pagination__mat-paginator" [length]="page?.totalElements" [pageIndex]="pageSelected - 1 "
<div class="pagination__go-to"> hidePageSize="true" [pageSize]="page?.size" (page)="changeToPage($event)" [showFirstLastButtons]="true"
<div class="pagination__go-to-label">Page: </div> [ngClass]="
{'pagination__mat-paginator--expanded': expanded,
'pagination__mat-paginator--collapsed': !expanded }"></mat-paginator>
<div class="pagination__go-to" *ngIf="expanded">
<div class="pagination__go-to-label">Page:</div>
<mat-form-field> <mat-form-field>
<input #inputTypeAhead matInput type="text" [matAutocomplete]="auto" [(ngModel)]="value" name="accessId" <input #inputTypeAhead matInput type="text" [matAutocomplete]="auto" [(ngModel)]="value" name="accessId"
(ngModelChange)="filter(value)"/> (ngModelChange)="filter(value)"/>
<mat-autocomplete #autoComplete autoActiveFirstOption (optionSelected)="goToPage($event.option.value)" <mat-autocomplete #autoComplete autoActiveFirstOption (optionSelected)="goToPage($event.option.value)"
#auto="matAutocomplete"> #auto="matAutocomplete">
<mat-option *ngFor="let pageNumber of filteredPages" [value]="pageNumber">{{ pageNumber }}</mat-option> <mat-option *ngFor="let pageNumber of filteredPages" [value]="pageNumber">{{ pageNumber }}</mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>

View File

@ -2,26 +2,34 @@
display: flex; display: flex;
max-width: 100%; max-width: 100%;
background-color: #fafafa; background-color: #fafafa;
} &__mat-paginator {
mat-paginator { background-color: #fafafa;
background-color: #fafafa; font-feature-settings: 'tnum';
width: 70%; font-variant-numeric: tabular-nums;
font-feature-settings: 'tnum'; &--expanded {
font-variant-numeric: tabular-nums; width: 70%;
} }
//Original &--collapsed {
.pagination__go-to { width: 100%;
margin-right: 12px; text-align: center;
display: flex; }
align-items: baseline;
mat-form-field {
width: 56px;
margin: 6px 4px 0 4px;
font-size: 12px;
} }
.pagination__go-to-label { &__go-to {
margin: 0 4px; margin-right: 12px;
font-size: 12px; display: flex;
color: rgba(0, 0, 0, 0.54); align-items: baseline;
mat-form-field {
width: 56px;
margin: 6px 4px 0 4px;
font-size: 12px;
}
.pagination__go-to-label {
margin: 0 4px;
font-size: 12px;
color: rgba(0, 0, 0, 0.54);
}
} }
} }
.hidden {
display: none;
}

View File

@ -1,4 +1,14 @@
import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, ViewChild } from '@angular/core'; import {
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
OnInit,
Output,
SimpleChanges,
ViewChild
} from '@angular/core';
import { Page } from 'app/shared/models/page'; import { Page } from 'app/shared/models/page';
import { MatPaginator } from '@angular/material/paginator'; import { MatPaginator } from '@angular/material/paginator';
@ -17,12 +27,17 @@ export class PaginationComponent implements OnInit, OnChanges {
@Input() @Input()
numberOfItems: number; numberOfItems: number;
@Input()
expanded: boolean = true;
@Output() @Output()
changePage = new EventEmitter<number>(); changePage = new EventEmitter<number>();
@ViewChild(MatPaginator, { static: true }) @ViewChild(MatPaginator, { static: true })
paginator: MatPaginator; paginator: MatPaginator;
@ViewChild('pagination') paginationWrapper: ElementRef;
hasItems = true; hasItems = true;
pageSelected = 1; pageSelected = 1;
pageNumbers: number[]; pageNumbers: number[];
@ -30,6 +45,32 @@ export class PaginationComponent implements OnInit, OnChanges {
value: number; value: number;
ngOnInit() { ngOnInit() {
this.changeLabel();
this.value = 1;
}
ngOnChanges(changes: SimpleChanges): void {
const rangeLabel = this.paginationWrapper?.nativeElement?.querySelector('.mat-paginator-range-label');
const container = this.paginationWrapper?.nativeElement?.querySelector('.mat-paginator-container');
if (rangeLabel && container) {
if (!this.expanded) {
container.style.justifyContent = 'center';
rangeLabel.style.display = 'none';
} else {
container.style.justifyContent = 'flex-end';
rangeLabel.style.display = 'block';
}
}
if (changes.page && changes.page.currentValue) {
this.pageSelected = changes.page.currentValue.number;
}
this.hasItems = this.numberOfItems > 0;
if (changes.page) {
this.updateGoto();
}
}
changeLabel() {
// Custom label: EG. "1-7 of 21 workbaskets" // Custom label: EG. "1-7 of 21 workbaskets"
// return `${start} - ${end} of ${length} workbaskets`; // return `${start} - ${end} of ${length} workbaskets`;
@ -44,16 +85,6 @@ export class PaginationComponent implements OnInit, OnChanges {
return `${start} - ${end} of ${length}`; return `${start} - ${end} of ${length}`;
} }
}; };
this.value = 1;
}
ngOnChanges(changes: SimpleChanges): void {
if (changes.page && changes.page.currentValue) {
this.pageSelected = changes.page.currentValue.number;
}
this.hasItems = this.numberOfItems > 0;
if (changes.page) {
this.updateGoto();
}
} }
changeToPage(event) { changeToPage(event) {