TSK-1412: Distribution targets MD (#1365)

* TSK-1412: Refactor workbasket dual list to workbasket distribution targets list

* TSK-1412: Refactor workbasket dual list to workbasket distribution targets list

* TSK-1412: rework css layout to be more dynamic and resilient

* TSK-1412: new list, new dialog, new action bar

* TSK-1412: Refactor workbasket dual list to workbasket distribution targets list

* TSK-1412: rework css layout to be more dynamic and resilient

* TSK-1412: new list, new dialog, new action bar

* TSK-1412: update new workbasket distribution target list to load data internally

* TSK-1412: update distribution targets

* TSK-1421: enable multiple selection

* TSK-1412: Updated angular theme to match with taskana

* TSK-1412: quick bug fixes

* TSK-1412: Rework how workbasket distribution targets list behaves

* TSK-1412: Update workbasket distribution target list to new design

* TSK-1412: fixed filter function in distribution targets

* TSK-1412: remove unused component, rename correct CSS names

* TSK-1412: clean up code in workbasket distribution targets

* TSK-1412: fix bugs, rename variables

* TSK-1412: finalized jest tests

* TSK-1412: fix all other jest tests

* TSK-1412: disable devmode

* TSK-1412: remove unused imports, add tooltips
This commit is contained in:
Chi Nguyen 2020-12-08 16:11:13 +01:00 committed by GitHub
parent 4dd426e060
commit 900343b722
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 998 additions and 568 deletions

View File

@ -24,7 +24,8 @@
"styles": [
"./node_modules/@angular/material/prebuilt-themes/indigo-pink.css",
"./node_modules/bootstrap/dist/css/bootstrap.min.css",
"src/theme/_main.scss"
"src/theme/_main.scss",
"src/theme/custom-theme-material.scss"
],
"scripts": [
"node_modules/jquery/dist/jquery.min.js",

View File

@ -19,11 +19,15 @@ import { WorkbasketListToolbarComponent } from './components/workbasket-list-too
import { WorkbasketDetailsComponent } from './components/workbasket-details/workbasket-details.component';
import { WorkbasketInformationComponent } from './components/workbasket-information/workbasket-information.component';
import { WorkbasketDistributionTargetsComponent } from './components/workbasket-distribution-targets/workbasket-distribution-targets.component';
import { WorkbasketDualListComponent } from './components/workbasket-dual-list/workbasket-dual-list.component';
import { WorkbasketDistributionTargetsListComponent } from './components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component';
import { WorkbasketAccessItemsComponent } from './components/workbasket-access-items/workbasket-access-items.component';
import { ClassificationListComponent } from './components/classification-list/classification-list.component';
import { ClassificationDetailsComponent } from './components/classification-details/classification-details.component';
import { ImportExportComponent } from './components/import-export/import-export.component';
import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component';
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
/**
* Services
*/
@ -31,15 +35,16 @@ import { SavingWorkbasketService } from './services/saving-workbaskets.service';
import { ClassificationDefinitionService } from './services/classification-definition.service';
import { WorkbasketDefinitionService } from './services/workbasket-definition.service';
import { ImportExportService } from './services/import-export.service';
import { ClassificationOverviewComponent } from './components/classification-overview/classification-overview.component';
import { WorkbasketOverviewComponent } from './components/workbasket-overview/workbasket-overview.component';
/**
* Material Design
*/
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatSelectModule } from '@angular/material/select';
import { MatMenuModule } from '@angular/material/menu';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { AdministrationOverviewComponent } from './components/administration-overview/administration-overview.component';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatDividerModule } from '@angular/material/divider';
@ -72,7 +77,7 @@ const DECLARATIONS = [
WorkbasketDetailsComponent,
WorkbasketInformationComponent,
WorkbasketDistributionTargetsComponent,
WorkbasketDualListComponent,
WorkbasketDistributionTargetsListComponent,
ClassificationOverviewComponent,
ClassificationListComponent,
ClassificationTypesSelectorComponent,
@ -111,6 +116,7 @@ const DECLARATIONS = [
SavingWorkbasketService,
ClassificationCategoriesService,
ImportExportService
]
],
entryComponents: []
})
export class AdministrationModule {}

View File

@ -3,7 +3,7 @@
<div class="classification-details__wrapper" id="classification-details" *ngIf="classification && !requestInProgress">
<!-- TITLE + ACTION BUTTONS -->
<section class="classification-details__action-toolbar">
<mat-toolbar class="classification-details__action-toolbar">
<h4 class="classification-details__headline">{{classification.name}}&nbsp; [{{classification.type}}]
<span *ngIf="isCreatingNewClassification" class="badge warning"> {{badgeMessage$ | async}}</span>
</h4>
@ -39,7 +39,7 @@
</mat-menu>
</div>
</section>
</mat-toolbar>
<!-- DETAILED FIELDS -->
<div class="panel-body" style="padding: 0">
@ -80,17 +80,19 @@
<!-- SERVICE LEVEL AND PRIORITY-->
<div class="classification-details__service-and-priority">
<mat-form-field appearance="outline" class="classification-details__mat-form-field">
<mat-label> Service Level </mat-label>
<label for="classification-service-level"></label>
<input matInput type="text" required maxlength="255"
id="classification-service-level" placeholder="Service Level"
[(ngModel)]="classification.serviceLevel" name="classification.serviceLevel"
#serviceLevel="ngModel" (input)="validateInputOverflow(serviceLevel, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(serviceLevel.name)" class="error">{{lengthError}}</div>
<div class="classification-details__service">
<mat-form-field appearance="outline" class="classification-details__mat-form-field">
<mat-label> Service Level </mat-label>
<label for="classification-service-level"></label>
<input matInput type="text" required maxlength="255"
id="classification-service-level" placeholder="Service Level"
[(ngModel)]="classification.serviceLevel" name="classification.serviceLevel"
#serviceLevel="ngModel" (input)="validateInputOverflow(serviceLevel, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(serviceLevel.name)" class="error">{{lengthError}}</div>
</div>
<div>
<div class="classification-details__priority">
<!-- I replaced this component by the mat-form-field. Is this the same?
I don't understand all methods in the number-picker component.
<taskana-shared-number-picker [(ngModel)]="classification.priority"

View File

