From 900343b722316f2917c844cc3b533b7f9718b5b3 Mon Sep 17 00:00:00 2001 From: Chi Nguyen <6671583+cnguyen-de@users.noreply.github.com> Date: Tue, 8 Dec 2020 16:11:13 +0100 Subject: [PATCH] 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 --- web/angular.json | 3 +- .../administration/administration.module.ts | 18 +- .../classification-details.component.html | 26 +- .../classification-details.component.scss | 20 +- .../classification-details.component.spec.ts | 2 + .../classification-list.component.scss | 1 - .../classification-overview.component.scss | 9 +- .../workbasket-details.component.scss | 4 - ...t-distribution-targets-list.component.html | 65 ++++ ...t-distribution-targets-list.component.scss | 61 ++++ ...stribution-targets-list.component.spec.ts} | 38 +- ...ket-distribution-targets-list.component.ts | 75 ++++ ...basket-distribution-targets.component.html | 176 ++++++---- ...basket-distribution-targets.component.scss | 34 +- ...ket-distribution-targets.component.spec.ts | 156 +++++++++ ...rkbasket-distribution-targets.component.ts | 329 +++++++----------- .../workbasket-dual-list.component.html | 57 --- .../workbasket-dual-list.component.scss | 112 ------ .../workbasket-dual-list.component.ts | 50 --- .../workbasket-information.component.scss | 5 +- .../workbasket-information.component.spec.ts | 4 +- .../workbasket-list-toolbar.component.html | 2 +- .../workbasket-list-toolbar.component.spec.ts | 19 +- .../workbasket-list-toolbar.component.ts | 9 +- .../workbasket-list.component.scss | 2 - .../workbasket-list.component.spec.ts | 9 +- .../workbasket-overview.component.scss | 10 +- web/src/app/app.component.scss | 4 +- .../components/filter/filter.component.ts | 3 + .../shared/pipes/select-workbaskets.pipe.ts | 5 +- .../app/shared/store/mock-data/mock-store.ts | 210 +++++++++++ .../workbasket-store/workbasket.actions.ts | 4 + .../workbasket-store/workbasket.selectors.ts | 5 + .../workbasket-store/workbasket.state.ts | 31 +- web/src/theme/custom-theme-material.scss | 8 + 35 files changed, 998 insertions(+), 568 deletions(-) create mode 100644 web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.html create mode 100644 web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.scss rename web/src/app/administration/components/{workbasket-dual-list/workbasket-dual-list.component.spec.ts => workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.spec.ts} (69%) create mode 100644 web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.ts create mode 100644 web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.spec.ts delete mode 100644 web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.html delete mode 100644 web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.scss delete mode 100644 web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.ts create mode 100644 web/src/theme/custom-theme-material.scss diff --git a/web/angular.json b/web/angular.json index 3fd19046a..93eb971fc 100644 --- a/web/angular.json +++ b/web/angular.json @@ -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", diff --git a/web/src/app/administration/administration.module.ts b/web/src/app/administration/administration.module.ts index f82ea50c4..4b9af5858 100644 --- a/web/src/app/administration/administration.module.ts +++ b/web/src/app/administration/administration.module.ts @@ -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 {} diff --git a/web/src/app/administration/components/classification-details/classification-details.component.html b/web/src/app/administration/components/classification-details/classification-details.component.html index 720264688..327f8c6b4 100644 --- a/web/src/app/administration/components/classification-details/classification-details.component.html +++ b/web/src/app/administration/components/classification-details/classification-details.component.html @@ -3,7 +3,7 @@
-
+

{{classification.name}}  [{{classification.type}}] {{badgeMessage$ | async}}

@@ -39,7 +39,7 @@
- +
@@ -80,17 +80,19 @@
- - Service Level - - - -
{{lengthError}}
+
+ + Service Level + + + +
{{lengthError}}
+
-
+
+
+ + +
+ +
+ +
+ +
+

+ {{workbasket.name}}, {{workbasket.key}} +

+

{{workbasket.description}}  

+

{{workbasket.owner}}  

