TSK-1806: Bug fixes for Workbasket Distribution Targets

Bug fixes:
  Changes no longer get discarded when switching tabs
  Saving of Workbasket Distribution Targets is now possible from all tabs
  Saving of Workbasket Distribution Targets now works correctly and does not throw an error 400
  Removing of Distribution Targets now works correctly
  Removed loading of duplicate distribution targets
  Added E2E tests for most previously mentioned bugs
This commit is contained in:
Tristan 2022-02-28 18:39:03 +01:00 committed by Tristan2357
parent 17d9567b35
commit 28be668453
12 changed files with 218 additions and 72 deletions

View File

@ -113,4 +113,108 @@ context('TASKANA Workbaskets', () => {
cy.saveWorkbaskets();
});
it('should be possible to change the name of a workbasket and switch tabs and transfer workbaskets without loosing changes', () => {
cy.visitTestWorkbasket();
cy.get('#workbasket-name').clear().type(Cypress.env('testValueWorkbaskets'));
cy.visitWorkbasketsDistributionTargetsPage();
cy.get(
'#dual-list-Left > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper > :nth-child(1)'
)
.should('contain.text', 'Basxet1')
.click();
cy.get('.distribution-targets-list__action-buttons--chooser').click();
cy.get('.workbasket-details__title-name').should('contain.text', Cypress.env('testValueWorkbaskets'));
});
it('should be possible to transfer distribution targets and switch tabs without loosing changes', () => {
cy.visitTestWorkbasket();
cy.visitWorkbasketsDistributionTargetsPage();
cy.get(
'#dual-list-Left > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper > :nth-child(1)'
)
.should('contain.text', 'Basxet1')
.click();
cy.get('.distribution-targets-list__action-buttons--chooser').click();
cy.visitWorkbasketsInformationPage();
cy.visitWorkbasketsDistributionTargetsPage();
cy.get(
'#dual-list-Right > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper > :nth-child(2)'
).should('contain.text', 'Basxet1');
cy.undoWorkbaskets();
cy.get(
'#dual-list-Right > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper'
)
.children()
.should('have.length', 1);
});
it('should be possible to transfer distribution targets and save changes from another tab', () => {
cy.visitTestWorkbasket();
cy.visitWorkbasketsDistributionTargetsPage();
cy.get(
'#dual-list-Left > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper > :nth-child(1)'
)
.should('contain.text', 'Basxet1')
.click();
cy.get('.distribution-targets-list__action-buttons--chooser').click();
cy.visitWorkbasketsInformationPage();
cy.saveWorkbaskets();
cy.visitWorkbasketsDistributionTargetsPage();
cy.get(
'#dual-list-Right > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper > :nth-child(2)'
)
.should('contain.text', 'Basxet1')
.click();
cy.get('.distribution-targets-list__action-buttons--selected').click();
cy.saveWorkbaskets();
});
it('should be possible to change workbasket information and save changes from another tab', () => {
cy.visitTestWorkbasket();
cy.get('#wb-custom-4').clear().type(Cypress.env('testValueWorkbaskets'));
cy.visitWorkbasketsDistributionTargetsPage();
cy.saveWorkbaskets();
cy.visitWorkbasketsInformationPage();
cy.get('#wb-custom-4').should('have.value', Cypress.env('testValueWorkbaskets'));
});
it('should be possible', () => {
cy.visitTestWorkbasket();
cy.visitWorkbasketsDistributionTargetsPage();
});
it('should be possible to remove all distribution targets', () => {
cy.visitTestWorkbasket();
cy.visitWorkbasketsDistributionTargetsPage();
cy.get('#dual-list-Right > .distribution-targets-list > .mat-toolbar > :nth-child(4)').click();
cy.get('.distribution-targets-list__action-buttons--selected').click();
cy.get(
'#dual-list-Right > .distribution-targets-list > .mat-selection-list > .cdk-virtual-scroll-viewport > .cdk-virtual-scroll-content-wrapper'
)
.children()
.should('have.length', 0);
cy.undoWorkbaskets();
});
});

View File