@ -1,7 +1,6 @@
@import 'src/theme/_colors.scss';
.classification-details {
width: 100%;
height: calc(100vh - 100px);
overflow-y: auto;
}
@ -12,12 +11,12 @@
/* ACTION TOOLBAR */
.classification-details__headline {
padding-top: 0.5rem;
font-size: 1.5rem;
padding: 0.5rem;
}
.classification-details__action-toolbar {
width: calc(100% - 500px);
position: fixed;
padding: 12px 32px 12px 24px;
padding: 8px 30px 12px 24px;
height: 68px;
background-color: #fff;
display: flex;
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
@ -98,11 +97,18 @@
.classification-details__service-and-priority {
display: flex;
justify-content: space-between;
width: 100%;
}
.classification-details__service {
width: 100%;
margin-right: 10px;
}
.classification-details__priority {
flex-grow: 1;
}
.classification-details__mat-form-field {
width: 70%;
width: 100%;
margin-right: 10px;
}

View File

@ -31,6 +31,7 @@ import { MatInputModule } from '@angular/material/input';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatToolbarModule } from '@angular/material/toolbar';
@Component({ selector: 'taskana-shared-field-error-display', template: '' })
class FieldErrorDisplayStub {
@ -120,6 +121,7 @@ describe('ClassificationDetailsComponent', () => {
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
FormsModule,
MatIconModule,
MatToolbarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,

View File

@ -2,7 +2,6 @@
.classification-list {
height: calc(100vh - 55px);
width: 500px;
}
.classification-list__action-toolbar {
padding: 0 16px;

View File

@ -1,10 +1,13 @@
.classification-overview {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
overflow: hidden;
align-items: stretch;
}
taskana-administration-classification-details {
width: 100%;
taskana-administration-classification-list {
width: 500px;
}
taskana-administration-classification-details {
flex-grow: 1;
}

View File

@ -1,9 +1,5 @@
@import 'src/theme/_colors.scss';
.workbasket-details {
width: 100%;
height: calc(100vh - 100px);
}
.workbasket-details__toolbar {
background-color: white;
padding: 16px 36px 12px 24px;

View File

@ -0,0 +1,65 @@
<div id="dual-list-Left" class="distribution-targets-list">
<mat-toolbar>
<span class="distribution-targets-list__header">{{header}}</span>
<button mat-flat-button class="distribution-targets-list__action-button" (click)="changeToolbarState(!toolbarState)"
>
<span *ngIf="!toolbarState">
Display filter
<mat-icon class="button-icon">filter_list</mat-icon>
</span>
<span *ngIf="toolbarState">
Hide filter
<mat-icon class="button-icon">keyboard_arrow_up</mat-icon>
</span>
</button>
<span style="flex: 1 1 auto"> </span>
<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>
</mat-toolbar>
<taskana-shared-filter *ngIf="toolbarState" (performFilter)="performAvailableFilter($event)"
component="distribution-target" (inputComponent)="setComponent($event)"></taskana-shared-filter>
<!-- WORKBASKET LIST -->
<div class="distribution-targets-list__list" infiniteScroll [infiniteScrollDistance]="1" [infiniteScrollThrottle]="50" (scrolled)="onScroll()"
[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 | selectWorkbaskets: distributionTargetsSelected: side"
[selected]="workbasket.selected" type="text"
(click)="workbasket.selected = !workbasket.selected"
[value]="workbasket.workbasketId">
<div class="distribution-targets-list__item-wrapper">
<div class="distribution-targets-list__item-icon">
<taskana-administration-icon-type [type]="workbasket.type" size="large" tooltip="true"></taskana-administration-icon-type>
</div>
<div class="distribution-targets-list__item-info">
<p>
<b>{{workbasket.name}}</b>, <i>{{workbasket.key}} </i>
</p>
<p>{{workbasket.description}} &nbsp;</p>
<p>{{workbasket.owner}} &nbsp;</p>
</div>
<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>
</div>
<mat-divider></mat-divider>
</mat-list-option>
</mat-selection-list>
</div>
<div class="distribution-targets-list__empty-list" *ngIf="distributionTargets?.length == 0">
There is currently no distributed workbasket
</div>
</div>

View File

@ -0,0 +1,61 @@
@import 'src/theme/_colors.scss';
.distribution-targets-list {
&__header {
text-overflow: ellipsis;
overflow: hidden;
max-width: 50%;
@media screen and (max-width: 1280px) {
max-width: 100px;
}
}
&__action-button {
margin-left: 0.5rem;
}
&__list {
min-width: 100%;
background-color: white;
border-radius: 10px;
overflow-y: scroll;
height: calc(100vh - 360px) !important;
@media screen and (max-width: 991px) {
height: calc((100vh - 315px));
min-height: 83px;
}
}
&__item-wrapper {
display: flex;
}
&__item-icon {
padding: 32px 24px 24px 8px;
}
&__item-info {
display: block;
padding: 8px 0;
}
&__empty-list {
height: calc(100vh - 360px);
border-radius: 10px;
background-color: white;
font-size: 24px;
font-weight: bold;
color: $dark-green;
display: flex;
align-items: center;
justify-content: center;
}
}
.mat-list-item {
height: 90px !important;
}
.mat-list-single-selected-option {
background-color: $green !important;
color: white;
}
p {
margin: 0;
}

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
import { WorkbasketDualListComponent } from './workbasket-dual-list.component';
import { WorkbasketDistributionTargetsListComponent } from './workbasket-distribution-targets-list.component';
import { Filter } from '../../../shared/models/filter';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import { ICONTYPES } from '../../../shared/models/icon-types';
@ -8,6 +8,9 @@ import { SelectWorkBasketPipe } from '../../../shared/pipes/select-workbaskets.p
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 { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatListModule } from '@angular/material/list';
@Component({ selector: 'taskana-shared-filter', template: '' })
class FilterStub {
@ -25,33 +28,39 @@ class IconTypeStub {
@Input() text: string;
}
describe('WorkbasketDualListComponent', () => {
let fixture: ComponentFixture<WorkbasketDualListComponent>;
describe('WorkbasketDistributionTargetsListComponent', () => {
let fixture: ComponentFixture<WorkbasketDistributionTargetsListComponent>;
let debugElement: DebugElement;
let component: WorkbasketDualListComponent;
let component: WorkbasketDistributionTargetsListComponent;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [InfiniteScrollModule, BrowserAnimationsModule],
declarations: [WorkbasketDualListComponent, FilterStub, SpinnerStub, IconTypeStub, SelectWorkBasketPipe],
imports: [MatIconModule, MatToolbarModule, MatListModule, InfiniteScrollModule, BrowserAnimationsModule],
declarations: [
WorkbasketDistributionTargetsListComponent,
FilterStub,
SpinnerStub,
IconTypeStub,
SelectWorkBasketPipe
],
providers: []
}).compileComponents();
fixture = TestBed.createComponent(WorkbasketDualListComponent);
fixture = TestBed.createComponent(WorkbasketDistributionTargetsListComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
component.distributionTargets = workbasketReadStateMock.paginatedWorkbasketsSummary.workbaskets;
component.distributionTargetsSelected = [];
component.side = Side.LEFT;
component.side = Side.AVAILABLE;
}));
it('should create component', () => {
expect(component).toBeTruthy();
});
it('should set sideNumber to 0 when side is Side.LEFT', () => {
it('should set sideNumber to 0 when side is Side.AVAILABLE', () => {
fixture.detectChanges();
expect(component.sideNumber).toBe(0);
expect(component.side).toBe(Side.AVAILABLE);
});
it('should select all distribution targets', () => {
@ -67,13 +76,6 @@ describe('WorkbasketDualListComponent', () => {
expect(scrollingEmitSpy).toHaveBeenCalledWith(component.side);
});
it('should emit filter model and side when performing filter', () => {
const performDualListFilterSpy = jest.spyOn(component.performDualListFilter, 'emit');
const filterModelMock: Filter = { filterParams: 'filter' };
component.performAvailableFilter(filterModelMock);
expect(performDualListFilterSpy).toHaveBeenCalledWith({ filterBy: filterModelMock, side: component.side });
});
it('should change toolbar state', () => {
expect(component.toolbarState).toBe(false);
component.changeToolbarState(true);
@ -83,7 +85,7 @@ describe('WorkbasketDualListComponent', () => {
it('should display all available workbaskets', () => {
fixture.detectChanges();
const distributionTargetList = debugElement.nativeElement.getElementsByClassName(
'workbasket-list__distribution-targets'
'workbasket-distribution-targets__workbaskets-item'
);
expect(distributionTargetList).toHaveLength(5);
});

View File

@ -0,0 +1,75 @@
import {
Component,
OnInit,
Input,
Output,
EventEmitter,
AfterContentChecked,
ChangeDetectorRef,
ViewChild
} from '@angular/core';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { Filter } from 'app/shared/models/filter';
import { expandDown } from 'app/shared/animations/expand.animation';
import { Side } from '../workbasket-distribution-targets/workbasket-distribution-targets.component';
import { MatSelectionList } from '@angular/material/list';
@Component({
selector: 'taskana-administration-workbasket-distribution-targets-list',
templateUrl: './workbasket-distribution-targets-list.component.html',
styleUrls: ['./workbasket-distribution-targets-list.component.scss'],
animations: [expandDown]
})
export class WorkbasketDistributionTargetsListComponent implements OnInit, AfterContentChecked {
@Input() distributionTargets: WorkbasketSummary[];
@Input() distributionTargetsSelected: WorkbasketSummary[];
@Output() performDualListFilter = new EventEmitter<{ filterBy: Filter; side: Side }>();
@Input() requestInProgress = false;
@Input() loadingItems? = false;
@Input() side: Side;
@Input() header: string;
@Output() scrolling = new EventEmitter<Side>();
@Input() allSelected;
@Output() allSelectedChange = new EventEmitter<boolean>();
toolbarState = false;
component = '';
@ViewChild('workbasket') distributionTargetsList: MatSelectionList;
constructor(private changeDetector: ChangeDetectorRef) {}
ngOnInit() {
this.allSelected = !this.allSelected;
}
ngAfterContentChecked(): void {
this.changeDetector.detectChanges();
}
selectAll(selected: boolean) {
if (typeof this.distributionTargetsList !== 'undefined') {
this.allSelected = !this.allSelected;
this.distributionTargetsList.options.map((item) => (item['selected'] = selected));
}
this.distributionTargets.map((item) => (item['selected'] = selected));
this.allSelectedChange.emit(this.allSelected);
}
setComponent(component: string) {
this.component = component;
}
onScroll() {
this.scrolling.emit(this.side);
}
performAvailableFilter(filterModel: Filter) {
if (this.component === 'distribution-target') {
this.performDualListFilter.emit({ filterBy: filterModel, side: this.side });
}
}
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
}

View File

@ -1,77 +1,111 @@
<div *ngIf="workbasket" id="wb-information" class="panel panel-default">
<!-- ACTION TOOLBAR-->
<!--
<div class="panel-heading">
<div class="pull-right btn-group">
<button type="button" (click)="onSave()" [disabled]="action === 'COPY'" data-toggle="tooltip" title="Save"
class="btn btn-default btn-primary">
<span class="material-icons md-20">save</span>
</button>
<button type="button" (click)="onClear()" data-toggle="tooltip" title="Undo Changes"
class="btn btn-default">
<span class="material-icons md-20 blue">undo</span>
</button>
</div>
<h4 class="panel-header">{{workbasket.name}}
<span *ngIf="!workbasket.workbasketId" class="badge warning"> {{badgeMessage}}</span>
</h4>
<div *ngIf="workbasket" 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()">
Display side-by-side
<mat-icon class="distribution-targets-list__button-icon">view_week</mat-icon>
</button>
<button mat-flat-button class="distribution-targets-list__action-button distribution-targets-list__toggle-view-button"
*ngIf="sideBySide" (click)="toggleSideBySideView()">
Display in single-view
<mat-icon class="distribution-targets-list__button-icon">view_agenda</mat-icon>
</button>
<!-- SIDE BY SIDE VIEW BUTTONS -->
<div class="distribution-targets-list__action-buttons" *ngIf="sideBySide">
<div class="distribution-targets-list__action-buttons--selected"
style="justify-content: flex-end; margin-right: 2%">
<button mat-flat-button color="warn"
class="distribution-targets-list__action-button distribution-targets-list-dialog__remove-button"
(click)="moveDistributionTargets(side.SELECTED)">
Remove selected distribution target
<mat-icon>remove</mat-icon>
</button>
</div>
<span style="flex-grow: 1"> </span>
<div class="distribution-targets-list__action-buttons--chooser"
style="justify-content: flex-end;">
<button mat-flat-button color="accent"
class="distribution-targets-list__action-button distribution-targets-list-dialog__add-button"
(click)="moveDistributionTargets(side.AVAILABLE)">
Add selected distribution targets
<mat-icon>add</mat-icon>
</button>
</div>
</div>
-->
<!-- DISTRIBUTION TABLE-->
<div #panelBody class="panel-body">
<!-- DISTRIBUTION LEFT LIST -->
<div class="dual-list list-left col-xs-12 col-md-5-6 container">
<taskana-administration-workbasket-dual-list #dualListLeft id="dual-list-Left" header="Available distribution targets"
[distributionTargets]="distributionTargetsLeft"
[distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
(scrolling)="onScroll($event)"
[side]="side.LEFT"
[requestInProgress]="requestInProgressLeft"
[loadingItems]="loadingItems"
[(allSelected)]="selectAllLeft"></taskana-administration-workbasket-dual-list>
</div>
<!-- SINGLE VIEW BUTTONS WHEN DISPLAYING SELECTED DISTRIBUTION TARGETS -->
<div class="distribution-targets-list__action-buttons distribution-targets-list__action-buttons--selected"
*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)">
Remove selected distribution target
<mat-icon>remove</mat-icon>
</button>
<!-- DISTRIBUTION ACTION BUTTONS -->
<div class="hidden-xs hidden-sm col-md-1 list-arrows text-center button-margin-top">
<button (click)="moveDistributionTargets(side.LEFT)"
[disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-right" data-toggle="tooltip"
title="Move to selected distribution targets">
<span class="material-icons md-20 blue">chevron_right</span>
</button>
<button (click)="moveDistributionTargets(side.RIGHT)"
[disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-left" data-toggle="tooltip"
title="Move to available distribution targets">
<span class="material-icons md-20 blue">chevron_left</span>
</button>
</div>
<div class="hidden visible-xs visible-sm col-xs-12 list-arrows text-center">
<button (click)="moveDistributionTargets(side.LEFT)"
[disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-down" data-toggle="tooltip"
title="Move to selected distribution targets">
<span class="material-icons md-20 blue">expand_more</span>
</button>
<button (click)="moveDistributionTargets(side.RIGHT)"
[disabled]="requestInProgressLeft || requestInProgressRight"
class="btn btn-default move-up" data-toggle="tooltip"
title="Move to available distribution targets">
<span class="material-icons md-20 blue">expand_less</span>
</button>
</div>
<span style="flex: 1 1 auto"> </span>
<!-- DISTRIBUTION RIGHT LIST -->
<div class="dual-list list-right col-xs-12 col-md-5-6 container">
<taskana-administration-workbasket-dual-list #dualListRight id="dual-list-right" header="Selected distribution targets"
[distributionTargets]="distributionTargetsRight"
[distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
[requestInProgress]="requestInProgressRight"
[side]="side.RIGHT"
[(allSelected)]="selectAllRight"></taskana-administration-workbasket-dual-list>
</div>
<button mat-stroked-button
class="distribution-targets-list__action-button distribution-targets-list-dialog__display-button"
(click)="toggleDistributionTargetsPicker()">
Display available distribution targets
<mat-icon>launch</mat-icon>
</button>
</div>
<!-- SINGLE VIEW BUTTONS WHEN CHOOSING DISTRIBUTION TARGETS -->
<div class="distribution-targets-list__action-buttons distribution-targets-list__action-buttons--chooser"
*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)">
Add selected distribution targets
<mat-icon>add</mat-icon>
</button>
<span style="flex: 1 1 auto"> </span>
<button mat-flat-button color="warn" class="distribution-targets-list-dialog__check-button"
(click)="toggleDistributionTargetsPicker()">
Close selection
<mat-icon>close</mat-icon>
</button>
</div>
</mat-toolbar>
<div class="distribution-targets-list__lists"
[ngClass]="sideBySide ? 'distribution-targets-list__lists--side' : 'distribution-targets-list__lists--single'">
<!-- DISTRIBUTION TABLE-->
<taskana-administration-workbasket-distribution-targets-list
[ngClass]="sideBySide ? 'distribution-targets-list__lists--left-side' : ''"
header="Selected distribution targets"
[distributionTargets]="distributionTargetsRight"
[distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
[side]="side.SELECTED"
(scrolling)="onScroll()"
[loadingItems]="loadingItems"
[(allSelected)]="selectAllLeft"
[hidden]="displayingDistributionTargetsPicker && !sideBySide"
>
</taskana-administration-workbasket-distribution-targets-list>
<taskana-administration-workbasket-distribution-targets-list
header="Available distribution targets"
[distributionTargets]="availableDistributionTargets"
[side]="side.AVAILABLE"
[distributionTargetsSelected]="distributionTargetsSelected"
(performDualListFilter)="performFilter($event)"
(scrolling)="onScroll()"
[loadingItems]="loadingItems"
[(allSelected)]="selectAllRight"
*ngIf="displayingDistributionTargetsPicker"
>
</taskana-administration-workbasket-distribution-targets-list>
</div>
</div>

View File

@ -1,13 +1,27 @@
.button-margin-top {
margin-top: 100px;
}
@import 'src/theme/_colors.scss';
.list-arrows > button {
margin: 10px 0px;
}
.col-md-5-6 {
@media (min-width: 992px) {
width: 45.82%;
.distribution-targets-list {
&__action-buttons {
display: flex;
flex-grow: 1;
}
&__action-button {
margin-right: 0.5rem;
}
&__button-icon {
color: #555;
}
&__lists {
padding: 0 16px 16px 16px;
background-color: $light-grey;
&--side {
display: flex;
taskana-administration-workbasket-distribution-targets-list {
width: 50%;
}
}
&--left-side {
margin-right: 1%;
}
}
}

View File

@ -0,0 +1,156 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
import { Side, WorkbasketDistributionTargetsComponent } from './workbasket-distribution-targets.component';
import { WorkbasketSummary } from '../../../shared/models/workbasket-summary';
import { Filter } from '../../../shared/models/filter';
import { MatIconModule } from '@angular/material/icon';
import { MatToolbarModule } from '@angular/material/toolbar';
import { MatButtonModule } from '@angular/material/button';
import { Observable, of } from 'rxjs';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
import { SavingWorkbasketService } from '../../services/saving-workbaskets.service';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import { Actions, NgxsModule, Store } from '@ngxs/store';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
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';
const routeParamsMock = { id: 'workbasket' };
const activatedRouteMock = {
firstChild: {
params: of(routeParamsMock)
}
};
@Component({ selector: 'taskana-administration-workbasket-distribution-targets-list', template: '' })
class WorkbasketDistributionTargetsListStub {
@Input() distributionTargets: WorkbasketSummary[];
@Input() distributionTargetsSelected: WorkbasketSummary[];
@Output() performDualListFilter = new EventEmitter<{ filterBy: Filter; side: Side }>();
@Input() requestInProgress = false;
@Input() loadingItems? = false;
@Input() side: Side;
@Input() header: string;
@Output() scrolling = new EventEmitter<Side>();
@Input() allSelected;
@Output() allSelectedChange = new EventEmitter<boolean>();
}
const workbasketServiceSpy = jest.fn().mockImplementation(
(): Partial<WorkbasketService> => ({
getWorkBasketsSummary: jest.fn().mockReturnValue(of()),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of())
})
);
const savingWorkbasketServiceSpy = jest.fn().mockImplementation(
(): Partial<SavingWorkbasketService> => ({
triggeredDistributionTargetsSaving: jest.fn().mockReturnValue(of())
})
);
const notificationsServiceSpy = jest.fn().mockImplementation(
(): Partial<NotificationService> => ({
showToast: jest.fn().mockReturnValue(true)
})
);
const requestInProgressServiceSpy = jest.fn().mockImplementation(
(): Partial<RequestInProgressService> => ({
setRequestInProgress: jest.fn().mockReturnValue(of())
})
);
describe('WorkbasketDistributionTargetsComponent', () => {
let fixture: ComponentFixture<WorkbasketDistributionTargetsComponent>;
let debugElement: DebugElement;
let component: 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, useClass: workbasketServiceSpy },
{ provide: SavingWorkbasketService, useClass: savingWorkbasketServiceSpy },
{ provide: NotificationService, useClass: notificationsServiceSpy },
{ provide: ActivatedRoute, useValue: activatedRouteMock },
{ provide: RequestInProgressService, useClass: requestInProgressServiceSpy }
]
}).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();
}));
it('should create component', () => {
expect(component).toBeTruthy();
});
it('should display side-by-side view by default', () => {
expect(component.sideBySide).toBe(true);
expect(debugElement.nativeElement.querySelector('.distribution-targets-list__lists--side')).toBeTruthy();
});
it('should display single view when toggle view button is clicked', () => {
const toggleViewButton = debugElement.nativeElement.querySelector('.distribution-targets-list__toggle-view-button');
expect(toggleViewButton).toBeTruthy();
toggleViewButton.click();
fixture.detectChanges();
expect(component.sideBySide).toBe(false);
expect(debugElement.nativeElement.querySelector('.distribution-targets-list__lists--side')).toBeFalsy();
});
it('should get available and selected distribution targets', () => {
component.getWorkbaskets();
expect(component.availableDistributionTargets).toHaveLength(8); //mock-data has 8 entries
expect(component.distributionTargetsSelected).toHaveLength(3); //mock-data has 3 entries
});
it('should emit filter model and side when performing filter', () => {
const performDualListFilterSpy = jest.spyOn(component, 'performFilter');
const filterModelMock: Filter = { filterParams: { name: '', description: '', owner: '', type: '', key: '' } };
component.performFilter({ filterBy: filterModelMock, side: component.side });
expect(performDualListFilterSpy).toHaveBeenCalledWith({ filterBy: filterModelMock, side: component.side });
});
it('should move distribution targets to selected list', () => {
component.availableDistributionTargets[0]['selected'] = true; // select first item in available array
component.distributionTargetsRight = component.distributionTargetsSelected;
component.moveDistributionTargets(Side.AVAILABLE);
expect(component.distributionTargetsSelected).toHaveLength(4); // mock-data only has 3
});
it('should reset distribution targets to last state when undo is called', () => {
component.distributionTargetsClone = component.availableDistributionTargets;
component.distributionTargetsSelectedClone = component.distributionTargetsSelected;
component.availableDistributionTargets[0]['selected'] = true; // select first item in available array
component.distributionTargetsRight = component.distributionTargetsSelected;
component.moveDistributionTargets(Side.AVAILABLE);
expect(component.distributionTargetsSelected).toHaveLength(4); // mock-data only has 3
component.onClear();
expect(component.distributionTargetsSelected).toHaveLength(3);
});
});

View File

@ -1,4 +1,4 @@
import { Component, ElementRef, Input, OnChanges, OnDestroy, OnInit, SimpleChanges, ViewChild } from '@angular/core';
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { Observable, Subject } from 'rxjs';
import { Workbasket } from 'app/shared/models/workbasket';
@ -6,53 +6,50 @@ 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 { ACTION } from 'app/shared/models/action';
import { WorkbasketService } from 'app/shared/services/workbasket/workbasket.service';
import { SavingWorkbasketService, SavingInformation } from 'app/administration/services/saving-workbaskets.service';
import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { Page } from 'app/shared/models/page';
import { OrientationService } from 'app/shared/services/orientation/orientation.service';
import { Orientation } from 'app/shared/models/orientation';
import { Select, Store } from '@ngxs/store';
import { take, takeUntil } from 'rxjs/operators';
import { filter, takeUntil } from 'rxjs/operators';
import { NOTIFICATION_TYPES } from '../../../shared/models/notifications';
import { NotificationService } from '../../../shared/services/notifications/notification.service';
import {
GetAvailableDistributionTargets,
GetWorkbasketDistributionTargets,
GetWorkbasketsSummary,
UpdateWorkbasketDistributionTargets
} from '../../../shared/store/workbasket-store/workbasket.actions';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { WorkbasketStateModel } from '../../../shared/store/workbasket-store/workbasket.state';
import { MatDialog } from '@angular/material/dialog';
import { ButtonAction } from '../../models/button-action';
export enum Side {
LEFT,
RIGHT
AVAILABLE,
SELECTED
}
@Component({
selector: 'taskana-administration-workbasket-distribution-targets',
templateUrl: './workbasket-distribution-targets.component.html',
styleUrls: ['./workbasket-distribution-targets.component.scss']
})
export class WorkbasketDistributionTargetsComponent implements OnInit, OnChanges, OnDestroy {
export class WorkbasketDistributionTargetsComponent implements OnInit, OnDestroy {
@Input()
workbasket: Workbasket;
@Input()
action: ACTION;
badgeMessage = '';
toolbarState = false;
sideBySide = true;
displayingDistributionTargetsPicker = true;
distributionTargetsSelectedResource: WorkbasketDistributionTargets;
distributionTargetsLeft: Array<WorkbasketSummary> = [];
availableDistributionTargets: Array<WorkbasketSummary> = [];
distributionTargetsRight: Array<WorkbasketSummary> = [];
distributionTargetsSelected: Array<WorkbasketSummary>;
distributionTargetsClone: Array<WorkbasketSummary>;
distributionTargetsSelectedClone: Array<WorkbasketSummary>;
requestInProgressLeft = false;
requestInProgressRight = false;
loadingItems = false;
side = Side;
private initialized = false;
@ -61,56 +58,37 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnChanges
selectAllLeft = false;
selectAllRight = false;
@ViewChild('panelBody')
panelBody: ElementRef;
@Select(WorkbasketSelectors.workbasketDistributionTargets)
workbasketDistributionTargets$: Observable<WorkbasketDistributionTargets>;
@Select(WorkbasketSelectors.availableDistributionTargets)
availableDistributionTargets$: Observable<WorkbasketSummary[]>;
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
destroy$ = new Subject<void>();
constructor(
private workbasketService: WorkbasketService,
private savingWorkbaskets: SavingWorkbasketService,
private requestInProgressService: RequestInProgressService,
private orientationService: OrientationService,
private notificationsService: NotificationService,
private store: Store
private store: Store,
public matDialog: MatDialog
) {}
/**
* 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() {
this.workbasketDistributionTargets$.subscribe((workbasketDistributionTargets) => {
if (typeof workbasketDistributionTargets !== 'undefined') {
this.distributionTargetsSelectedResource = { ...workbasketDistributionTargets };
this.distributionTargetsSelected = this.distributionTargetsSelectedResource.distributionTargets;
this.distributionTargetsSelectedClone = { ...this.distributionTargetsSelected };
TaskanaQueryParameters.page = 1;
this.calculateNumberItemsList();
this.getWorkbaskets();
}
});
}
ngOnChanges(changes: SimpleChanges) {
if (changes.action) {
this.setBadge();
}
}
onScroll(side: Side) {
if (side === this.side.LEFT && this.page.totalPages > TaskanaQueryParameters.page) {
this.loadingItems = true;
this.getNextPage(side);
}
}
init() {
this.onRequest();
if (!this.workbasket._links.distributionTargets) {
return;
}
this.store.dispatch(new GetWorkbasketDistributionTargets(this.workbasket._links.distributionTargets.href));
this.store.dispatch(new GetAvailableDistributionTargets());
this.availableDistributionTargets$.pipe(takeUntil(this.destroy$)).subscribe((availableDistributionTargets) => {
this.availableDistributionTargets = availableDistributionTargets;
});
this.savingWorkbaskets
.triggeredDistributionTargetsSaving()
.pipe(takeUntil(this.destroy$))
@ -121,117 +99,130 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnChanges
}
});
this.orientationService
.getOrientation()
.pipe(takeUntil(this.destroy$))
.subscribe(() => {
this.calculateNumberItemsList();
this.workbasketDistributionTargets$.subscribe((workbasketDistributionTargets) => {
if (typeof workbasketDistributionTargets !== 'undefined') {
this.distributionTargetsSelectedResource = { ...workbasketDistributionTargets };
this.distributionTargetsSelected = this.distributionTargetsSelectedResource.distributionTargets;
this.distributionTargetsSelectedClone = { ...this.distributionTargetsSelected };
TaskanaQueryParameters.page = 1;
this.getWorkbaskets();
}
});
this.buttonAction$
.pipe(takeUntil(this.destroy$))
.pipe(filter((buttonAction) => typeof buttonAction !== 'undefined'))
.subscribe((button) => {
switch (button) {
case ButtonAction.SAVE:
this.onSave();
break;
case ButtonAction.UNDO:
this.onClear();
break;
default:
break;
}
});
}
onScroll() {
if (this.page.totalPages > TaskanaQueryParameters.page) {
this.loadingItems = true;
this.getNextPage();
}
}
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
toggleDistributionTargetsPicker() {
this.displayingDistributionTargetsPicker = !this.displayingDistributionTargetsPicker;
}
getWorkbaskets(side?: Side) {
if (this.distributionTargetsSelected && !this.initialized) {
this.initialized = true;
TaskanaQueryParameters.pageSize = this.cards + this.distributionTargetsSelected.length;
}
// TODO: Implement this into NGXS
this.workbasketService
.getWorkBasketsSummary(true)
.pipe(takeUntil(this.destroy$))
.subscribe((distributionTargetsAvailable: WorkbasketSummaryRepresentation) => {
if (TaskanaQueryParameters.page === 1) {
this.distributionTargetsLeft = [];
this.availableDistributionTargets = [];
this.page = distributionTargetsAvailable.page;
}
if (side === this.side.LEFT) {
this.distributionTargetsLeft.push(...distributionTargetsAvailable.workbaskets);
} else if (side === this.side.RIGHT) {
if (side === this.side.AVAILABLE) {
this.availableDistributionTargets.push(...distributionTargetsAvailable.workbaskets);
} else if (side === this.side.SELECTED) {
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable.workbaskets);
} else {
this.distributionTargetsLeft.push(...distributionTargetsAvailable.workbaskets);
this.availableDistributionTargets.push(...distributionTargetsAvailable.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable.workbaskets);
this.distributionTargetsClone = Object.assign([], distributionTargetsAvailable.workbaskets);
}
this.onRequest(true);
});
}
getNextPage(side?: Side) {
TaskanaQueryParameters.page += 1;
this.getWorkbaskets(side);
}
performFilter(dualListFilter: any) {
this.fillDistributionTargets(dualListFilter.side, undefined);
this.onRequest(false, dualListFilter.side);
this.store
.dispatch(
new GetWorkbasketsSummary(
true,
'',
'',
'',
dualListFilter.filterBy.filterParams.name,
dualListFilter.filterBy.filterParams.description,
'',
dualListFilter.filterBy.filterParams.owner,
dualListFilter.filterBy.filterParams.type,
'',
dualListFilter.filterBy.filterParams.key,
'',
true
)
this.workbasketService
.getWorkBasketsSummary(
true,
'',
'',
'',
dualListFilter.filterBy.filterParams.name,
dualListFilter.filterBy.filterParams.description,
'',
dualListFilter.filterBy.filterParams.owner,
dualListFilter.filterBy.filterParams.type,
'',
dualListFilter.filterBy.filterParams.key,
'',
true
)
.subscribe((state: WorkbasketStateModel) => {
this.fillDistributionTargets(dualListFilter.side, state.paginatedWorkbasketsSummary.workbaskets);
this.onRequest(true, dualListFilter.side);
.pipe(takeUntil(this.destroy$))
.subscribe((distributionTargetsAvailable: WorkbasketSummaryRepresentation) => {
this.fillDistributionTargets(dualListFilter.side, []);
if (TaskanaQueryParameters.page === 1) {
this.availableDistributionTargets = [];
this.page = distributionTargetsAvailable.page;
}
if (dualListFilter.side === this.side.AVAILABLE) {
this.availableDistributionTargets.push(...distributionTargetsAvailable.workbaskets);
} else if (dualListFilter.side === this.side.SELECTED) {
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable.workbaskets);
} else {
this.availableDistributionTargets.push(...distributionTargetsAvailable.workbaskets);
this.distributionTargetsRight = Object.assign([], distributionTargetsAvailable.workbaskets);
this.distributionTargetsClone = Object.assign([], distributionTargetsAvailable.workbaskets);
}
});
}
onSave() {
this.requestInProgressService.setRequestInProgress(true);
this.store
.dispatch(
new UpdateWorkbasketDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href,
this.getSeletedIds()
)
this.store.dispatch(
new UpdateWorkbasketDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href,
this.getSelectedIds()
)
.subscribe(
() => {
this.requestInProgressService.setRequestInProgress(false);
return true;
},
(error) => {
this.requestInProgressService.setRequestInProgress(false);
return false;
}
);
/* TODO: OLD IMPLEMENTATION, KEPT HERE FOR REFERENCE
this.workbasketService.updateWorkBasketsDistributionTargets(
this.distributionTargetsSelectedResource._links.self.href, this.getSeletedIds()
).subscribe(response => {
this.requestInProgressService.setRequestInProgress(false);
this.distributionTargetsSelected = response.distributionTargets;
this.distributionTargetsSelectedClone = Object.assign([], this.distributionTargetsSelected);
this.distributionTargetsClone = Object.assign([], this.distributionTargetsLeft);
this.notificationsService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', this.workbasket.name]])
);
return true;
},
error => {
this.notificationsService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
this.requestInProgressService.setRequestInProgress(false);
return false;
});
*/
);
return false;
}
moveDistributionTargets(side: number) {
if (side === Side.LEFT) {
const itemsLeft = this.distributionTargetsLeft.length;
if (side === Side.AVAILABLE) {
const itemsLeft = this.availableDistributionTargets.length;
const itemsRight = this.distributionTargetsRight.length;
const itemsSelected = this.getSelectedItems(this.distributionTargetsLeft);
const itemsSelected = this.getSelectedItems(this.availableDistributionTargets);
this.distributionTargetsSelected = [...this.distributionTargetsSelected, ...itemsSelected];
this.distributionTargetsRight = this.distributionTargetsRight.concat(itemsSelected);
if (
@ -245,102 +236,52 @@ export class WorkbasketDistributionTargetsComponent implements OnInit, OnChanges
const itemsSelected = this.getSelectedItems(this.distributionTargetsRight);
this.distributionTargetsSelected = this.removeSelectedItems(this.distributionTargetsSelected, itemsSelected);
this.distributionTargetsRight = this.removeSelectedItems(this.distributionTargetsRight, itemsSelected);
this.distributionTargetsLeft = this.distributionTargetsLeft.concat(itemsSelected);
this.availableDistributionTargets = this.availableDistributionTargets.concat(itemsSelected);
this.unselectItems(itemsSelected);
}
}
onClear() {
this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT);
this.distributionTargetsLeft = Object.assign([], this.distributionTargetsClone);
this.availableDistributionTargets = Object.assign([], this.distributionTargetsClone);
this.distributionTargetsRight = Object.assign([], this.distributionTargetsSelectedClone);
this.distributionTargetsSelected = Object.assign([], this.distributionTargetsSelectedClone);
}
calculateNumberItemsList() {
if (this.panelBody) {
const cardHeight = 72;
const unusedHeight = 100;
this.cards =
this.orientationService.calculateNumberItemsList(
this.panelBody.nativeElement.offsetHeight,
cardHeight,
unusedHeight,
true
) + 1; // TODO: warum +1
}
}
fillDistributionTargets(side: Side, workbaskets: WorkbasketSummary[]) {
this.distributionTargetsLeft = side === Side.LEFT ? workbaskets : this.distributionTargetsLeft;
this.distributionTargetsRight = side === Side.RIGHT ? workbaskets : this.distributionTargetsRight;
}
getNextPage(side: Side) {
TaskanaQueryParameters.page += 1;
this.getWorkbaskets(side);
}
setBadge() {
if (this.action === ACTION.COPY) {
this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`;
}
this.availableDistributionTargets = side === Side.AVAILABLE ? workbaskets : this.availableDistributionTargets;
this.distributionTargetsRight = side === Side.SELECTED ? workbaskets : this.distributionTargetsRight;
}
getSelectedItems(originList: any): Array<any> {
return originList.filter((item: any) => item.selected === true);
}
unselectItems(originList: any): Array<any> {
// eslint-disable-next-line no-restricted-syntax
for (const item of originList) {
if (item.selected && item.selected === true) {
unselectItems(originList: Array<any>): Array<any> {
return originList
.filter((item) => item.selected)
.map((item) => {
item.selected = false;
}
}
return originList;
});
}
removeSelectedItems(originList: any, selectedItemList) {
removeSelectedItems(originList, selectedItemList) {
const copyList = [...originList];
for (let index = originList.length - 1; index >= 0; index--) {
if (selectedItemList.some((itemToRemove) => originList[index].workbasketId === itemToRemove.workbasketId)) {
originList.splice(index, 1);
copyList.splice(index, 1);
}
}
return originList;
return copyList;
}
onRequest(finished: boolean = false, side?: Side) {
this.loadingItems = false;
const inProgress = !finished;
switch (side) {
case Side.LEFT:
this.requestInProgressLeft = inProgress;
break;
case Side.RIGHT:
this.requestInProgressRight = inProgress;
break;
default:
this.requestInProgressLeft = inProgress;
this.requestInProgressRight = inProgress;
}
getSelectedIds(): Array<string> {
return this.distributionTargetsSelected.map((distributionTarget) => distributionTarget.workbasketId);
}
getSeletedIds(): Array<string> {
const distributionTargetsSelelected: Array<string> = [];
this.distributionTargetsSelected.forEach((item) => {
distributionTargetsSelelected.push(item.workbasketId);
});
return distributionTargetsSelelected;
}
private uncheckSelectAll(side: number) {
if (side === Side.LEFT && this.selectAllLeft) {
this.selectAllLeft = false;
}
if (side === Side.RIGHT && this.selectAllRight) {
this.selectAllRight = false;
}
toggleSideBySideView() {
this.sideBySide = !this.sideBySide;
this.displayingDistributionTargetsPicker = true; //always display picker when toggle from side-by-side to single
}
ngOnDestroy() {

View File

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

View File

@ -1,112 +0,0 @@
$selected-item: #e3f3f5;
.dual-list {
min-height: 250px;
padding: 0px;
background-color: #f5f5f5;
border: 1px solid #e3e3e3;
-webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);
& .row {
padding: 0px 0px 0px 3px;
}
& .row:first {
border-top: 1px solid #ddd;
}
& div.pull-right {
margin-right: 17px;
}
& > .list-group {
margin-bottom: 0px;
margin-top: 0px;
}
& > .list-group > li {
border-left: none;
border-right: none;
}
overflow-x: hidden;
overflow-y: hidden;
@media screen and (max-width: 991px) {
height: calc((100vh - 241px) / 2);
min-height: 120px;
margin-bottom: 0;
}
max-height: calc(100vh - 194px);
margin-bottom: 2px;
}
.infinite-scroll {
overflow-y: scroll;
height: calc(100vh - 233px);
@media screen and (max-width: 991px) {
height: calc((100vh - 315px) / 2);
min-height: 83px;
}
}
.header {
margin: 2px -15px 1px -15px;
}
.list-group {
margin-top: 8px;
}
ul > li {
&:first-child.list-group-item.selected {
border-color: #ddd;
}
&.list-group-item.selected {
background-color: $selected-item;
}
}
.list-left li {
cursor: pointer;
}
button.no-style {
background: none;
border: none;
}
.row.list-group {
margin-left: 2px;
}
.list-group > li {
border-left: none;
border-right: none;
}
a > label {
height: 2em;
width: 100%;
}
dd,
dt {
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
dt > i {
font-weight: normal;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
}
li > div.row > dl {
margin-bottom: 0px;
}
li.list-group-item:hover {
color: #555;
text-decoration: none;
background-color: #f5f5f5;
}

View File

@ -1,50 +0,0 @@
import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges } from '@angular/core';
import { WorkbasketSummary } from 'app/shared/models/workbasket-summary';
import { Filter } from 'app/shared/models/filter';
import { expandDown } from 'app/shared/animations/expand.animation';
import { Side } from '../workbasket-distribution-targets/workbasket-distribution-targets.component';
@Component({
selector: 'taskana-administration-workbasket-dual-list',
templateUrl: './workbasket-dual-list.component.html',
styleUrls: ['./workbasket-dual-list.component.scss'],
animations: [expandDown]
})
export class WorkbasketDualListComponent implements OnInit {
@Input() distributionTargets: WorkbasketSummary[];
@Input() distributionTargetsSelected: WorkbasketSummary[];
@Output() performDualListFilter = new EventEmitter<{ filterBy: Filter; side: Side }>();
@Input() requestInProgress = false;
@Input() loadingItems? = false;
@Input() side: Side;
@Input() header: string;
@Output() scrolling = new EventEmitter<Side>();
@Input() allSelected;
@Output() allSelectedChange = new EventEmitter<boolean>();
sideNumber = 0;
toolbarState = false;
ngOnInit() {
this.sideNumber = this.side === Side.LEFT ? 0 : 1;
}
selectAll(selected: boolean) {
this.distributionTargets.forEach((element: any) => {
element.selected = selected;
});
this.allSelectedChange.emit(this.allSelected);
}
onScroll() {
this.scrolling.emit(this.side);
}
performAvailableFilter(filterModel: Filter) {
this.performDualListFilter.emit({ filterBy: filterModel, side: this.side });
}
changeToolbarState(state: boolean) {
this.toolbarState = state;
}
}

View File

@ -26,10 +26,13 @@
}
.workbasket-information__mat-form-field {
width: 70%;
width: 100%;
margin-right: 10px;
}
.dropdown-menu {
min-width: auto;
}
.workbasket-information__custom-fields {
width: 100%;
}

View File

@ -61,7 +61,9 @@ const workbasketServiceMock = jest.fn().mockImplementation(
updateWorkbasket: jest.fn().mockReturnValue(of(true)),
markWorkbasketForDeletion: jest.fn().mockReturnValue(of(true)),
createWorkbasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock })),
getWorkBasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock }))
getWorkBasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock })),
getWorkBasketAccessItems: jest.fn().mockReturnValue(of()),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of())
})
);

View File

@ -27,6 +27,6 @@
</div>
<taskana-shared-filter *ngIf="showFilter" (performFilter)="filtering($event)"></taskana-shared-filter>
<taskana-shared-filter *ngIf="showFilter" (performFilter)="filtering($event)" component="workbasket-list" (inputComponent)="setComponent($event)"></taskana-shared-filter>
</div>

View File

@ -2,7 +2,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { WorkbasketListToolbarComponent } from './workbasket-list-toolbar.component';
import { Component, DebugElement, EventEmitter, Input, Output } from '@angular/core';
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Observable, of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { WorkbasketState } from '../../../shared/store/workbasket-store/workbasket.state';
import { WorkbasketService } from '../../../shared/services/workbasket/workbasket.service';
@ -17,6 +17,7 @@ import { MatIconModule } from '@angular/material/icon';
import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatDialogModule } from '@angular/material/dialog';
import { RouterTestingModule } from '@angular/router/testing';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
const getDomainFn = jest.fn().mockReturnValue(true);
const domainServiceMock = jest.fn().mockImplementation(
@ -43,6 +44,12 @@ class FilterStub {
@Output() performFilter = new EventEmitter<Filter>();
}
const requestInProgressServiceSpy = jest.fn().mockImplementation(
(): Partial<RequestInProgressService> => ({
setRequestInProgress: jest.fn().mockReturnValue(of())
})
);
describe('WorkbasketListToolbarComponent', () => {
let fixture: ComponentFixture<WorkbasketListToolbarComponent>;
let debugElement: DebugElement;
@ -62,7 +69,11 @@ describe('WorkbasketListToolbarComponent', () => {
MatDialogModule
],
declarations: [WorkbasketListToolbarComponent, ImportExportStub, SortStub, FilterStub],
providers: [{ provide: DomainService, useClass: domainServiceMock }, WorkbasketService]
providers: [
{ provide: DomainService, useClass: domainServiceMock },
{ provide: RequestInProgressService, useClass: requestInProgressServiceSpy },
WorkbasketService
]
}).compileComponents();
fixture = TestBed.createComponent(WorkbasketListToolbarComponent);
@ -107,7 +118,7 @@ describe('WorkbasketListToolbarComponent', () => {
expect(sort).toMatchObject(mockSort);
});
it('should emit value when filtering is called', (done) => {
it('should emit value when filtering is called', async((done) => {
const mockFilter: Filter = { filterParams: 'abc' };
let filterBy: Filter = { filterParams: 'abc' };
component.performFilter.subscribe((filter: Filter) => {
@ -116,7 +127,7 @@ describe('WorkbasketListToolbarComponent', () => {
});
component.filtering(filterBy);
expect(filterBy).toMatchObject(mockFilter);
});
}));
/* HTML */

View File

@ -43,6 +43,7 @@ export class WorkbasketListToolbarComponent implements OnInit {
filterParams = { name: '', key: '', type: '', description: '', owner: '' };
filterType = TaskanaType.WORKBASKETS;
showFilter = false;
component = '';
@Select(WorkbasketSelectors.workbasketActiveAction)
workbasketActiveAction$: Observable<ACTION>;
@ -63,7 +64,13 @@ export class WorkbasketListToolbarComponent implements OnInit {
}
filtering(filterBy: Filter) {
this.performFilter.emit(filterBy);
if (this.component === 'workbasket-list') {
this.performFilter.emit(filterBy);
}
}
setComponent(component: string) {
this.component = component;
}
addWorkbasket() {

View File

@ -2,9 +2,7 @@
.workbasket-list {
height: calc(100vh - 156px);
overflow-x: hidden;
overflow-y: hidden;
min-width: 500px;
}
.workbasket-list__workbaskets {
overflow-y: hidden;

View File

@ -23,17 +23,20 @@ import { MatListModule } from '@angular/material/list';
import { DomainService } from '../../../shared/services/domain/domain.service';
import { RouterTestingModule } from '@angular/router/testing';
import { RequestInProgressService } from '../../../shared/services/request-in-progress/request-in-progress.service';
import { selectedWorkbasketMock } from '../../../shared/store/mock-data/mock-store';
const workbasketSavedTriggeredFn = jest.fn().mockReturnValue(of(1));
const workbasketSummaryFn = jest.fn().mockReturnValue(of({}));
const getWorkbasketFn = jest.fn().mockReturnValue(of({ workbasketId: '1' }));
const getWorkbasketFn = jest.fn().mockReturnValue(of(selectedWorkbasketMock));
const getWorkbasketActionToolbarExpansionFn = jest.fn().mockReturnValue(of(false));
const workbasketServiceMock = jest.fn().mockImplementation(
(): Partial<WorkbasketService> => ({
workbasketSavedTriggered: workbasketSavedTriggeredFn,
getWorkBasketsSummary: workbasketSummaryFn,
getWorkBasket: getWorkbasketFn,
getWorkbasketActionToolbarExpansion: getWorkbasketActionToolbarExpansionFn
getWorkbasketActionToolbarExpansion: getWorkbasketActionToolbarExpansionFn,
getWorkBasketAccessItems: jest.fn().mockReturnValue(of({})),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of({}))
})
);
@ -137,7 +140,7 @@ describe('WorkbasketListComponent', () => {
fixture.detectChanges();
let actionDispatched = false;
actions$.pipe(ofActionDispatched(SelectWorkbasket)).subscribe(() => (actionDispatched = true));
component.selectWorkbasket('1');
component.selectWorkbasket('WBI:000000000000000000000000000000000902');
expect(actionDispatched).toBe(true);
}));

View File

@ -1,13 +1,13 @@
.workbasket-overview {
display: flex;
flex-direction: row;
width: 100%;
height: 100%;
overflow: hidden;
align-items: stretch;
}
taskana-administration-workbasket-details {
width: calc(100% - 500px);
height: calc(100% - 213px);
taskana-administration-workbasket-list {
width: 500px;
}
taskana-administration-workbasket-details {
flex-grow: 1;
}

View File

@ -17,7 +17,9 @@
width: 350px;
background-color: $dark-green;
}
.mat-drawer-content {
overflow: hidden !important;
}
.sidenav__drawer-list-item {
margin-left: 30px;
}

View File

@ -9,6 +9,7 @@ import { TaskanaType } from 'app/shared/models/taskana-type';
styleUrls: ['./filter.component.scss']
})
export class FilterComponent implements OnInit {
@Input() component: string;
@Input() allTypes: Map<ICONTYPES, string> = new Map([
[ICONTYPES.ALL, 'All'],
[ICONTYPES.PERSONAL, 'Personal'],
@ -29,6 +30,7 @@ export class FilterComponent implements OnInit {
@Input() filterType = TaskanaType.WORKBASKETS;
@Output() performFilter = new EventEmitter<Filter>();
@Output() inputComponent = new EventEmitter<string>();
filter: Filter;
filterParamKeys = [];
@ -59,6 +61,7 @@ export class FilterComponent implements OnInit {
}
search() {
this.inputComponent.emit(this.component);
this.performFilter.emit(this.filter);
}

View File

@ -1,14 +1,14 @@
import { Pipe, PipeTransform } from '@angular/core';
import { TaskanaQueryParameters } from 'app/shared/util/query-parameters';
import { WorkbasketSummary } from '../models/workbasket-summary';
@Pipe({ name: 'selectWorkbaskets' })
export class SelectWorkBasketPipe implements PipeTransform {
transform(originArray: any, selectionArray: any, arg1: any): Object[] {
transform(originArray: any, selectionArray: any, arg1: any): WorkbasketSummary[] {
let returnArray = [];
if (!originArray || !selectionArray) {
return returnArray;
}
for (let index = originArray.length - 1; index >= 0; index--) {
if (
(arg1 &&
@ -21,6 +21,7 @@ export class SelectWorkBasketPipe implements PipeTransform {
originArray.splice(index, 1);
}
}
if (originArray.length > TaskanaQueryParameters.pageSize) {
originArray.slice(0, TaskanaQueryParameters.pageSize);
}

View File

@ -288,5 +288,215 @@ 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',
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
},
{
workbasketId: 'WBI:000000000000000000000000000000000900',
key: 'sort001',
name: 'basxet0',
domain: 'DOMAIN_A',
type: 'TOPIC',
description: 'Lorem ipsum dolor sit amet.',
owner: 'Max',
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: 'Max',
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: 'Max',
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: 'Max',
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: 'Max',
custom1: '',
custom2: '',
custom3: '',
custom4: '',
orgLevel1: '',
orgLevel2: '',
orgLevel3: '',
orgLevel4: '',
markedForDeletion: false
}
],
workbasketAccessItems: workbasketAccessItemsMock
};

View File

@ -99,6 +99,10 @@ export class GetWorkbasketDistributionTargets {
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[]) {}

View File

@ -60,6 +60,11 @@ export class WorkbasketSelectors {
static workbasketDistributionTargets(state: WorkbasketStateModel): WorkbasketDistributionTargets {
return state.workbasketDistributionTargets;
}
@Selector([WorkbasketState])
static availableDistributionTargets(state: WorkbasketStateModel): WorkbasketSummary[] {
return state.workbasketAvailableDistributionTargets;
}
}
export interface WorkbasketAndAction {

View File

@ -8,6 +8,7 @@ import {
CopyWorkbasket,
CreateWorkbasket,
DeselectWorkbasket,
GetAvailableDistributionTargets,
GetWorkbasketAccessItems,
GetWorkbasketDistributionTargets,
GetWorkbasketsSummary,
@ -32,6 +33,7 @@ import { WorkbasketSummary } from '../../models/workbasket-summary';
import { WorkbasketComponent } from '../../../administration/models/workbasket-component';
import { ButtonAction } from '../../../administration/models/button-action';
import { ActivatedRoute } from '@angular/router';
import { RequestInProgressService } from '../../services/request-in-progress/request-in-progress.service';
class InitializeStore {
static readonly type = '[Workbasket] Initializing state';
@ -43,7 +45,8 @@ export class WorkbasketState implements NgxsAfterBootstrap {
private workbasketService: WorkbasketService,
private location: Location,
private notificationService: NotificationService,
private route: ActivatedRoute
private route: ActivatedRoute,
private requestInProgressService: RequestInProgressService
) {}
@Action(InitializeStore)
@ -131,6 +134,10 @@ export class WorkbasketState implements NgxsAfterBootstrap {
selectedWorkbasket,
action: ACTION.READ
});
ctx.dispatch(new GetWorkbasketAccessItems(ctx.getState().selectedWorkbasket._links.accessItems.href));
ctx.dispatch(
new GetWorkbasketDistributionTargets(ctx.getState().selectedWorkbasket._links.distributionTargets.href)
);
})
);
}
@ -186,6 +193,9 @@ export class WorkbasketState implements NgxsAfterBootstrap {
@Action(OnButtonPressed)
doWorkbasketDetailsAction(ctx: StateContext<WorkbasketStateModel>, action: OnButtonPressed): Observable<any> {
ctx.patchState({ button: action.button });
setTimeout(() => {
ctx.patchState({ button: undefined });
}, 500);
return of(null);
}
@ -347,15 +357,32 @@ export class WorkbasketState implements NgxsAfterBootstrap {
);
}
@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> {
this.requestInProgressService.setRequestInProgress(true);
return this.workbasketService.updateWorkBasketsDistributionTargets(action.url, action.distributionTargetsIds).pipe(
take(1),
tap(
(updatedWorkbasketsDistributionTargets) => {
ctx.patchState({
workbasketDistributionTargets: updatedWorkbasketsDistributionTargets
});
this.requestInProgressService.setRequestInProgress(false);
this.notificationService.showToast(
NOTIFICATION_TYPES.SUCCESS_ALERT_8,
new Map<string, string>([['workbasketName', ctx.getState().selectedWorkbasket.name]])
@ -363,6 +390,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
},
(error) => {
this.notificationService.triggerError(NOTIFICATION_TYPES.SAVE_ERR_3, error);
this.requestInProgressService.setRequestInProgress(false);
}
)
);
@ -392,6 +420,7 @@ export interface WorkbasketStateModel {
action: ACTION;
workbasketAccessItems: WorkbasketAccessItemsRepresentation;
workbasketDistributionTargets: WorkbasketDistributionTargets;
workbasketAvailableDistributionTargets: WorkbasketSummary[];
selectedComponent: WorkbasketComponent;
button: ButtonAction | undefined;
}

View File

@ -0,0 +1,8 @@
@import '~@angular/material/theming';
@include mat-core();
$my-app-primary: mat-palette($mat-blue-grey);
$my-app-accent: mat-palette($mat-teal, 500, 900, A100);
$my-app-warn: mat-palette($mat-red, 600);
$my-app-theme: mat-light-theme($my-app-primary, $my-app-accent, $my-app-warn);
@include angular-material-theme($my-app-theme);