+
+ +
+ error +
+
+ + + +
+
+
+ +
+ There is currently no distributed workbasket +
+
diff --git a/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.scss b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.scss new file mode 100644 index 000000000..c4e2832d3 --- /dev/null +++ b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.scss @@ -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; +} diff --git a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.spec.ts b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.spec.ts similarity index 69% rename from web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.spec.ts rename to web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.spec.ts index b10af4022..62979f4f4 100644 --- a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.spec.ts +++ b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.spec.ts @@ -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; +describe('WorkbasketDistributionTargetsListComponent', () => { + let fixture: ComponentFixture; 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); }); diff --git a/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.ts b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.ts new file mode 100644 index 000000000..06a8d3b7b --- /dev/null +++ b/web/src/app/administration/components/workbasket-distribution-targets-list/workbasket-distribution-targets-list.component.ts @@ -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(); + @Input() allSelected; + @Output() allSelectedChange = new EventEmitter(); + + 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; + } +} diff --git a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.html b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.html index e6a42bc81..209da7845 100644 --- a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.html +++ b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.html @@ -1,77 +1,111 @@ -
- - +
+
+ +
+ +
+ +
---> - -
- -
- -
+ +
+ - - - + - -
- -
+
+ + +
+ + + + + +
+ + + +
+ + + + + + + + + +
diff --git a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.scss b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.scss index ac213ad3b..4d09ed4b5 100644 --- a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.scss +++ b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.scss @@ -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%; + } } } diff --git a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.spec.ts b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.spec.ts new file mode 100644 index 000000000..1bf3d49e9 --- /dev/null +++ b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.spec.ts @@ -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(); + @Input() allSelected; + @Output() allSelectedChange = new EventEmitter(); +} + +const workbasketServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + getWorkBasketsSummary: jest.fn().mockReturnValue(of()), + getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of()) + }) +); + +const savingWorkbasketServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + triggeredDistributionTargetsSaving: jest.fn().mockReturnValue(of()) + }) +); +const notificationsServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + showToast: jest.fn().mockReturnValue(true) + }) +); +const requestInProgressServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + setRequestInProgress: jest.fn().mockReturnValue(of()) + }) +); + +describe('WorkbasketDistributionTargetsComponent', () => { + let fixture: ComponentFixture; + let debugElement: DebugElement; + let component: WorkbasketDistributionTargetsComponent; + let store: Store; + let actions$: Observable; + + 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); + }); +}); diff --git a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.ts b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.ts index aff496266..7c9fde50e 100644 --- a/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.ts +++ b/web/src/app/administration/components/workbasket-distribution-targets/workbasket-distribution-targets.component.ts @@ -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 = []; + availableDistributionTargets: Array = []; distributionTargetsRight: Array = []; distributionTargetsSelected: Array; distributionTargetsClone: Array; distributionTargetsSelectedClone: Array; - 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; + @Select(WorkbasketSelectors.availableDistributionTargets) + availableDistributionTargets$: Observable; + + @Select(WorkbasketSelectors.buttonAction) + buttonAction$: Observable; + destroy$ = new Subject(); 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([['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 { return originList.filter((item: any) => item.selected === true); } - unselectItems(originList: any): Array { - // eslint-disable-next-line no-restricted-syntax - for (const item of originList) { - if (item.selected && item.selected === true) { + unselectItems(originList: Array): Array { + 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 { + return this.distributionTargetsSelected.map((distributionTarget) => distributionTarget.workbasketId); } - getSeletedIds(): Array { - const distributionTargetsSelelected: Array = []; - 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() { diff --git a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.html b/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.html deleted file mode 100644 index c2f697f8a..000000000 --- a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.html +++ /dev/null @@ -1,57 +0,0 @@ -
- -
-
-
- -
-
-
{{header}}
-
-
- -
-
- -
- -
- -
- - -
-
    -
  • -
    -
    - -
    -
    -
    {{distributionTarget.name}}, - {{distributionTarget.key}} -
    -
    {{distributionTarget.description}}  
    -
    {{distributionTarget.owner}}  
    -
    -
    -
  • -
  • - -
  • -
-
-
diff --git a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.scss b/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.scss deleted file mode 100644 index cb7dcecba..000000000 --- a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.scss +++ /dev/null @@ -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; -} diff --git a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.ts b/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.ts deleted file mode 100644 index 15004eaef..000000000 --- a/web/src/app/administration/components/workbasket-dual-list/workbasket-dual-list.component.ts +++ /dev/null @@ -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(); - @Input() allSelected; - @Output() allSelectedChange = new EventEmitter(); - - 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; - } -} diff --git a/web/src/app/administration/components/workbasket-information/workbasket-information.component.scss b/web/src/app/administration/components/workbasket-information/workbasket-information.component.scss index f39b880b0..22847f4e1 100644 --- a/web/src/app/administration/components/workbasket-information/workbasket-information.component.scss +++ b/web/src/app/administration/components/workbasket-information/workbasket-information.component.scss @@ -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%; +} diff --git a/web/src/app/administration/components/workbasket-information/workbasket-information.component.spec.ts b/web/src/app/administration/components/workbasket-information/workbasket-information.component.spec.ts index e64a2630f..8e73f294c 100644 --- a/web/src/app/administration/components/workbasket-information/workbasket-information.component.spec.ts +++ b/web/src/app/administration/components/workbasket-information/workbasket-information.component.spec.ts @@ -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()) }) ); diff --git a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.html b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.html index 9d6b897a4..babeb3108 100644 --- a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.html +++ b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.html @@ -27,6 +27,6 @@
- +
diff --git a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.spec.ts b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.spec.ts index 264975b34..cfde11fe5 100644 --- a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.spec.ts +++ b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.spec.ts @@ -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(); } +const requestInProgressServiceSpy = jest.fn().mockImplementation( + (): Partial => ({ + setRequestInProgress: jest.fn().mockReturnValue(of()) + }) +); + describe('WorkbasketListToolbarComponent', () => { let fixture: ComponentFixture; 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 */ diff --git a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.ts b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.ts index 3a34ada27..1786b2292 100644 --- a/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.ts +++ b/web/src/app/administration/components/workbasket-list-toolbar/workbasket-list-toolbar.component.ts @@ -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; @@ -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() { diff --git a/web/src/app/administration/components/workbasket-list/workbasket-list.component.scss b/web/src/app/administration/components/workbasket-list/workbasket-list.component.scss index bd33b4b95..550e222d1 100644 --- a/web/src/app/administration/components/workbasket-list/workbasket-list.component.scss +++ b/web/src/app/administration/components/workbasket-list/workbasket-list.component.scss @@ -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; diff --git a/web/src/app/administration/components/workbasket-list/workbasket-list.component.spec.ts b/web/src/app/administration/components/workbasket-list/workbasket-list.component.spec.ts index 3e4911ec9..d6ea91867 100644 --- a/web/src/app/administration/components/workbasket-list/workbasket-list.component.spec.ts +++ b/web/src/app/administration/components/workbasket-list/workbasket-list.component.spec.ts @@ -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 => ({ 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); })); diff --git a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss index 746510663..4bdcdea3a 100644 --- a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss +++ b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.scss @@ -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; } diff --git a/web/src/app/app.component.scss b/web/src/app/app.component.scss index b41e01931..c3ee9061d 100644 --- a/web/src/app/app.component.scss +++ b/web/src/app/app.component.scss @@ -17,7 +17,9 @@ width: 350px; background-color: $dark-green; } - +.mat-drawer-content { + overflow: hidden !important; +} .sidenav__drawer-list-item { margin-left: 30px; } diff --git a/web/src/app/shared/components/filter/filter.component.ts b/web/src/app/shared/components/filter/filter.component.ts index a82d583e8..8f9487342 100644 --- a/web/src/app/shared/components/filter/filter.component.ts +++ b/web/src/app/shared/components/filter/filter.component.ts @@ -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 = 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(); + @Output() inputComponent = new EventEmitter(); filter: Filter; filterParamKeys = []; @@ -59,6 +61,7 @@ export class FilterComponent implements OnInit { } search() { + this.inputComponent.emit(this.component); this.performFilter.emit(this.filter); } diff --git a/web/src/app/shared/pipes/select-workbaskets.pipe.ts b/web/src/app/shared/pipes/select-workbaskets.pipe.ts index b9b0d3ad5..3d011d073 100644 --- a/web/src/app/shared/pipes/select-workbaskets.pipe.ts +++ b/web/src/app/shared/pipes/select-workbaskets.pipe.ts @@ -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); } diff --git a/web/src/app/shared/store/mock-data/mock-store.ts b/web/src/app/shared/store/mock-data/mock-store.ts index e9ac250ad..a234e5a41 100644 --- a/web/src/app/shared/store/mock-data/mock-store.ts +++ b/web/src/app/shared/store/mock-data/mock-store.ts @@ -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 }; diff --git a/web/src/app/shared/store/workbasket-store/workbasket.actions.ts b/web/src/app/shared/store/workbasket-store/workbasket.actions.ts index 1ab1b86b2..90cd71088 100644 --- a/web/src/app/shared/store/workbasket-store/workbasket.actions.ts +++ b/web/src/app/shared/store/workbasket-store/workbasket.actions.ts @@ -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[]) {} diff --git a/web/src/app/shared/store/workbasket-store/workbasket.selectors.ts b/web/src/app/shared/store/workbasket-store/workbasket.selectors.ts index ec50edf33..81ad29882 100644 --- a/web/src/app/shared/store/workbasket-store/workbasket.selectors.ts +++ b/web/src/app/shared/store/workbasket-store/workbasket.selectors.ts @@ -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 { diff --git a/web/src/app/shared/store/workbasket-store/workbasket.state.ts b/web/src/app/shared/store/workbasket-store/workbasket.state.ts index dd5e54620..83a68ed12 100644 --- a/web/src/app/shared/store/workbasket-store/workbasket.state.ts +++ b/web/src/app/shared/store/workbasket-store/workbasket.state.ts @@ -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, action: OnButtonPressed): Observable { 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): Observable { + return this.workbasketService.getWorkBasketsSummary(true).pipe( + take(1), + tap((workbasketAvailableDistributionTargets: WorkbasketSummaryRepresentation) => { + ctx.patchState({ + workbasketAvailableDistributionTargets: workbasketAvailableDistributionTargets.workbaskets + }); + }) + ); + } + @Action(UpdateWorkbasketDistributionTargets) updateWorkbasketDistributionTargets( ctx: StateContext, action: UpdateWorkbasketDistributionTargets ): Observable { + 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([['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; } diff --git a/web/src/theme/custom-theme-material.scss b/web/src/theme/custom-theme-material.scss new file mode 100644 index 000000000..ea870a4e1 --- /dev/null +++ b/web/src/theme/custom-theme-material.scss @@ -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);