@ -34,6 +34,15 @@ Cypress.Commands.add('saveWorkbaskets', () => {
cy.get('button').contains('Save').click();
});
/**
* @memberof cy
* @method undoWorkbaskets
* @returns Chainable
*/
Cypress.Commands.add('undoWorkbaskets', () => {
cy.get('button').contains('Undo Changes').click();
});
/**
* @memberof cy
* @method verifyPageLoad

View File

@ -1,4 +1,6 @@
import {
AfterViewChecked,
AfterViewInit,
Component,
ElementRef,
Input,
@ -41,7 +43,7 @@ import { ButtonAction } from '../../models/button-action';
animations: [highlight],
styleUrls: ['./workbasket-access-items.component.scss']
})
export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDestroy {
export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDestroy, AfterViewInit, AfterViewChecked {
@Input()
workbasket: Workbasket;
@ -65,7 +67,6 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
});
toggleValidationAccessIdMap = new Map<number, boolean>();
initialized = false;
added = false;
isNewAccessItemsFromStore = false;
isAccessItemsTabSelected = false;
@ -126,10 +127,13 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
let accessItems = [...accessItemsRepresentation.accessItems];
accessItems = this.sortAccessItems(accessItems, 'accessId');
this.accessItemsRepresentation = { accessItems: accessItems, _links: accessItemsRepresentation._links };
this.accessItemsRepresentation = {
accessItems: accessItems,
_links: accessItemsRepresentation._links
};
this.setAccessItemsGroups(accessItems);
this.accessItemsClone = this.cloneAccessItems(accessItems);
this.accessItemsResetClone = this.cloneAccessItems(accessItems);
this.accessItemsClone = this.cloneAccessItems();
this.accessItemsResetClone = this.cloneAccessItems();
this.isNewAccessItemsFromStore = true;
}
@ -270,7 +274,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
this.formsValidatorService.formSubmitAttempt = false;
this.AccessItemsForm.reset();
this.setAccessItemsGroups(this.accessItemsResetClone);
this.accessItemsClone = this.cloneAccessItems(this.accessItemsResetClone);
this.accessItemsClone = this.cloneAccessItems();
this.notificationsService.showSuccess('WORKBASKET_ACCESS_ITEM_RESTORE');
}
@ -333,7 +337,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest
checkbox.checked = areAllCheckboxesSelected;
}
cloneAccessItems(inputaccessItem): WorkbasketAccessItems[] {
cloneAccessItems(): WorkbasketAccessItems[] {
return this.AccessItemsForm.value.accessItemsGroups.map((accessItems: WorkbasketAccessItems) => ({
...accessItems
}));

View File

@ -1,58 +1,79 @@
<div class="workbasket-details">
<mat-toolbar class="workbasket-details__toolbar">
<!-- TITLE -->
<h4 class="workbasket-details__title">
<span class="workbasket-details__title-name" matTooltip="{{workbasket.name}}">{{workbasket.name}}</span>
<span class="workbasket-details__title-badge" matTooltip="{{ badgeMessage$ | async }}"> {{ badgeMessage$ | async }}</span>
<span class="workbasket-details__title-name"
matTooltip="{{workbasket.name}}">{{workbasket.name}}</span>
<span class="workbasket-details__title-badge"
matTooltip="{{ badgeMessage$ | async }}"> {{ badgeMessage$ | async }}</span>
</h4>
<span class="workbasket-details__spacer"></span>
<button mat-button class="workbasket-details__button workbasket-details__save-button"
matTooltip="Save changes in current workbasket" (click)="onSubmit()">
<!-- SAVE -->
<button (click)="onSubmit()" class="workbasket-details__button workbasket-details__save-button"
mat-button matTooltip="Save changes in current workbasket">
Save
<mat-icon class="md-20">save</mat-icon>
</button>
<button mat-stroked-button class="workbasket-details__button" matTooltip="Revert changes to previous saved state"
(click)="onRestore()">
<!-- UNDO -->
<button (click)="onRestore()" class="workbasket-details__button" mat-stroked-button
matTooltip="Revert changes to previous saved state">
Undo Changes
<mat-icon class="workbasket-details__button-aquamarine md-20">restore</mat-icon>
</button>
<button mat-stroked-button [matMenuTriggerFor]="buttonMenu" matTooltip="More actions" class="action-toolbar__button"
id="action-toolbar__more-buttons">
<!-- MENU -->
<button [matMenuTriggerFor]="buttonMenu" class="action-toolbar__button"
id="action-toolbar__more-buttons" mat-stroked-button
matTooltip="More actions">
<mat-icon>more_vert</mat-icon>
</button>
<mat-menu #buttonMenu="matMenu">
<button mat-menu-item class="workbasket-details__dropdown"
matTooltip="Copy current values to create new workbasket" (click)="onCopy()">
<!-- COPY -->
<button (click)="onCopy()" class="workbasket-details__dropdown"
mat-menu-item matTooltip="Copy current values to create new workbasket">
<mat-icon class="workbasket-details__button-aquamarine">content_copy</mat-icon>
<span>Copy</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown"
matTooltip="Remove this workbasket as distribution target" (click)="onRemoveAsDistributionTarget()">
<!-- REMOVE AS DISTRIBUTION TARGET -->
<button (click)="onRemoveAsDistributionTarget()" class="workbasket-details__dropdown"
mat-menu-item matTooltip="Remove this workbasket as distribution target">
<mat-icon class="workbasket-details__button-red">remove_circle_outline</mat-icon>
<span>Remove as distribution target</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown" matTooltip="Delete this workbasket"
(click)="onRemoveWorkbasket()">
<!-- DELETE -->
<button (click)="onRemoveWorkbasket()" class="workbasket-details__dropdown" mat-menu-item
matTooltip="Delete this workbasket">
<mat-icon class="workbasket-details__button-red">delete</mat-icon>
<span>Delete</span>
</button>
<button mat-menu-item class="workbasket-details__dropdown" style="border-bottom-style: none;"
matTooltip="Close this workbasket and discard all changes" (click)="onClose()">
<!-- CLOSE AND DISCARD -->
<button (click)="onClose()" class="workbasket-details__dropdown" mat-menu-item
matTooltip="Close this workbasket and discard all changes"
style="border-bottom-style: none;">
<mat-icon>close</mat-icon>
<span>Close</span>
</button>
</mat-menu>
</mat-toolbar>
<mat-tab-group animationDuration="0ms" (selectedIndexChange)="selectComponent($event)"
[selectedIndex]="selectedTab$ | async">
<!-- TABS -->
<mat-tab-group (selectedIndexChange)="selectComponent($event)"
[selectedIndex]="selectedTab$ | async"
animationDuration="0ms">
<mat-tab label="Information">
<taskana-administration-workbasket-information [workbasket]="workbasket" [action]="action">
<taskana-administration-workbasket-information [action]="action" [workbasket]="workbasket">
</taskana-administration-workbasket-information>
</mat-tab>
<mat-tab label="Access">
<taskana-administration-workbasket-access-items [workbasket]="workbasket" [expanded]="expanded">
<taskana-administration-workbasket-access-items [expanded]="expanded"
[workbasket]="workbasket">
</taskana-administration-workbasket-access-items>
</mat-tab>
<mat-tab label="Distribution Targets">

View File

@ -1,11 +1,11 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable, Subject } from 'rxjs';
import { Observable, of, Subject, timeout } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket';
import { ACTION } from 'app/shared/models/action';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { Select, Store } from '@ngxs/store';
import { takeUntil } from 'rxjs/operators';
import { Actions, ofActionSuccessful, Select, Store } from '@ngxs/store';
import { catchError, filter, take, takeUntil } from 'rxjs/operators';
import {
WorkbasketAndComponentAndAction,
WorkbasketSelectors
@ -15,11 +15,13 @@ import {
CopyWorkbasket,
DeselectWorkbasket,
OnButtonPressed,
SelectComponent
SelectComponent,
UpdateWorkbasket,
UpdateWorkbasketDistributionTargets
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { ButtonAction } from '../../models/button-action';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { WorkbasketComponent } from '../../models/workbasket-component';
import { cloneDeep } from 'lodash';
@Component({
selector: 'taskana-administration-workbasket-details',
@ -29,7 +31,6 @@ import { WorkbasketComponent } from '../../models/workbasket-component';
export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
workbasket: Workbasket;
action: ACTION;
selectedComponent: WorkbasketComponent;
@Select(WorkbasketSelectors.selectedComponent)
selectedTab$: Observable<number>;
@ -40,6 +41,9 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
@Select(WorkbasketSelectors.selectedWorkbasketAndComponentAndAction)
selectedWorkbasketAndComponentAndAction$: Observable<WorkbasketAndComponentAndAction>;
@Select(WorkbasketSelectors.selectedWorkbasket)
selectedWorkbasket$: Observable<Workbasket>;
destroy$ = new Subject<void>();
@Input() expanded: boolean;
@ -50,7 +54,8 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
private router: Router,
private domainService: DomainService,
private requestInProgressService: RequestInProgressService,
private store: Store
private store: Store,
private ngxsActions$: Actions
) {}
ngOnInit() {
@ -58,28 +63,39 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy {
}
getWorkbasketFromStore() {
// this is necessary since we receive workbaskets from store even when there is no update
// this would unintentionally discard changes
/*
get workbasket from store only when (to avoid discarding changes):
a) workbasket with another ID is selected (includes copying)
b) empty workbasket is created
*/
this.selectedWorkbasketAndComponentAndAction$.pipe(takeUntil(this.destroy$)).subscribe((object) => {
const workbasket = object.selectedWorkbasket;
const action = object.action;
const component = object.selectedComponent;
// get workbasket from store when:
// a) workbasket with another ID is selected (includes copying)
// b) empty workbasket is created
// c) saving the workbasket
const isAnotherId = this.workbasket?.workbasketId !== workbasket?.workbasketId;
const isCreation = action !== this.action && action === ACTION.CREATE;
const isSameComponent = component === this.selectedComponent;
if (isAnotherId || isCreation || isSameComponent) {
this.workbasket = { ...workbasket };
if (isAnotherId || isCreation) {
this.workbasket = cloneDeep(workbasket);
}
this.action = action;
this.selectedComponent = component;
});
// c) saving the workbasket
this.ngxsActions$.pipe(ofActionSuccessful(UpdateWorkbasket), takeUntil(this.destroy$)).subscribe(() => {
this.store
.dispatch(new UpdateWorkbasketDistributionTargets())
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.selectedWorkbasket$
.pipe(
take(5),
timeout(250),
catchError(() => of(null)),
filter((val) => val !== null)
)
.subscribe((wb) => (this.workbasket = wb));
});
});
}

