TSK-1760: Started Rework of store for workbasketAccessItems

Now uses a virtually scrolling list and pages DistributionTargets
This commit is contained in:
Tristan 2022-01-03 20:14:34 +01:00 committed by Tristan2357
parent 490a76cf4b
commit 0eb85565bc
23 changed files with 1257 additions and 637 deletions

View File

@ -39,6 +39,7 @@
"chart.js": "2.9.4",
"core-js": "3.18.3",
"file-saver": "2.0.5",
"lodash": "^4.5.0",
"ng2-charts": "2.4.3",
"ngx-bootstrap": "7.1.0",
"ngx-infinite-scroll": "10.0.1",
@ -60,6 +61,7 @@
"@typescript-eslint/parser": "5.1.0",
"compression-webpack-plugin": "9.0.0",
"cypress": "7.7.0",
"@types/lodash": "^4.14.178",
"cypress-intellij-reporter": "0.0.6",
"eslint": "8.0.1",
"eslint-config-prettier": "8.3.0",

View File

@ -56,6 +56,7 @@ import { MatRippleModule } from '@angular/material/core';
import { MatTableModule } from '@angular/material/table';
import { MatDialogModule } from '@angular/material/dialog';
import { MatExpansionModule } from '@angular/material/expansion';
import { ScrollingModule } from '@angular/cdk/scrolling';
const MODULES = [
CommonModule,
@ -66,7 +67,8 @@ const MODULES = [
SharedModule,
AdministrationRoutingModule,
TypeaheadModule,
InfiniteScrollModule
InfiniteScrollModule,
ScrollingModule
];
const DECLARATIONS = [

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { ImportExportComponent } from './import-export.component';
import { StartupService } from '../../../shared/services/startup/startup.service';
@ -11,9 +11,7 @@ import { ImportExportService } from '../../services/import-export.service';
import { HttpClient } from '@angular/common/http';
import { of } from 'rxjs';
import { ClassificationDefinitionService } from '../../services/classification-definition.service';
import { take } from 'rxjs/operators';
import { TaskanaType } from '../../../shared/models/taskana-type';
import { BlobGenerator } from '../../../shared/util/blob-generator';
jest.mock('../../../shared/util/blob-generator');
@ -45,31 +43,33 @@ xdescribe('ImportExportComponent', () => {
})
);
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [ImportExportComponent],
providers: [
StartupService,
TaskanaEngineService,
WindowRefService,
WorkbasketDefinitionService,
ClassificationDefinitionService,
ImportExportService,
{ provide: DomainService, useClass: domainServiceSpy },
{ provide: NotificationService, useClass: notificationServiceSpy },
{ provide: HttpClient, useClass: httpSpy }
]
}).compileComponents();
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [ImportExportComponent],
providers: [
StartupService,
TaskanaEngineService,
WindowRefService,
WorkbasketDefinitionService,
ClassificationDefinitionService,
ImportExportService,
{ provide: DomainService, useClass: domainServiceSpy },
{ provide: NotificationService, useClass: notificationServiceSpy },
{ provide: HttpClient, useClass: httpSpy }
]
}).compileComponents();
jest.clearAllMocks();
jest.clearAllMocks();
fixture = TestBed.createComponent(ImportExportComponent);
debugElement = fixture.debugElement;
app = fixture.debugElement.componentInstance;
app.currentSelection = TaskanaType.WORKBASKETS;
fixture.detectChanges();
}));
fixture = TestBed.createComponent(ImportExportComponent);
debugElement = fixture.debugElement;
app = fixture.debugElement.componentInstance;
app.currentSelection = TaskanaType.WORKBASKETS;
fixture.detectChanges();
})
);
it('should create component', () => {
expect(app).toBeTruthy();

View File

@ -56,7 +56,7 @@
</taskana-administration-workbasket-access-items>
</mat-tab>
<mat-tab label="Distribution Targets">
<taskana-administration-workbasket-distribution-targets [workbasket]="workbasket">
<taskana-administration-workbasket-distribution-targets >
</taskana-administration-workbasket-distribution-targets>
</mat-tab>
</mat-tab-group>

View File

@ -1,9 +1,10 @@
<div id="dual-list-Left" class="distribution-targets-list">
<div class="distribution-targets-list" id="dual-list-Left">
<mat-toolbar>
<span class="distribution-targets-list__header" matTooltip="{{header}}">{{header}}</span>
<!-- FILTER BUTTON -->
<button mat-flat-button class="distribution-targets-list__action-button" (click)="changeToolbarState(!toolbarState)"
<button (click)="changeToolbarState(!toolbarState)"
class="distribution-targets-list__action-button" mat-flat-button
>
<span *ngIf="!toolbarState">
Display filter
@ -18,30 +19,53 @@
<span style="flex: 1 1 auto"> </span>
<!-- SELECT ALL BUTTON -->
<button mat-flat-button class="distribution-targets-list__action-button" (click)="selectAll(!allSelected);">
<mat-icon class="button-icon" *ngIf="allSelected" matTooltip="Deselect all items">check_box</mat-icon>
<mat-icon class="button-icon" *ngIf="!allSelected" matTooltip="Select all items">check_box_outline_blank</mat-icon>
<button (click)="selectAll(!allSelected);" class="distribution-targets-list__action-button"
mat-flat-button>
<mat-icon *ngIf="allSelected" class="button-icon" matTooltip="Deselect all items">check_box
</mat-icon>
<mat-icon *ngIf="!allSelected" class="button-icon" matTooltip="Select all items">
check_box_outline_blank
</mat-icon>
</button>
</mat-toolbar>
<!-- FILTER COMPONENT -->
<taskana-shared-workbasket-filter *ngIf="toolbarState" isExpanded="true" [component]="component"></taskana-shared-workbasket-filter>
<taskana-shared-workbasket-filter *ngIf="toolbarState" [component]="component"
isExpanded="true"></taskana-shared-workbasket-filter>
<!-- EMPTY LIST -->
<div *ngIf="distributionTargets?.length == 0" class="distribution-targets-list__empty-list">
<!-- AVAILABLE SIDE -->
<div *ngIf="side === 0" style="padding: 0 16px;">
There are currently no Workbaskets for distribution
</div>
<!-- SELECTED SIDE -->
<div *ngIf="side === 1" style="padding: 0 16px;">
There is currently no distributed Workbasket
</div>
</div>
<!-- WORKBASKET LIST -->
<div class="{{toolbarState? 'distribution-targets-list__list--with-filter' : 'distribution-targets-list__list--no-filter'}}"
infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50"
[scrollWindow]="false" *ngIf="distributionTargets?.length > 0">
<mat-selection-list #workbasket [multiple]="true">
<mat-list-option class="workbasket-distribution-targets__workbaskets-item"
*ngFor="let workbasket of distributionTargets | orderBy: ['type', 'name']"
[selected]="workbasket.selected"
(click)="workbasket.selected = !workbasket.selected"
[value]="workbasket.workbasketId">
<mat-selection-list #workbasket [multiple]="true">
<cdk-virtual-scroll-viewport #scroller
class="{{toolbarState? 'distribution-targets-list__list--with-filter' : 'distribution-targets-list__list--no-filter'}}"
itemSize="90">
<mat-list-option
*cdkVirtualFor="let workbasket of distributionTargets| orderBy: ['type', 'name']; templateCacheSize: 0"
(click)="updateSelectAll(!workbasket.selected) && (workbasket.selected = !workbasket.selected)"
[selected]="workbasket.selected"
[value]="workbasket.workbasketId"
class="workbasket-distribution-targets__workbaskets-item">
<div class="distribution-targets-list__item-wrapper">
<!-- ICON -->
<div class="distribution-targets-list__item-icon">
<taskana-administration-icon-type [type]="workbasket.type" size="large" tooltip="true"></taskana-administration-icon-type>
<taskana-administration-icon-type [type]="workbasket.type" size="large"
tooltip="true"></taskana-administration-icon-type>
</div>
<!-- INFO -->
@ -54,32 +78,19 @@
</div>
<!-- MARKED FOR DELETION -->
<div class="workbaskets-item__marked" *ngIf="workbasket.markedForDeletion">
<span title="Marked for deletion" matTooltip="Marked for deletion"
class="material-icons md-20 {{workbasket.workbasketId === selectedId ? 'white': 'red' }} ">error</span>
<div *ngIf="workbasket.markedForDeletion" class="workbaskets-item__marked">
<span
class="material-icons md-20 red "
matTooltip="Marked for deletion"
title="Marked for deletion">error</span>
</div>
</div>
<mat-divider></mat-divider>
</mat-list-option>
</mat-selection-list>
</div>
<!-- EMPTY LIST -->
<div class="distribution-targets-list__empty-list" *ngIf="distributionTargets?.length == 0">
<!-- AVAILABLE SIDE -->
<div *ngIf="side === 0" style="padding: 0 16px;">
There are currently no Workbaskets for distribution
</div>
<!-- SELECTED SIDE -->
<div *ngIf="side === 1" style="padding: 0 16px;">
There is currently no distributed Workbasket
</div>
</div>
</cdk-virtual-scroll-viewport>
</mat-selection-list>
</div>

View File

@ -1,15 +1,24 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, flush, TestBed, waitForAsync } from '@angular/core/testing';
import { Component, DebugElement, Input, Pipe, PipeTransform } from '@angular/core';
import { WorkbasketDistributionTargetsListComponent } from './workbasket-distribution-targets-list.component';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { WorkbasketType } from '../../../shared/models/workbasket-type';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store';
import { Side } from '../workbasket-distribution-targets/workbasket-distribution-targets.component';
import { engineConfigurationMock, workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { Side } from '../../models/workbasket-distribution-enums';
import { NgxsModule, Store } from '@ngxs/store';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
import { animationFrameScheduler, EMPTY, of } from 'rxjs';
import { HttpClient } from '@angular/common/http';
import { DomainService } from '../../../shared/services/domain/domain.service';
import { MatDialogModule } from '@angular/material/dialog';
import { ActivatedRoute } from '@angular/router';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { ScrollingModule } from '@angular/cdk/scrolling';
@Component({ selector: 'taskana-shared-workbasket-filter', template: '' })
class FilterStub {
@ -38,6 +47,31 @@ describe('WorkbasketDistributionTargetsListComponent', () => {
let fixture: ComponentFixture<WorkbasketDistributionTargetsListComponent>;
let debugElement: DebugElement;
let component: WorkbasketDistributionTargetsListComponent;
let store: Store;
const routeParamsMock = { id: 'workbasket' };
const activatedRouteMock = {
firstChild: {
params: of(routeParamsMock)
}
};
const httpSpy = jest.fn().mockImplementation(
(): Partial<HttpClient> => ({
get: jest.fn().mockReturnValue(of([])),
post: jest.fn().mockReturnValue(of([]))
})
);
const domainServiceSpy: Partial<DomainService> = {
getSelectedDomainValue: jest.fn().mockReturnValue(of(null)),
getSelectedDomain: jest.fn().mockReturnValue(of('A')),
getDomains: jest.fn().mockReturnValue(of(null))
};
const requestInProgressServiceSpy: Partial<RequestInProgressService> = {
setRequestInProgress: jest.fn().mockReturnValue(of(null))
};
beforeEach(
waitForAsync(() => {
@ -46,12 +80,23 @@ describe('WorkbasketDistributionTargetsListComponent', () => {
MatIconModule,
MatToolbarModule,
MatListModule,
MatDialogModule,
MatTooltipModule,
InfiniteScrollModule,
BrowserAnimationsModule
ScrollingModule,
BrowserAnimationsModule,
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketDistributionTargetsListComponent, FilterStub, SpinnerStub, IconTypeStub, OrderByMock],
providers: []
providers: [
{ provide: HttpClient, useValue: httpSpy },
{
provide: DomainService,
useValue: domainServiceSpy
},
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: RequestInProgressService, useValue: requestInProgressServiceSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(WorkbasketDistributionTargetsListComponent);
@ -59,6 +104,13 @@ describe('WorkbasketDistributionTargetsListComponent', () => {
component = fixture.componentInstance;
component.distributionTargets = workbasketReadStateMock.paginatedWorkbasketsSummary.workbaskets;
component.side = Side.AVAILABLE;
component.transferDistributionTargetObservable = EMPTY;
store = TestBed.inject(Store);
store.reset({
...store.snapshot(),
engineConfiguration: engineConfigurationMock,
workbasket: workbasketReadStateMock
});
})
);
@ -83,13 +135,20 @@ describe('WorkbasketDistributionTargetsListComponent', () => {
expect(debugElement.nativeElement.querySelector('taskana-shared-workbasket-filter')).toBeTruthy();
});
it('should display all available workbaskets', () => {
it('should display all available workbaskets', fakeAsync(() => {
// On the first cycle we render the items.
fixture.detectChanges();
flush();
// Flush the initial fake scroll event.
animationFrameScheduler.flush();
flush();
fixture.detectChanges();
const distributionTargetList = debugElement.nativeElement.getElementsByClassName(
'workbasket-distribution-targets__workbaskets-item'
);
expect(distributionTargetList).toHaveLength(5);
});
expect(distributionTargetList).toHaveLength(3);
}));
it('should call orderBy pipe', () => {
const orderBySpy = jest.spyOn(OrderByMock.prototype, 'transform');

View File

@ -1,18 +1,32 @@
import {
AfterContentChecked,
AfterViewInit,
ChangeDetectorRef,
Component,
Input,
AfterContentChecked,
ChangeDetectorRef,
ViewChild,
Output,
EventEmitter,
OnChanges,
SimpleChanges
OnInit,
SimpleChanges,
ViewChild
} from '@angular/core';
import { isEqual } from 'lodash';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { expandDown } from 'app/shared/animations/expand.animation';
import { AllSelected, Side } from '../workbasket-distribution-targets/workbasket-distribution-targets.component';
import { MatSelectionList } from '@angular/material/list';
import { CdkVirtualScrollViewport } from '@angular/cdk/scrolling';
import { Side } from '../../models/workbasket-distribution-enums';
import { Select, Store } from '@ngxs/store';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { filter, map, pairwise, take, takeUntil, throttleTime } from 'rxjs/operators';
import {
FetchAvailableDistributionTargets,
FetchWorkbasketDistributionTargets,
TransferDistributionTargets
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { Observable, Subject } from 'rxjs';
import { WorkbasketQueryFilterParameter } from '../../../shared/models/workbasket-query-filter-parameter';
import { FilterSelectors } from '../../../shared/store/filter-store/filter.selectors';
import { WorkbasketDistributionTarget } from '../../../shared/models/workbasket-distribution-target';
@Component({
selector: 'taskana-administration-workbasket-distribution-targets-list',
@ -20,19 +34,68 @@ import { MatSelectionList } from '@angular/material/list';
styleUrls: ['./workbasket-distribution-targets-list.component.scss'],
animations: [expandDown]
})
export class WorkbasketDistributionTargetsListComponent implements AfterContentChecked, OnChanges {
@Input() distributionTargets: WorkbasketSummary[];
export class WorkbasketDistributionTargetsListComponent
implements AfterContentChecked, OnChanges, OnInit, AfterViewInit
{
@Input() side: Side;
@Input() header: string;
@Input() allSelected;
allSelected;
@Input() component;
@Input() transferDistributionTargetObservable: Observable<Side>;
@Output() allSelectedEmitter = new EventEmitter<AllSelected>();
@Select(WorkbasketSelectors.workbasketDistributionTargets)
workbasketDistributionTargets$: Observable<WorkbasketSummary[]>;
@Select(WorkbasketSelectors.availableDistributionTargets)
availableDistributionTargets$: Observable<WorkbasketSummary[]>;
@Select(FilterSelectors.getAvailableDistributionTargetsFilter)
availableDistributionTargetsFilter$: Observable<WorkbasketQueryFilterParameter>;
@Select(FilterSelectors.getSelectedDistributionTargetsFilter)
selectedDistributionTargetsFilter$: Observable<WorkbasketQueryFilterParameter>;
toolbarState = false;
@ViewChild('workbasket') distributionTargetsList: MatSelectionList;
constructor(private changeDetector: ChangeDetectorRef) {}
distributionTargets: WorkbasketDistributionTarget[];
@ViewChild('workbasket') distributionTargetsList: MatSelectionList;
@ViewChild('scroller') workbasketList: CdkVirtualScrollViewport;
private destroy$ = new Subject<void>();
private filter: WorkbasketQueryFilterParameter;
private allSelectedDiff = 0;
constructor(private changeDetector: ChangeDetectorRef, private store: Store) {}
ngOnInit(): void {
if (this.side === Side.AVAILABLE) {
this.availableDistributionTargets$.pipe(takeUntil(this.destroy$)).subscribe((wbs) => {
this.distributionTargets = wbs.map((wb) => {
return { ...wb, selected: this.allSelected };
});
});
this.availableDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
this.filter = filter;
this.store.dispatch(new FetchAvailableDistributionTargets(true, this.filter));
this.selectAll(false);
});
} else {
this.workbasketDistributionTargets$.pipe().subscribe((wbs) => {
this.distributionTargets = wbs.map((wb) => {
return { ...wb };
});
});
this.selectedDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
if (isEqual(this.filter, filter)) return;
this.filter = filter;
this.store.dispatch(new FetchWorkbasketDistributionTargets(true, this.filter));
this.selectAll(false);
});
}
this.transferDistributionTargetObservable.subscribe((targetSide) => {
if (targetSide !== this.side) this.transferDistributionTargets(targetSide);
});
}
ngAfterContentChecked(): void {
this.changeDetector.detectChanges();
@ -44,17 +107,63 @@ export class WorkbasketDistributionTargetsListComponent implements AfterContentC
}
}
ngAfterViewInit() {
this.workbasketList
.elementScrolled()
.pipe(
map(() => this.workbasketList.measureScrollOffset('bottom')),
pairwise(),
filter(([y1, y2]) => y2 < y1 && y2 < 270),
throttleTime(200)
)
.subscribe(() => {
if (this.side === Side.AVAILABLE) {
this.store.dispatch(new FetchAvailableDistributionTargets(false, this.filter));
} else {
this.store.dispatch(new FetchWorkbasketDistributionTargets(false, this.filter));
}
});
}
selectAll(selected: boolean) {
if (typeof this.distributionTargetsList !== 'undefined') {
this.allSelected = selected;
this.distributionTargetsList.options.map((item) => (item['selected'] = selected));
this.distributionTargets.map((item) => (item['selected'] = selected));
this.allSelectedEmitter.emit({ value: this.allSelected, side: this.side });
this.distributionTargets.map((wb) => (wb.selected = selected));
if (selected) this.allSelectedDiff = this.distributionTargets.length;
else this.allSelectedDiff = 0;
}
}
transferDistributionTargets(targetSide: Side) {
let selectedWBs = this.distributionTargets.filter((item: any) => item.selected === true);
this.distributionTargets.forEach((wb) => (wb.selected = false));
this.store
.dispatch(new TransferDistributionTargets(targetSide, selectedWBs))
.pipe(take(1))
.subscribe(() => {
if (this.distributionTargets.length === 0) {
const desiredAction =
targetSide === Side.SELECTED
? new FetchAvailableDistributionTargets(false, this.filter)
: new FetchWorkbasketDistributionTargets(false, this.filter);
this.store.dispatch(desiredAction);
}
});
}
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
updateSelectAll(selected: boolean) {
if (selected) this.allSelectedDiff++;
else this.allSelectedDiff--;
this.allSelected = this.allSelectedDiff === this.distributionTargets.length;
return true;
}
ngOnDestroy(): void {
this.destroy$.next();
this.destroy$.complete();
}
}

View File

@ -1,4 +1,4 @@
<div *ngIf="workbasket" id="wb-information" class="workbasket-distribution-targets">
<div id="wb-information" class="workbasket-distribution-targets">
<mat-toolbar class="distribution-targets-list__action-toolbar" >
<button mat-flat-button class="distribution-targets-list__action-button distribution-targets-list__toggle-view-button"
*ngIf="!sideBySide" (click)="toggleSideBySideView()">
@ -18,7 +18,7 @@
style="justify-content: flex-end; margin-right: 2%">
<button mat-flat-button color="accent"
class="distribution-targets-list__action-button distribution-targets-list-dialog__add-button"
(click)="moveDistributionTargets(side.AVAILABLE)">
(click)="moveDistributionTargets(sideEnum.SELECTED)">
Add selected distribution targets
<mat-icon>add</mat-icon>
</button>
@ -30,7 +30,7 @@
style="justify-content: flex-end;">
<button mat-flat-button color="warn"
class="distribution-targets-list__action-button distribution-targets-list-dialog__remove-button"
(click)="moveDistributionTargets(side.SELECTED)">
(click)="moveDistributionTargets(sideEnum.AVAILABLE)">
Remove selected distribution target
<mat-icon>remove</mat-icon>
</button>
@ -43,7 +43,7 @@
*ngIf="!displayingDistributionTargetsPicker && !sideBySide">
<button mat-flat-button color="warn"
class="distribution-targets-list__action-button distribution-targets-list-dialog__remove-button"
(click)="moveDistributionTargets(side.SELECTED)">
(click)="moveDistributionTargets(sideEnum.AVAILABLE)">
Remove selected distribution target
<mat-icon>remove</mat-icon>
</button>
@ -63,7 +63,7 @@
*ngIf="displayingDistributionTargetsPicker && !sideBySide">
<button mat-flat-button color="accent"
class="distribution-targets-list__action-button distribution-targets-list-dialog__add-button"
(click)="moveDistributionTargets(side.AVAILABLE)">
(click)="moveDistributionTargets(sideEnum.SELECTED)">
Add selected distribution targets
<mat-icon>add</mat-icon>
</button>
@ -86,23 +86,19 @@
<taskana-administration-workbasket-distribution-targets-list
[ngClass]="sideBySide ? 'distribution-targets-list__lists--left-side' : ''"
header="Available distribution targets"
[distributionTargets]="availableDistributionTargets"
[side]="side.AVAILABLE"
[allSelected]="selectAllLeft"
[side]="sideEnum.AVAILABLE"
*ngIf="displayingDistributionTargetsPicker"
[component]="'availableDistributionTargets'"
(allSelectedEmitter)="setAllSelected($event)"
[transferDistributionTargetObservable]="transferDistributionTargetObservable"
>
</taskana-administration-workbasket-distribution-targets-list>
<taskana-administration-workbasket-distribution-targets-list
header="Selected distribution targets"
[distributionTargets]="selectedDistributionTargets"
[side]="side.SELECTED"
[allSelected]="selectAllRight"
[side]="sideEnum.SELECTED"
[hidden]="displayingDistributionTargetsPicker && !sideBySide"
[component]="'selectedDistributionTargets'"
(allSelectedEmitter)="setAllSelected($event)"
[transferDistributionTargetObservable]="transferDistributionTargetObservable"
>
</taskana-administration-workbasket-distribution-targets-list>
</div>

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { Component, DebugElement, Input } from '@angular/core';
import { Side, WorkbasketDistributionTargetsComponent } from './workbasket-distribution-targets.component';
import { WorkbasketDistributionTargetsComponent } from './workbasket-distribution-targets.component';
import { WorkbasketSummary } from '../../../shared/models/workbasket-summary';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
@ -13,12 +13,9 @@ import { WorkbasketState } from '../../../shared/store/workbasket-store/workbask
import { ActivatedRoute } from '@angular/router';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { MatDialogModule } from '@angular/material/dialog';
import {
engineConfigurationMock,
selectedWorkbasketMock,
workbasketReadStateMock
} from '../../../shared/store/mock-data/mock-store';
import { engineConfigurationMock, workbasketReadStateMock } from '../../../shared/store/mock-data/mock-store';
import { DomainService } from '../../../shared/services/domain/domain.service';
import { Side } from '../../models/workbasket-distribution-enums';
const routeParamsMock = { id: 'workbasket' };
const activatedRouteMock = {
@ -26,6 +23,7 @@ const activatedRouteMock = {
params: of(routeParamsMock)
}
};
@Component({ selector: 'taskana-administration-workbasket-distribution-targets-list', template: '' })
class WorkbasketDistributionTargetsListStub {
@Input() distributionTargets: WorkbasketSummary[];
@ -36,21 +34,21 @@ class WorkbasketDistributionTargetsListStub {
}
const domainServiceSpy: Partial<DomainService> = {
getSelectedDomainValue: jest.fn().mockReturnValue(of()),
getSelectedDomainValue: jest.fn().mockReturnValue(of(null)),
getSelectedDomain: jest.fn().mockReturnValue(of('A')),
getDomains: jest.fn().mockReturnValue(of())
getDomains: jest.fn().mockReturnValue(of(null))
};
const workbasketServiceSpy: Partial<WorkbasketService> = {
getWorkBasketsSummary: jest.fn().mockReturnValue(of()),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of())
getWorkBasketsSummary: jest.fn().mockReturnValue(of(null)),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of(null))
};
const notificationsServiceSpy: Partial<NotificationService> = {
showSuccess: jest.fn().mockReturnValue(true)
};
const requestInProgressServiceSpy: Partial<RequestInProgressService> = {
setRequestInProgress: jest.fn().mockReturnValue(of())
setRequestInProgress: jest.fn().mockReturnValue(of(null))
};
describe('WorkbasketDistributionTargetsComponent', () => {
@ -60,38 +58,39 @@ describe('WorkbasketDistributionTargetsComponent', () => {
let store: Store;
let actions$: Observable<any>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
MatIconModule,
MatDialogModule,
MatToolbarModule,
MatButtonModule,
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketDistributionTargetsComponent, WorkbasketDistributionTargetsListStub],
providers: [
{ provide: WorkbasketService, useValue: workbasketServiceSpy },
{ provide: NotificationService, useValue: notificationsServiceSpy },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: RequestInProgressService, useValue: requestInProgressServiceSpy },
{ provide: DomainService, useValue: domainServiceSpy }
]
}).compileComponents();
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
MatIconModule,
MatDialogModule,
MatToolbarModule,
MatButtonModule,
NgxsModule.forRoot([WorkbasketState])
],
declarations: [WorkbasketDistributionTargetsComponent, WorkbasketDistributionTargetsListStub],
providers: [
{ provide: WorkbasketService, useValue: workbasketServiceSpy },
{ provide: NotificationService, useValue: notificationsServiceSpy },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: RequestInProgressService, useValue: requestInProgressServiceSpy },
{ provide: DomainService, useValue: domainServiceSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(WorkbasketDistributionTargetsComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
engineConfiguration: engineConfigurationMock,
workbasket: workbasketReadStateMock
});
component.workbasket = selectedWorkbasketMock;
fixture.detectChanges();
}));
fixture = TestBed.createComponent(WorkbasketDistributionTargetsComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
engineConfiguration: engineConfigurationMock,
workbasket: workbasketReadStateMock
});
fixture.detectChanges();
})
);
it('should create component', () => {
expect(component).toBeTruthy();
@ -110,72 +109,4 @@ describe('WorkbasketDistributionTargetsComponent', () => {
expect(component.sideBySide).toBe(false);
expect(debugElement.nativeElement.querySelector('.distribution-targets-list__lists--side')).toBeFalsy();
});
it('should get available and selected distribution targets', () => {
// mock-data has 8 entries, array should be filtered by selected distribution targets
expect(component.availableDistributionTargets).toHaveLength(5);
expect(component.availableDistributionTargetsUndoClone).toHaveLength(5);
expect(component.availableDistributionTargetsFilterClone).toHaveLength(5);
// mock-data has 3 entries
expect(component.selectedDistributionTargets).toHaveLength(3);
expect(component.selectedDistributionTargetsUndoClone).toHaveLength(3);
expect(component.selectedDistributionTargetsFilterClone).toHaveLength(3);
});
it('should move distribution targets to selected list', () => {
component.availableDistributionTargets[0]['selected'] = true; // select first item in available array
const removeSelectedItems = jest.spyOn(component, 'removeSelectedItems');
component.moveDistributionTargets(Side.AVAILABLE);
expect(component.selectedDistributionTargets).toHaveLength(4); // mock-data only has 3
expect(component.selectedDistributionTargetsFilterClone).toHaveLength(4);
expect(removeSelectedItems).toHaveBeenCalled();
});
it('should move distribution targets to available list', () => {
component.selectedDistributionTargets[0]['selected'] = true; // select first item in available array
const removeSelectedItems = jest.spyOn(component, 'removeSelectedItems');
component.moveDistributionTargets(Side.SELECTED);
expect(component.availableDistributionTargets).toHaveLength(6); // mock-data has 5
expect(component.availableDistributionTargetsFilterClone).toHaveLength(6);
expect(removeSelectedItems).toHaveBeenCalled();
});
it('should set selectAll checkboxes to false when moving a workbasket', () => {
component.selectAllRight = true;
component.moveDistributionTargets(Side.SELECTED);
expect(component.selectAllRight).toBeFalsy();
component.selectAllLeft = true;
component.moveDistributionTargets(Side.AVAILABLE);
expect(component.selectAllLeft).toBeFalsy();
});
it('should call unselectItems() when moving a workbasket', () => {
const unselectItems = jest.spyOn(component, 'unselectItems');
[Side.SELECTED, Side.AVAILABLE].forEach((side) => {
component.moveDistributionTargets(side);
expect(unselectItems).toHaveBeenCalled();
});
});
it('should reset distribution targets to last state when undo is called', () => {
component.availableDistributionTargets[0]['selected'] = true; // select first item in available array
component.moveDistributionTargets(Side.AVAILABLE);
expect(component.selectedDistributionTargets).toHaveLength(4); // mock-data only has 3
component.onClear();
expect(component.selectedDistributionTargets).toHaveLength(3);
expect(component.selectedDistributionTargetsFilterClone).toHaveLength(3);
});
it('should call performFilter when filter value from store is obtained', () => {
const performFilter = jest.spyOn(component, 'performFilter');
component.ngOnInit();
expect(performFilter).toHaveBeenCalledTimes(2);
});
});