View File

@ -1,4 +1,4 @@
<div class="distribution-targets-list" id="dual-list-Left">
<div class="distribution-targets-list">
<mat-toolbar>
<span class="distribution-targets-list__header" matTooltip="{{header}}">{{header}}</span>

View File

@ -75,6 +75,7 @@ export class WorkbasketDistributionTargetsListComponent
});
});
this.availableDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
if (typeof this.filter === 'undefined' || isEqual(this.filter, filter)) return;
this.filter = filter;
this.store.dispatch(new FetchAvailableDistributionTargets(true, this.filter));
this.selectAll(false);
@ -86,7 +87,7 @@ export class WorkbasketDistributionTargetsListComponent
});
});
this.selectedDistributionTargetsFilter$.pipe(takeUntil(this.destroy$)).subscribe((filter) => {
if (isEqual(this.filter, filter)) return;
if (typeof this.filter === 'undefined' || isEqual(this.filter, filter)) return;
this.filter = filter;
this.store
.dispatch(new FetchWorkbasketDistributionTargets(true))
@ -144,12 +145,8 @@ export class WorkbasketDistributionTargetsListComponent
.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);
if (this.distributionTargets.length === 0 && targetSide === Side.SELECTED) {
this.store.dispatch(new FetchAvailableDistributionTargets(false, this.filter));
}
});
}