View File

@ -1,37 +1,22 @@
import { Component, Input, OnDestroy, OnInit, SimpleChanges } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Component, OnDestroy, OnInit } from '@angular/core';
import { forkJoin, Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { WorkbasketSummaryRepresentation } from 'app/shared/models/workbasket-summary-representation';
import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-distribution-targets';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { Actions, ofActionCompleted, Select, Store } from '@ngxs/store';
import { filter, take, takeUntil } from 'rxjs/operators';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import {
GetAvailableDistributionTargets,
GetWorkbasketDistributionTargets,
FetchAvailableDistributionTargets,
FetchWorkbasketDistributionTargets,
SaveNewWorkbasket,
UpdateWorkbasket,
UpdateWorkbasketDistributionTargets
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { ButtonAction } from '../../models/button-action';
import { Pair } from '../../../shared/models/pair';
import { WorkbasketQueryFilterParameter } from '../../../shared/models/workbasket-query-filter-parameter';
import { FilterSelectors } from '../../../shared/store/filter-store/filter.selectors';
import { SetWorkbasketFilter } from '../../../shared/store/filter-store/filter.actions';
export enum Side {
AVAILABLE,
SELECTED
}
export interface AllSelected {
value: boolean;
side?: Side;
}
import { Side } from '../../models/workbasket-distribution-enums';
import { ClearWorkbasketFilter } from '../../../shared/store/filter-store/filter.actions';
@Component({
selector: 'taskana-administration-workbasket-distribution-targets',
@ -39,33 +24,11 @@ export interface AllSelected {
styleUrls: ['./workbasket-distribution-targets.component.scss']
})
export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy {
@Input()
workbasket: Workbasket;
toolbarState = false;
sideEnum = Side;
sideBySide = true;
displayingDistributionTargetsPicker = true;
side = Side;
selectAllRight = false;
selectAllLeft = false;
availableDistributionTargets: WorkbasketSummary[] = [];
availableDistributionTargetsUndoClone: WorkbasketSummary[];
availableDistributionTargetsFilterClone: WorkbasketSummary[] = [];
availableDistributionTargetsFilter: WorkbasketQueryFilterParameter;
selectedDistributionTargets: WorkbasketSummary[];
selectedDistributionTargetsUndoClone: WorkbasketSummary[];
selectedDistributionTargetsFilterClone: WorkbasketSummary[] = [];
selectedDistributionTargetsResource: WorkbasketDistributionTargets;
selectedDistributionTargetsFilter: WorkbasketQueryFilterParameter;
@Select(WorkbasketSelectors.workbasketDistributionTargets)
workbasketDistributionTargets$: Observable<WorkbasketDistributionTargets>;
@Select(WorkbasketSelectors.availableDistributionTargets)
availableDistributionTargets$: Observable<WorkbasketSummary[]>;
transferDistributionTargetObservable = new Subject<Side>();
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
@ -73,61 +36,16 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
@Select(WorkbasketSelectors.selectedWorkbasket)
selectedWorkbasket$: Observable<Workbasket>;
@Select(FilterSelectors.getAvailableDistributionTargetsFilter)
availableDistributionTargetsFilter$: Observable<WorkbasketQueryFilterParameter>;
@Select(FilterSelectors.getSelectedDistributionTargetsFilter)
selectedDistributionTargetsFilter$: Observable<WorkbasketQueryFilterParameter>;
destroy$ = new Subject<void>();
private selectedWorkbasket: WorkbasketSummary;
constructor(
private workbasketService: WorkbasketService,
private notificationsService: NotificationService,
private store: Store,
private ngxsActions$: Actions
) {}
constructor(private notificationsService: NotificationService, private store: Store, private ngxsActions$: Actions) {}
/**
* Rework with modification based on old components,
* would be ideal to completely redo whole components using drag and drop angular components and clearer logics
*/
ngOnInit() {
if (this.workbasket?.workbasketId) {
this.store.dispatch(new GetWorkbasketDistributionTargets(this.workbasket._links.distributionTargets.href));
this.store.dispatch(new GetAvailableDistributionTargets());
}
this.workbasketDistributionTargets$.pipe(takeUntil(this.destroy$)).subscribe((workbasketDistributionTargets) => {
if (typeof workbasketDistributionTargets !== 'undefined') {
this.selectedDistributionTargetsResource = { ...workbasketDistributionTargets };
this.selectedDistributionTargets = [];
workbasketDistributionTargets.distributionTargets.forEach((distributionTarget) => {
const target = {};
Object.keys(distributionTarget).forEach((key) => {
target[key] = distributionTarget[key];
});
this.selectedDistributionTargets.push(target);
});
this.selectedDistributionTargetsFilterClone = [...this.selectedDistributionTargets];
this.selectedDistributionTargetsUndoClone = [...this.selectedDistributionTargets];
this.getAvailableDistributionTargets();
}
});
this.availableDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
this.availableDistributionTargetsFilter = filter;
this.performFilter({ left: 0, right: filter });
});
this.selectedDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
this.selectedDistributionTargetsFilter = filter;
this.performFilter({ left: 1, right: filter });
});
// saving workbasket distributions targets when existing workbasket was modified
this.ngxsActions$.pipe(ofActionCompleted(UpdateWorkbasket), takeUntil(this.destroy$)).subscribe(() => {
this.onSave();
@ -135,14 +53,21 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
// saving workbasket distributions targets when workbasket was copied or created
this.ngxsActions$.pipe(ofActionCompleted(SaveNewWorkbasket), takeUntil(this.destroy$)).subscribe(() => {
this.selectedWorkbasket$.pipe(take(1)).subscribe((workbasket) => {
this.selectedDistributionTargetsResource._links = {
self: { href: workbasket._links.distributionTargets.href }
};
this.selectedWorkbasket$.pipe(take(1)).subscribe(() => {
this.onSave();
});
});
this.selectedWorkbasket$.pipe(takeUntil(this.destroy$)).subscribe((wb) => {
if (wb !== undefined && wb.workbasketId !== this.selectedWorkbasket?.workbasketId) {
if (this.selectedWorkbasket?.workbasketId) {
this.store.dispatch(new FetchWorkbasketDistributionTargets(true));
this.store.dispatch(new FetchAvailableDistributionTargets(true));
}
this.selectedWorkbasket = wb;
}
});
this.buttonAction$
.pipe(takeUntil(this.destroy$))
.pipe(filter((buttonAction) => typeof buttonAction !== 'undefined'))
@ -157,173 +82,27 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.workbasket.currentValue !== changes.workbasket.previousValue) {
this.setAllSelected({ value: false });
this.getAvailableDistributionTargets();
}
}
setAllSelected(allSelected: AllSelected) {
const side = allSelected.side;
const value = allSelected.value;
this.selectAllLeft = side === Side.AVAILABLE || typeof side === 'undefined' ? value : this.selectAllLeft;
this.selectAllRight = side === Side.SELECTED || typeof side === 'undefined' ? value : this.selectAllRight;
}
filterWorkbasketsByWorkbasketIDs(workbaskets: WorkbasketSummary[], IDs: WorkbasketSummary[]): WorkbasketSummary[] {
const workbasketIds: string[] = IDs.map((workbasket) => workbasket.workbasketId);
return workbaskets.filter((workbasket) => !workbasketIds.includes(workbasket.workbasketId));
}
getAvailableDistributionTargets() {
this.availableDistributionTargets$
.pipe(
take(1),
filter((availableDistributionTargets) => typeof availableDistributionTargets !== 'undefined')
)
.subscribe((availableDistributionTargets) => {
this.availableDistributionTargets = availableDistributionTargets.map((wb) => ({ ...wb }));
if (this.selectedDistributionTargets && this.selectedDistributionTargets.length !== 0) {
this.availableDistributionTargets = this.filterWorkbasketsByWorkbasketIDs(
this.availableDistributionTargets,
this.selectedDistributionTargets
);
}
this.availableDistributionTargetsUndoClone = [...this.availableDistributionTargets];
this.availableDistributionTargetsFilterClone = [...this.availableDistributionTargets];
});
}
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
toggleDistributionTargetsPicker() {
this.displayingDistributionTargetsPicker = !this.displayingDistributionTargetsPicker;
}
// TODO:
// an own filter would save a lot of work here because the workbasketService filter
// returns all available distribution targets and must be filtered again
performFilter({ left: side, right: filter }: Pair<Side, WorkbasketQueryFilterParameter>) {
this.workbasketService
.getWorkBasketsSummary(true, filter)
.pipe(takeUntil(this.destroy$))
.subscribe((distributionTargetsAvailable: WorkbasketSummaryRepresentation) => {
const isFilterEmpty =
filter['name-like'].length === 0 &&
filter['key-like'].length === 0 &&
filter['description-like'].length === 0 &&
filter['owner-like'].length === 0 &&
filter['type'].length === 0;
// filter available side
if (side === Side.AVAILABLE) {
if (isFilterEmpty) {
this.availableDistributionTargets = this.availableDistributionTargetsFilterClone;
} else {
this.availableDistributionTargets = this.filterWorkbasketsByWorkbasketIDs(
distributionTargetsAvailable.workbaskets,
this.selectedDistributionTargetsFilterClone
);
}
}
// filter selected side
else if (side === Side.SELECTED) {
if (isFilterEmpty) {
this.selectedDistributionTargets = this.selectedDistributionTargetsFilterClone;
} else {
const ids = distributionTargetsAvailable.workbaskets.map((workbasket) => workbasket.workbasketId);
this.selectedDistributionTargets = this.selectedDistributionTargetsFilterClone.filter((workbasket) =>
ids.includes(workbasket.workbasketId)
);
}
}
});
onSave(): void {
this.store.dispatch(new UpdateWorkbasketDistributionTargets());
}
onSave() {
this.store.dispatch(
new UpdateWorkbasketDistributionTargets(
this.selectedDistributionTargetsResource._links.self.href,
this.getSelectedIds()
)
);
return false;
}
moveDistributionTargets(side: number) {
// get all workbaskets without applied filter and without overwriting the selected property
this.selectedDistributionTargets = this.selectedDistributionTargets.concat(
this.filterWorkbasketsByWorkbasketIDs(
this.selectedDistributionTargetsFilterClone,
this.selectedDistributionTargets
)
);
this.availableDistributionTargets = this.availableDistributionTargets.concat(
this.filterWorkbasketsByWorkbasketIDs(
this.availableDistributionTargetsFilterClone,
this.availableDistributionTargets
)
);
if (side === Side.AVAILABLE) {
// moving available items to selected side
const itemsSelected = this.getSelectedItems(this.availableDistributionTargets);
this.selectedDistributionTargets = this.selectedDistributionTargets.concat(itemsSelected);
this.availableDistributionTargets = this.removeSelectedItems(this.availableDistributionTargets, itemsSelected);
this.unselectItems(itemsSelected);
} else {
// moving selected items to available side
const itemsSelected = this.getSelectedItems(this.selectedDistributionTargets);
this.selectedDistributionTargets = this.removeSelectedItems(this.selectedDistributionTargets, itemsSelected);
this.availableDistributionTargets = this.availableDistributionTargets.concat(itemsSelected);
this.unselectItems(itemsSelected);
}
this.setAllSelected({ value: false, side: side });
this.selectedDistributionTargetsFilterClone = this.selectedDistributionTargets;
this.availableDistributionTargetsFilterClone = this.availableDistributionTargets;
this.store.dispatch(new SetWorkbasketFilter(this.selectedDistributionTargetsFilter, 'selectedDistributionTargets'));
this.store.dispatch(
new SetWorkbasketFilter(this.availableDistributionTargetsFilter, 'availableDistributionTargets')
);
moveDistributionTargets(targetSide: Side): void {
this.transferDistributionTargetObservable.next(targetSide);
}
onClear() {
this.notificationsService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_RESTORE');
this.availableDistributionTargets = Object.assign([], this.availableDistributionTargetsUndoClone);
this.availableDistributionTargetsFilterClone = Object.assign([], this.availableDistributionTargetsUndoClone);
this.selectedDistributionTargets = Object.assign([], this.selectedDistributionTargetsUndoClone);
this.selectedDistributionTargetsFilterClone = Object.assign([], this.selectedDistributionTargetsUndoClone);
}
getSelectedItems(originList: any): any[] {
return originList.filter((item: any) => item.selected === true);
}
getSelectedIds(): string[] {
return this.selectedDistributionTargetsFilterClone.map((distributionTarget) => distributionTarget.workbasketId);
}
unselectItems(originList: any[]): any[] {
return originList
.filter((item) => item.selected)
.map((item) => {
item.selected = false;
});
}
removeSelectedItems(originList, selectedItemList) {
const copyList = [...originList];
for (let index = originList.length - 1; index >= 0; index--) {
if (selectedItemList.some((itemToRemove) => originList[index].workbasketId === itemToRemove.workbasketId)) {
copyList.splice(index, 1);
}
}
return copyList;
forkJoin([
this.store.dispatch(new FetchWorkbasketDistributionTargets(true)),
this.store.dispatch(new FetchAvailableDistributionTargets(true)),
this.store.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets')),
this.store.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'))
])
.pipe(take(1))
.subscribe(() => this.notificationsService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_RESTORE'));
}
toggleSideBySideView() {
@ -332,6 +111,7 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
}
ngOnDestroy() {
this.transferDistributionTargetObservable.complete();
this.destroy$.next();
this.destroy$.complete();
}

View File

@ -0,0 +1,9 @@
export enum Side {
AVAILABLE,
SELECTED
}
export interface AllSelected {
value: boolean;
side?: Side;
}

View File

@ -29,7 +29,6 @@ export class NavBarComponent implements OnInit {
ngOnInit() {
this.selectedRouteSubscription = this.selectedRouteService.getSelectedRoute().subscribe((value: string) => {
// does not work
// console.log('router', value);
this.selectedRoute = value;
this.setTitle(value);
});

View File

@ -0,0 +1,6 @@
import { WorkbasketType } from './workbasket-type';
import { WorkbasketSummary } from './workbasket-summary';
export interface WorkbasketDistributionTarget extends WorkbasketSummary {
selected?: boolean;
}

View File

@ -0,0 +1,7 @@
import { QueryPagingParameter } from './query-paging-parameter';
export class WorkbasketQueryPagingParameter implements QueryPagingParameter {
public 'page-size' = 40;
constructor(public page: number) {}
}

View File

@ -1,4 +1,4 @@
import { throwError as observableThrowError, Observable, Subject } from 'rxjs';
import { Observable, Subject, throwError as observableThrowError } from 'rxjs';
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from 'environments/environment';
@ -10,7 +10,7 @@ import { WorkbasketDistributionTargets } from 'app/shared/models/workbasket-dist
import { Sorting, WorkbasketQuerySortParameter } from 'app/shared/models/sorting';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { mergeMap, tap, catchError } from 'rxjs/operators';
import { catchError, mergeMap, tap } from 'rxjs/operators';
import { WorkbasketRepresentation } from '../../models/workbasket-representation';
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { QueryPagingParameter } from '../../models/query-paging-parameter';
@ -34,7 +34,7 @@ export class WorkbasketService {
filterParameter?: WorkbasketQueryFilterParameter,
sortParameter?: Sorting<WorkbasketQuerySortParameter>,
pagingParameter?: QueryPagingParameter
) {
): Observable<WorkbasketSummaryRepresentation> {
if (this.workbasketSummaryRef && !forceRequest) {
return this.workbasketSummaryRef;
}
@ -105,8 +105,19 @@ export class WorkbasketService {
}
// GET
getWorkBasketsDistributionTargets(url: string): Observable<WorkbasketDistributionTargets> {
return this.httpClient.get<WorkbasketDistributionTargets>(url);
getWorkBasketsDistributionTargets(
url: string,
filterParameter?: WorkbasketQueryFilterParameter,
sortParameter?: Sorting<WorkbasketQuerySortParameter>,
pagingParameter?: QueryPagingParameter
): Observable<WorkbasketDistributionTargets> {
return this.httpClient.get<WorkbasketDistributionTargets>(
`${url}/${asUrlQueryString({
...filterParameter,
...sortParameter,
...pagingParameter
})}`
);
}
// PUT

View File

@ -187,6 +187,588 @@ export const workbasketAccessItemsMock: WorkbasketAccessItemsRepresentation = {
}
};
export const workbasketAvailableDistributionTargets = {
workbaskets: [
{
workbasketId: 'WBI:000000000000000000000000000000000000',
key: 'ADMIN',
name: 'Postkorb Admin',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'Postkorb Admin',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000900',
key: 'sort001',
name: 'basxet0',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000901',
key: 'Sort002',
name: 'Basxet1',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000902',
key: 'sOrt003',
name: 'bAsxet2',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000903',
key: 'soRt004',
name: 'baSxet3',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000904',
key: 'sorT005',
name: 'basXet4',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000905',
key: 'Sort006',
name: 'basxEt5',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000906',
key: 'SOrt007',
name: 'basxeT6',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000907',
key: 'SoRt008',
name: 'BAsxet7',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000908',
key: 'SorT009',
name: 'BaSxet8',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:000000000000000000000000000000000909',
key: 'Sort010',
name: 'BasXet9',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'user-1-3',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000004',
key: 'TEAMLEAD-1',
name: 'PPK Teamlead KSC 1',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK Teamlead KSC 1',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000005',
key: 'TEAMLEAD-2',
name: 'PPK Teamlead KSC 2',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK Teamlead KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000006',
key: 'USER-1-1',
name: 'PPK User 1 KSC 1',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK User 1 KSC 1',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: 'custom4z',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000007',
key: 'USER-1-2',
name: 'PPK User 2 KSC 1',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK User 2 KSC 1',
owner: 'user-1-2',
custom1: 'custom1',
custom2: 'custom2',
custom3: 'custom3',
custom4: 'custom4',
orgLevel1: 'versicherung',
orgLevel2: 'abteilung',
orgLevel3: 'projekt',
orgLevel4: 'team',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000008',
key: 'USER-2-1',
name: 'PPK User 1 KSC 2',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK User 1 KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000009',
key: 'USER-2-2',
name: 'PPK User 2 KSC 2',
domain: 'DOMAIN_A',
type: 'PERSONAL',
description: 'PPK User 2 KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000010',
key: 'TPK_VIP',
name: 'Themenpostkorb VIP',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Themenpostkorb VIP',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000011',
key: 'GPK_B_KSC',
name: 'Gruppenpostkorb KSC B',
domain: 'DOMAIN_B',
type: 'GROUP',
description: 'Gruppenpostkorb KSC',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000012',
key: 'GPK_B_KSC_1',
name: 'Gruppenpostkorb KSC B1',
domain: 'DOMAIN_B',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 1',
owner: '',
custom1: 'custom1',
custom2: 'custom2',
custom3: 'custom3',
custom4: 'custom4',
orgLevel1: 'orgl1',
orgLevel2: 'orgl2',
orgLevel3: 'orgl3',
orgLevel4: 'aorgl4',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000013',
key: 'GPK_B_KSC_2',
name: 'Gruppenpostkorb KSC B2',
domain: 'DOMAIN_B',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000014',
key: 'USER-B-1',
name: 'PPK User 1 KSC 1 Domain B',
domain: 'DOMAIN_B',
type: 'PERSONAL',
description: 'PPK User 1 KSC 1 Domain B',
owner: '',
custom1: '',
custom2: 'custom20',
custom3: '',
custom4: 'custom4',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000015',
key: 'USER-B-2',
name: 'PPK User 2 KSC 1 Domain B',
domain: 'DOMAIN_B',
type: 'PERSONAL',
description: 'PPK User 1 KSC 1 Domain B',
owner: 'user-1-2',
custom1: 'ABCABC',
custom2: 'cust2',
custom3: 'cust3',
custom4: 'cust4',
orgLevel1: 'orgl1',
orgLevel2: 'orgl2',
orgLevel3: 'orgl3',
orgLevel4: 'orgl4',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000016',
key: 'TPK_VIP_2',
name: 'Themenpostkorb VIP 2',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Themenpostkorb VIP',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000017',
key: 'das_ist_ein_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_langer_key_1',
name: 'Testpostkorb',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description: null,
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000018',
key: 'das_ist_eine_lange_description_und_ein_langer_owner',
name: 'Testpostkorb',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description:
'Lorem ipsum dolor sit amet, consetetur sadipscingsed diam nonumy eirmod tempor invidunt ut labore sed diam nonumy eirmod tempor invidunt ut labore ore magna aliquyam erat, sed diam voluptua. At ves et accusam et justo duo dolores abcdfiskdk ekeke',
owner: 'das_ist_eine_sehr_sehr_sehr_sehr_sehr_lange_user_id',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000019',
key: 'das_ist_ein_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_langer_key_2',
name: 'das_ist_ein_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_langer_Testpostkorbname_ohne_Leerzeichen',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description: 'langer Key und langer Name ohne Leerzeichen',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000020',
key: 'das_ist_ein_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_langer_key_3',
name: 'das ist ein sehr sehr sehr sehr sehr sehr sehr sehr sehr sehr langer Testpostkorbname mit Leerzeichen 1',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description: 'langer Key und langer Name mit Leerzeichen',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000021',
key: 'das_ist_ein_sehr_sehr_sehr_sehr_sehr_sehr_sehr_sehr_langer_key_4',
name: 'das ist ein sehr sehr sehr sehr sehr sehr sehr sehr sehr sehr langer Testpostkorbname mit Leerzeichen 2',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description: 'langer Key, langer Name mit Leerzeichen und lange UserId',
owner: 'das_ist_eine_sehr_sehr_sehr_sehr_sehr_lange_user_id',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000022',
key: 'kurzer_key',
name: 'das ist ein sehr sehr sehr sehr sehr sehr sehr sehr sehr sehr langer Testpostkorbname mit Leerzeichen 3',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description: 'kurzer Key und langer Name mit Leerzeichen',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000023',
key: 'langer key, langer name, eine lange description, langer owner',
name: 'das ist ein sehr sehr sehr sehr sehr sehr sehr sehr sehr sehr langer Testpostkorbname mit Leerzeichen 4',
domain: 'DOMAIN_TEST',
type: 'TOPIC',
description:
'Lorem ipsum dolor sit amet, consetetur sadipscingsed diam nonumy eirmod tempor invidunt ut labore sed diam nonumy eirmod tempor invidunt ut labore ore magna aliquyam erat, sed diam voluptua. At ves et accusam et justo duo dolores abcdfiskdk ekeke',
owner: 'das_ist_eine_sehr_sehr_sehr_sehr_sehr_lange_user_id',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
}
],
page: {
size: 40,
totalElements: 34,
totalPages: 1,
number: 1
},
_links: {
self: {
href: 'http://localhost:8080/taskana/api/v1/workbaskets/?page=1&page-size=40'
},
first: {
href: 'http://localhost:8080/taskana/api/v1/workbaskets/?page-size=40&page=1'
},
last: {
href: 'http://localhost:8080/taskana/api/v1/workbaskets/?page-size=40&page=1'
}
}
};
export const workbasketReadStateMock = {
selectedWorkbasket: selectedWorkbasketMock,
paginatedWorkbasketsSummary: {
@ -304,69 +886,6 @@ export const workbasketReadStateMock = {
}
},
action: ACTION.READ,
workbasketDistributionTargets: {
_links: {
self: {
href: 'http://localhost:8080/taskana/api/v1/workbaskets/WBI:000000000000000000000000000000000900/distribution-targets'
}
},
distributionTargets: [
{
workbasketId: 'WBI:100000000000000000000000000000000001',
key: 'GPK_KSC',
name: 'Gruppenpostkorb KSC',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC',
owner: 'owner0815',
custom1: 'ABCQVW',
custom2: '',
custom3: 'xyz4',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000002',
key: 'GPK_KSC_1',
name: 'Gruppenpostkorb KSC 1',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 1',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000003',
key: 'GPK_KSC_2',
name: 'Gruppenpostkorb KSC 2',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
}
]
},
workbasketAvailableDistributionTargets: [
{
workbasketId: 'WBI:100000000000000000000000000000000001',
@ -513,7 +1032,73 @@ export const workbasketReadStateMock = {
markedForDeletion: false
}
],
workbasketAccessItems: workbasketAccessItemsMock
workbasketAccessItems: workbasketAccessItemsMock,
workbasketDistributionTargets: {
_links: {
self: {
href: 'http://localhost:8080/taskana/api/v1/workbaskets/WBI:000000000000000000000000000000000900/distribution-targets'
}
},
distributionTargets: [
{
workbasketId: 'WBI:100000000000000000000000000000000001',
key: 'GPK_KSC',
name: 'Gruppenpostkorb KSC',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC',
owner: 'owner0815',
custom1: 'ABCQVW',
custom2: '',
custom3: 'xyz4',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000002',
key: 'GPK_KSC_1',
name: 'Gruppenpostkorb KSC 1',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 1',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
},
{
workbasketId: 'WBI:100000000000000000000000000000000003',
key: 'GPK_KSC_2',
name: 'Gruppenpostkorb KSC 2',
domain: 'DOMAIN_A',
type: 'GROUP',
description: 'Gruppenpostkorb KSC 2',
owner: '',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
}
]
},
distributionTargetsPage: 0,
availableDistributionTargets: workbasketAvailableDistributionTargets,
availableDistributionTargetsPage: 0
};
export const settingsStateMock = {

View File

@ -6,6 +6,8 @@ import { WorkbasketComponent } from '../../../administration/models/workbasket-c
import { ButtonAction } from '../../../administration/models/button-action';
import { QueryPagingParameter } from '../../models/query-paging-parameter';
import { WorkbasketQueryFilterParameter } from '../../models/workbasket-query-filter-parameter';
import { WorkbasketSummary } from '../../models/workbasket-summary';
import { Side } from '../../../administration/models/workbasket-distribution-enums';
// Workbasket List
export class GetWorkbasketsSummary {
@ -21,6 +23,7 @@ export class GetWorkbasketsSummary {
export class SelectWorkbasket {
static readonly type = '[Workbasket] Select a workbasket';
constructor(public workbasketId: string) {}
}
@ -34,68 +37,93 @@ export class CreateWorkbasket {
export class SetActiveAction {
static readonly type = '[Workbasket] Specify current action';
constructor(public action: ACTION) {}
}
//Workbasket Details
export class SelectComponent {
static readonly type = '[Workbasket] Select component';
constructor(public component: WorkbasketComponent) {}
}
export class OnButtonPressed {
static readonly type = '[Workbasket] Button pressed';
constructor(public button: ButtonAction) {}
}
// Workbasket Information
export class SaveNewWorkbasket {
static readonly type = '[Workbasket] Save new workbasket';
constructor(public workbasket: Workbasket) {}
}
export class CopyWorkbasket {
static readonly type = '[Workbasket] Copy selected workbasket';
constructor(public workbasket: Workbasket) {}
}
export class UpdateWorkbasket {
static readonly type = '[Workbasket] Update a workbasket';
constructor(public url: string, public workbasket: Workbasket) {}
}
export class RemoveDistributionTarget {
static readonly type = '[Workbasket] Remove distribution targets of selected workbasket';
constructor(public url: string) {}
constructor(public url: string, public workbasket: Workbasket) {}
}
export class MarkWorkbasketForDeletion {
static readonly type = '[Workbasket] Mark selected workbasket for deletion';
constructor(public url: string) {}
}
export class RemoveDistributionTarget {
static readonly type = '[Workbasket] Remove selected workbasket as distribution target';
constructor(public url: string) {}
}
// Workbasket Access Items
export class GetWorkbasketAccessItems {
static readonly type = '[Workbasket] Get all workbasket access items';
constructor(public url: string) {}
}
export class UpdateWorkbasketAccessItems {
static readonly type = "[Workbasket] Update selected workbasket's access items";
static readonly type = '[Workbasket] Update selected workbaskets access items';
constructor(public url: string, public workbasketAccessItems: WorkbasketAccessItems[]) {}
}
// Workbasket Distribution Targets
export class GetWorkbasketDistributionTargets {
static readonly type = '[Workbasket] Get all workbasket distribution targets';
constructor(public url: string) {}
}
export class GetAvailableDistributionTargets {
static readonly type = '[Workbasket] Get available distribution targets';
}
export class UpdateWorkbasketDistributionTargets {
static readonly type = '[Workbasket] Update workbasket distribution targets';
constructor(public url: string, public distributionTargetsIds: string[]) {}
}
export class FetchWorkbasketDistributionTargets {
static readonly type = '[Workbasket] Fetch a subset of selected workbasket distribution targets';
constructor(
public refetchAll: boolean,
public filterParameter?: WorkbasketQueryFilterParameter,
public sortParameter?: Sorting<WorkbasketQuerySortParameter>
) {}
}
export class FetchAvailableDistributionTargets {
static readonly type = '[Workbasket] Fetch a subset of available workbasket distribution targets';
constructor(
public refetchAll: boolean,
public filterParameter?: WorkbasketQueryFilterParameter,
public sortParameter?: Sorting<WorkbasketQuerySortParameter>
) {}
}
export class TransferDistributionTargets {
static readonly type = '[Workbasket] Transfer a set of workbasket distribution targets';
constructor(public targetSide: Side, public workbasketSummaries: WorkbasketSummary[]) {}
}

View File

@ -66,13 +66,13 @@ export class WorkbasketSelectors {
// Workbasket Distribution Targets
@Selector([WorkbasketState])
static workbasketDistributionTargets(state: WorkbasketStateModel): WorkbasketDistributionTargets {
return state.workbasketDistributionTargets;
static workbasketDistributionTargets(state: WorkbasketStateModel): WorkbasketSummary[] {
return state.workbasketDistributionTargets.distributionTargets;
}
@Selector([WorkbasketState])
static availableDistributionTargets(state: WorkbasketStateModel): WorkbasketSummary[] {
return state.workbasketAvailableDistributionTargets;
return state.availableDistributionTargets.workbaskets;
}
@Selector([WorkbasketState])

View File

@ -8,9 +8,9 @@ import {
CopyWorkbasket,
CreateWorkbasket,
DeselectWorkbasket,
GetAvailableDistributionTargets,
FetchAvailableDistributionTargets,
FetchWorkbasketDistributionTargets,
GetWorkbasketAccessItems,
GetWorkbasketDistributionTargets,
GetWorkbasketsSummary,
MarkWorkbasketForDeletion,
OnButtonPressed,
@ -19,6 +19,7 @@ import {
SelectComponent,
SelectWorkbasket,
SetActiveAction,
TransferDistributionTargets,
UpdateWorkbasket,
UpdateWorkbasketAccessItems,
UpdateWorkbasketDistributionTargets
@ -38,6 +39,9 @@ import { TaskanaDate } from '../../util/taskana.date';
import { DomainService } from '../../services/domain/domain.service';
import { ClearWorkbasketFilter } from '../filter-store/filter.actions';
import { Injectable } from '@angular/core';
import { WorkbasketQueryPagingParameter } from '../../models/workbasket-query-paging-parameter';
import { Side } from '../../../administration/models/workbasket-distribution-enums';
import { cloneDeep } from 'lodash';
class InitializeStore {
static readonly type = '[Workbasket] Initializing state';
@ -77,12 +81,16 @@ export class WorkbasketState implements NgxsAfterBootstrap {
}
ctx.patchState({
badgeMessage: ''
badgeMessage: '',
availableDistributionTargetsPage: 0,
distributionTargetsPage: 0,
workbasketDistributionTargets: { distributionTargets: [], _links: {} },
availableDistributionTargets: { workbaskets: [], _links: {} }
});
ctx.dispatch(new SelectComponent(tab));
});
return of();
return of(null);
}
@Action(GetWorkbasketsSummary)
@ -127,9 +135,6 @@ export class WorkbasketState implements NgxsAfterBootstrap {
});
ctx.dispatch(new GetWorkbasketAccessItems(ctx.getState().selectedWorkbasket._links.accessItems.href));
ctx.dispatch(
new GetWorkbasketDistributionTargets(ctx.getState().selectedWorkbasket._links.distributionTargets.href)
);
this.location.go(
this.location
@ -204,7 +209,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
}
@Action(CopyWorkbasket)
copyWorkbasket(ctx: StateContext<WorkbasketStateModel>, action: CopyWorkbasket): Observable<any> {
copyWorkbasket(ctx: StateContext<WorkbasketStateModel>): Observable<any> {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets/(detail:new-workbasket)'));
ctx.dispatch(new OnButtonPressed(undefined));
@ -230,21 +235,24 @@ export class WorkbasketState implements NgxsAfterBootstrap {
tap((domain) => {
this.location.go(this.location.path().replace(/(workbaskets).*/g, 'workbaskets/(detail:new-workbasket)'));
if (!ctx.getState().workbasketAvailableDistributionTargets) {
ctx.dispatch(new GetAvailableDistributionTargets());
if (!ctx.getState().availableDistributionTargets) {
ctx.dispatch(new FetchAvailableDistributionTargets(true));
}
const emptyWorkbasket: Workbasket = {};
emptyWorkbasket.domain = domain;
emptyWorkbasket.type = WorkbasketType.PERSONAL;
const date = TaskanaDate.getDate();
const date: string = TaskanaDate.getDate();
emptyWorkbasket.created = date;
emptyWorkbasket.modified = date;
emptyWorkbasket.owner = '';
const accessItems = { accessItems: [], _links: {} };
const distributionTargets = { distributionTargets: [], _links: {} };
const accessItems: WorkbasketAccessItemsRepresentation = { accessItems: [], _links: {} };
const distributionTargets: WorkbasketDistributionTargets = {
_links: {},
distributionTargets: []
};
ctx.patchState({
action: ACTION.CREATE,
@ -348,73 +356,144 @@ export class WorkbasketState implements NgxsAfterBootstrap {
);
}
@Action(GetWorkbasketDistributionTargets)
getWorkbasketDistributionTargets(
ctx: StateContext<WorkbasketStateModel>,
action: GetWorkbasketDistributionTargets
): Observable<any> {
return this.workbasketService.getWorkBasketsDistributionTargets(action.url).pipe(
take(1),
tap((workbasketDistributionTargets) => {
ctx.patchState({
workbasketDistributionTargets
});
})
);
}
@Action(GetAvailableDistributionTargets)
getAvailableDistributionTargets(ctx: StateContext<WorkbasketStateModel>): Observable<any> {
return this.workbasketService.getWorkBasketsSummary(true).pipe(
take(1),
tap((workbasketAvailableDistributionTargets: WorkbasketSummaryRepresentation) => {
ctx.patchState({
workbasketAvailableDistributionTargets: workbasketAvailableDistributionTargets.workbaskets
});
})
);
}
@Action(UpdateWorkbasketDistributionTargets)
updateWorkbasketDistributionTargets(
ctx: StateContext<WorkbasketStateModel>,
action: UpdateWorkbasketDistributionTargets
): Observable<any> {
updateWorkbasketDistributionTargets(ctx: StateContext<WorkbasketStateModel>): Observable<any> {
this.requestInProgressService.setRequestInProgress(true);
return this.workbasketService.updateWorkBasketsDistributionTargets(action.url, action.distributionTargetsIds).pipe(
take(1),
tap(
(updatedWorkbasketsDistributionTargets) => {
ctx.patchState({
workbasketDistributionTargets: updatedWorkbasketsDistributionTargets
});
const workbasketId = ctx.getState().selectedWorkbasket?.workbasketId;
if (typeof workbasketId !== 'undefined') {
this.workbasketService.getWorkBasket(workbasketId).subscribe((selectedWorkbasket) => {
ctx.patchState({
selectedWorkbasket,
action: ACTION.READ
});
ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
});
}
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_SAVE', {
workbasketName: ctx.getState().selectedWorkbasket.name
});
return of(null);
},
() => {
this.requestInProgressService.setRequestInProgress(false);
}
return this.workbasketService
.updateWorkBasketsDistributionTargets(
ctx.getState().selectedWorkbasket._links.distributionTargets.href,
ctx.getState().workbasketDistributionTargets.distributionTargets.map((w) => w.workbasketId)
)
);
.pipe(
take(1),
tap({
next: (updatedWorkbasketsDistributionTargets) => {
ctx.patchState({
workbasketDistributionTargets: updatedWorkbasketsDistributionTargets
});
const workbasketId = ctx.getState().selectedWorkbasket?.workbasketId;
if (typeof workbasketId !== 'undefined') {
this.workbasketService.getWorkBasket(workbasketId).subscribe((selectedWorkbasket) => {
ctx.patchState({
selectedWorkbasket,
action: ACTION.READ
});
ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
});
}
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showSuccess('WORKBASKET_DISTRIBUTION_TARGET_SAVE', {
workbasketName: ctx.getState().selectedWorkbasket.name
});
return of(null);
},
error: () => {
this.requestInProgressService.setRequestInProgress(false);
}
})
);
}
ngxsAfterBootstrap(ctx?: StateContext<any>): void {
@Action(FetchWorkbasketDistributionTargets)
fetchWorkbasketDistributionTargets(
ctx: StateContext<WorkbasketStateModel>,
action: FetchWorkbasketDistributionTargets
): Observable<any> {
const { selectedWorkbasket, distributionTargetsPage, workbasketDistributionTargets } = ctx.getState();
const { filterParameter, sortParameter, refetchAll } = action;
const nextDistributionTargetsPage = refetchAll ? 1 : distributionTargetsPage + 1;
return this.workbasketService
.getWorkBasketsDistributionTargets(
selectedWorkbasket._links.distributionTargets.href,
filterParameter,
sortParameter,
new WorkbasketQueryPagingParameter(nextDistributionTargetsPage)
)
.pipe(
take(1),
tap((wbt: WorkbasketDistributionTargets) => {
if (!refetchAll && workbasketDistributionTargets) {
wbt.distributionTargets = workbasketDistributionTargets.distributionTargets.concat(wbt.distributionTargets);
}
ctx.patchState({
workbasketDistributionTargets: wbt,
distributionTargetsPage: nextDistributionTargetsPage
});
})
);
}
@Action(FetchAvailableDistributionTargets)
fetchAvailableDistributionTargets(
ctx: StateContext<WorkbasketStateModel>,
action: FetchAvailableDistributionTargets
): Observable<any> {
const { availableDistributionTargetsPage, availableDistributionTargets } = ctx.getState();
const { filterParameter, sortParameter, refetchAll } = action;
const nextAvailableDistributionTargetsPage = refetchAll ? 1 : availableDistributionTargetsPage + 1;
if (!refetchAll && nextAvailableDistributionTargetsPage > availableDistributionTargets.page?.totalPages) {
return of(null);
}
return this.workbasketService
.getWorkBasketsSummary(
true,
filterParameter,
sortParameter,
new WorkbasketQueryPagingParameter(nextAvailableDistributionTargetsPage)
)
.pipe(
take(1),
tap((wbSummaryRepresentation: WorkbasketSummaryRepresentation) => {
const distributionTargetSet = new Set(
ctx.getState().workbasketDistributionTargets.distributionTargets.map((wb) => wb.workbasketId)
);
wbSummaryRepresentation.workbaskets = wbSummaryRepresentation.workbaskets.filter((wb) => {
return !distributionTargetSet.has(wb.workbasketId);
});
if (!refetchAll && availableDistributionTargets) {
wbSummaryRepresentation.workbaskets = availableDistributionTargets.workbaskets.concat(
wbSummaryRepresentation.workbaskets
);
}
ctx.patchState({
availableDistributionTargets: wbSummaryRepresentation,
availableDistributionTargetsPage: nextAvailableDistributionTargetsPage
});
})
);
}
@Action(TransferDistributionTargets)
transferDistributionTargets(ctx: StateContext<WorkbasketStateModel>, action: TransferDistributionTargets): void {
let { workbasketDistributionTargets, availableDistributionTargets } = ctx.getState();
const workbasketSummarySet = new Set(action.workbasketSummaries.map((wb) => wb.workbasketId));
availableDistributionTargets = cloneDeep(availableDistributionTargets);
workbasketDistributionTargets = cloneDeep(workbasketDistributionTargets);
if (action.targetSide === Side.AVAILABLE) {
workbasketDistributionTargets.distributionTargets = workbasketDistributionTargets.distributionTargets.filter(
(wb) => !workbasketSummarySet.has(wb.workbasketId)
);
availableDistributionTargets.workbaskets = availableDistributionTargets.workbaskets.concat(
action.workbasketSummaries
);
} else {
availableDistributionTargets.workbaskets = availableDistributionTargets.workbaskets.filter(
(wb) => !workbasketSummarySet.has(wb.workbasketId)
);
workbasketDistributionTargets.distributionTargets = workbasketDistributionTargets.distributionTargets.concat(
action.workbasketSummaries
);
}
ctx.patchState({
availableDistributionTargets,
workbasketDistributionTargets
});
}
ngxsAfterBootstrap(ctx: StateContext<WorkbasketStateModel>): void {
ctx.dispatch(new InitializeStore());
}
}
@ -438,7 +517,9 @@ export interface WorkbasketStateModel {
action: ACTION;
workbasketAccessItems: WorkbasketAccessItemsRepresentation;
workbasketDistributionTargets: WorkbasketDistributionTargets;
workbasketAvailableDistributionTargets: WorkbasketSummary[];
distributionTargetsPage: number;
availableDistributionTargets: WorkbasketSummaryRepresentation;
availableDistributionTargetsPage: number;
selectedComponent: WorkbasketComponent;
badgeMessage: string;
button: ButtonAction | undefined;

View File

@ -1,4 +1,4 @@
@use "sass:math";
@use 'sass:math';
@import 'colors';
.item {

View File

@ -504,5 +504,5 @@ li.list-group-item:hover {
}
.hot-toast-icon {
align-self: center !important
align-self: center !important;
}

View File

@ -1828,6 +1828,11 @@
resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee"
integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4=
"@types/lodash@^4.14.178":
version "4.14.178"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.178.tgz#341f6d2247db528d4a13ddbb374bcdc80406f4f8"
integrity sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw==
"@types/minimatch@*":
version "3.0.5"
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.5.tgz#1001cc5e6a3704b83c236027e77f2f58ea010f40"
@ -6755,7 +6760,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.7.0:
lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.21, lodash@^4.5.0, lodash@^4.7.0:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==