View File

@ -90,6 +90,7 @@
*ngIf="displayingDistributionTargetsPicker"
[component]="'availableDistributionTargets'"
[transferDistributionTargetObservable]="transferDistributionTargetObservable"
id="dual-list-Left"
>
</taskana-administration-workbasket-distribution-targets-list>
@ -99,6 +100,7 @@
[hidden]="displayingDistributionTargetsPicker && !sideBySide"
[component]="'selectedDistributionTargets'"
[transferDistributionTargetObservable]="transferDistributionTargetObservable"
id="dual-list-Right"
>
</taskana-administration-workbasket-distribution-targets-list>
</div>

View File

@ -46,18 +46,6 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy
* would be ideal to completely redo whole components using drag and drop angular components and clearer logics
*/
ngOnInit() {
// saving workbasket distributions targets when existing workbasket was modified
this.ngxsActions$.pipe(ofActionCompleted(UpdateWorkbasket), takeUntil(this.destroy$)).subscribe(() => {
this.onSave();
});
// 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(() => {
this.onSave();
});
});
this.selectedWorkbasket$.pipe(takeUntil(this.destroy$)).subscribe((wb) => {
if (wb !== undefined && wb.workbasketId !== this.selectedWorkbasket?.workbasketId) {
if (this.selectedWorkbasket?.workbasketId) {

View File

@ -22,6 +22,7 @@ import { WorkbasketComponent } from '../../models/workbasket-component';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { ButtonAction } from '../../models/button-action';
import { AccessId } from '../../../shared/models/access-id';
import { cloneDeep } from 'lodash';
@Component({
selector: 'taskana-administration-workbasket-information',
@ -160,7 +161,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
} else {
this.store.dispatch(new UpdateWorkbasket(this.workbasket._links.self.href, this.workbasket)).subscribe(() => {
this.requestInProgressService.setRequestInProgress(false);
this.workbasketClone = { ...this.workbasket };
this.workbasketClone = cloneDeep(this.workbasket);
});
}
}

View File

@ -125,7 +125,7 @@ export class WorkbasketService {
url: string,
distributionTargetsIds: Set<string>
): Observable<WorkbasketDistributionTargets> {
return this.httpClient.put<WorkbasketDistributionTargets>(url, distributionTargetsIds);
return this.httpClient.put<WorkbasketDistributionTargets>(url, Array.from(distributionTargetsIds));
}
// DELETE

View File

@ -131,7 +131,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx.patchState({
selectedWorkbasket,
action: ACTION.READ,
badgeMessage: ``
badgeMessage: ''
});
ctx.dispatch(new GetWorkbasketAccessItems(ctx.getState().selectedWorkbasket._links.accessItems.href));
@ -144,6 +144,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
ctx.dispatch(new ClearWorkbasketFilter('selectedDistributionTargets'));
ctx.dispatch(new ClearWorkbasketFilter('availableDistributionTargets'));
ctx.dispatch(new FetchWorkbasketDistributionTargets(true));
ctx.dispatch(new FetchAvailableDistributionTargets(true));
})
);
}
@ -416,7 +418,9 @@ export class WorkbasketState implements NgxsAfterBootstrap {
take(1),
tap((wbt: WorkbasketDistributionTargets) => {
if (!refetchAll && workbasketDistributionTargets) {
wbt.distributionTargets = workbasketDistributionTargets.distributionTargets.concat(wbt.distributionTargets);
const completeArray = workbasketDistributionTargets.distributionTargets.concat(wbt.distributionTargets);
const idArrayNoDupe = [...new Set(completeArray.map((wb) => wb.workbasketId))];
wbt.distributionTargets = idArrayNoDupe.map((id) => completeArray.find((wb) => wb.workbasketId === id));
}
ctx.patchState({
workbasketDistributionTargets: wbt,
@ -468,7 +472,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(TransferDistributionTargets)
transferDistributionTargets(ctx: StateContext<WorkbasketStateModel>, action: TransferDistributionTargets): void {
let { workbasketDistributionTargets, availableDistributionTargets } = ctx.getState();
let { availableDistributionTargets, workbasketDistributionTargets } = ctx.getState();
const workbasketSummarySet = new Set(action.workbasketSummaries.map((wb) => wb.workbasketId));
availableDistributionTargets = cloneDeep(availableDistributionTargets);
workbasketDistributionTargets = cloneDeep(workbasketDistributionTargets);