From f42f79ef69e50de412130d836cb79806639791d0 Mon Sep 17 00:00:00 2001 From: Franzi321 <71708325+Franzi321@users.noreply.github.com> Date: Fri, 18 Dec 2020 15:27:59 +0100 Subject: [PATCH] TSK-1476: Rework workbasket access items (#1360) * TSK-1476: Rework Workbasket Access Items * TSK-1476: Rework Workbasket Access Items * TSK-1476: update --- .../access-items-management.component.spec.ts | 16 +- .../workbasket-access-items.component.html | 112 ++++++++----- .../workbasket-access-items.component.scss | 158 +++++++++++------- .../workbasket-access-items.component.spec.ts | 22 ++- .../workbasket-access-items.component.ts | 103 ++++++++++-- .../workbasket-details.component.html | 36 ++-- .../workbasket-details.component.spec.ts | 1 + .../workbasket-details.component.ts | 4 +- .../workbasket-overview.component.html | 4 +- .../workbasket-overview.component.spec.ts | 5 +- .../type-ahead/type-ahead.component.html | 5 +- .../type-ahead/type-ahead.component.ts | 33 ++-- .../data-sources/taskana-customization.json | 8 +- 13 files changed, 336 insertions(+), 171 deletions(-) diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts b/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts index 45fec96f7..8512fdfac 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.spec.ts @@ -60,6 +60,11 @@ describe('AccessItemsManagementComponent', () => { let store: Store; let actions$: Observable; + @Component({ selector: 'taskana-shared-spinner', template: '' }) + class TaskanaSharedSpinnerStub { + @Input() isRunning: boolean; + } + @Component({ selector: 'taskana-shared-sort', template: '' }) class TaskanaSharedSortStub { @Input() sortingFields: Map; @@ -91,7 +96,12 @@ describe('AccessItemsManagementComponent', () => { MatListModule, MatExpansionModule ], - declarations: [AccessItemsManagementComponent, TypeAheadComponent, TaskanaSharedSortStub], + declarations: [ + AccessItemsManagementComponent, + TypeAheadComponent, + TaskanaSharedSortStub, + TaskanaSharedSpinnerStub + ], providers: [ { provide: FormsValidatorService, useClass: formValidatorServiceSpy }, { provide: NotificationService, useClass: notificationServiceSpy }, @@ -148,10 +158,8 @@ describe('AccessItemsManagementComponent', () => { const groups = store.selectSnapshot((state) => state.accessItemsManagement); expect(selectedAccessId).not.toBeNull(); expect(groups).not.toBeNull(); - expect(app.accessItemsForm).not.toBeNull(); - app.onSelectAccessId(null); - expect(app.accessItemsForm).toBeNull(); + expect(groups).toMatchObject({}); }); it('should dispatch GetAccessItems action in searchForAccessItemsWorkbaskets', async((done) => { diff --git a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.html b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.html index f7d054333..b51bfa800 100644 --- a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.html +++ b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.html @@ -1,10 +1,24 @@
-
-
- - +
+
+ + +
+ +
@@ -22,86 +36,98 @@ - - + + + + + + - - - - - - -
- + - - -
- +
- - - + + + - + + - + + - + + - + + - + +
- - -
-
+ \ No newline at end of file diff --git a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.scss b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.scss index 75702088f..6ac5f4f77 100644 --- a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.scss +++ b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.scss @@ -1,22 +1,94 @@ @import '../../../../theme/colors'; +@import '~@angular/material/theming'; .workbasket-access-items { - max-width: calc(100vw - 500px); -} - -.workbasket-access-items__typeahead { - text-align: left; - min-width: 180px; - width: calc(100% + 20px); -} -td > input[type='checkbox'] { - margin-top: 0; - display: block; -} - -.panel-body { + height: calc(100vh - 213px); + overflow-y: auto; overflow-x: auto; - padding-top: 0; + display: inline-block; + + &__typeahead { + text-align: left; + min-width: 180px; + width: calc(100% + 20px); + line-height: 20px; + padding-bottom: 0% !important; + margin-top: 5px; + } + + &__table { + margin-top: 20px; + margin-left: auto; + margin-right: auto; + width: 98%; + text-align: center; + + & th { + padding: 0.25rem; + position: sticky; + top: 0; + z-index: 3; + background: white; + } + + & td { + padding-left: 0.5rem; + vertical-align: initial; + border-top: 1px solid #dee2e6; + } + + & tr:first-child > td { + border-top: 2px solid #dddddd; + } + + & td > input[type='checkbox'] { + margin-top: 0; + display: block; + } + + & tr:nth-child(odd) > td { + background-color: rgba(0, 0, 0, 0.05); + } + + & input[type='checkbox']:checked { + filter: hue-rotate(320deg) brightness(1); + } + } + + &__buttons-add-access { + color: white; + border: none; + background: $aquamarine; + margin: 10px 3px 0px 6px; + outline: none; + } + + &__buttons-delete-access { + color: red; + margin: 2px 0px 0px 3px; + outline: none; + } + + &__permission-checkbox, + &__check-all, + &__select-row { + display: inline-block; + height: 1rem; + line-height: 0; + margin: auto; + order: 0; + position: relative; + white-space: nowrap; + width: 1rem; + flex-shrink: 0; + cursor: pointer; + + @supports (-moz-appearance: none) { + //bigger checkboxes for firefox because firefox renders differently + width: 1.25rem; + height: 1.25rem; + } + } } .required-header { @@ -27,55 +99,17 @@ td > input[type='checkbox'] { color: red; } -th { - padding: 0.25rem; - position: sticky; - top: 0; - z-index: 3; - background: white; -} -.workbasket-access-items__table { - margin-top: 20px; -} -.workbasket-access-items__table thead th { - vertical-align: bottom; - border-bottom: 2px solid #dee2e6; +::ng-deep .workbasket-access-items__form .mat-form-field-wrapper { + padding-bottom: 0em; + height: 58px; } -.workbasket-access-items__table td, -.table th { - padding: 0.5rem; - vertical-align: middle; - border-top: 1px solid #dee2e6; +::ng-deep .workbasket-access-items__typeahead .typeahead__form .mat-form-field-infix { + padding: 0 0 0 0; + position: initial; + font-size: medium; } -.workbasket-access-items__table > thead > tr > th { - max-width: 150px; - border-bottom: none; -} - -.workbasket-access-items__permission-checkbox { - display: inline-block; - height: 1rem; - line-height: 0; - margin: auto; - order: 0; - position: relative; - white-space: nowrap; - width: 1rem; - flex-shrink: 0; - cursor: pointer; - - @supports (-moz-appearance: none) { - //bigger checkboxes for firefox because firefox renders differently - width: 1.25rem; - height: 1.25rem; - } -} -input[type='checkbox']:checked { - filter: hue-rotate(320deg) brightness(1); -} - -.workbasket-access-items__add-access { - margin: 16px 0 16px 16px; +::ng-deep .mat-form-field-appearance-outline .mat-form-field-flex { + margin-top: 0.7em; } diff --git a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.spec.ts b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.spec.ts index 72f808343..1d8979c41 100644 --- a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.spec.ts +++ b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.spec.ts @@ -1,6 +1,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { WorkbasketAccessItemsComponent } from './workbasket-access-items.component'; -import { DebugElement } from '@angular/core'; +import { Component, DebugElement, Input } from '@angular/core'; import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store'; import { Observable, of } from 'rxjs'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; @@ -32,11 +32,20 @@ import { } from '../../../shared/store/workbasket-store/workbasket.actions'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { ACTION } from '../../../shared/models/action'; +import { WorkbasketAccessItems } from '../../../shared/models/workbasket-access-items'; import { MatSelectModule } from '@angular/material/select'; import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatCheckboxModule } from '@angular/material/checkbox'; +import { MatIconModule } from '@angular/material/icon'; + +@Component({ selector: 'taskana-shared-spinner', template: '' }) +class SpinnerStub { + @Input() isRunning: boolean; + @Input() positionClass: string; +} const savingWorkbasketServiceSpy = jest.fn().mockImplementation( (): Partial => ({ @@ -91,9 +100,11 @@ describe('WorkbasketAccessItemsComponent', () => { MatInputModule, MatSelectModule, MatAutocompleteModule, - MatProgressBarModule + MatProgressBarModule, + MatCheckboxModule, + MatIconModule ], - declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent], + declarations: [WorkbasketAccessItemsComponent, TypeAheadComponent, SpinnerStub], providers: [ { provide: SavingWorkbasketService, useClass: savingWorkbasketServiceSpy }, { provide: RequestInProgressService, useClass: requestInProgressServiceSpy }, @@ -152,7 +163,9 @@ describe('WorkbasketAccessItemsComponent', () => { it('should add accessItems when add access item button is clicked', () => { fixture.detectChanges(); - const addAccessItemButton = debugElement.nativeElement.querySelector('button.workbasket-access-items__add-access'); + const addAccessItemButton = debugElement.nativeElement.querySelector( + 'button.workbasket-access-items__buttons-add-access' + ); const clearSpy = jest.spyOn(component, 'addAccessItem'); expect(addAccessItemButton.title).toMatch('Add new access'); @@ -172,7 +185,6 @@ describe('WorkbasketAccessItemsComponent', () => { const checkAllSpy = jest.spyOn(component, 'checkAll'); const checkAllButton = debugElement.nativeElement.querySelector('#checkbox-0-00'); expect(checkAllButton).toBeTruthy(); - checkAllButton.click(); expect(checkAllSpy).toHaveBeenCalled(); }); diff --git a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts index 1a8e22357..070f538b9 100644 --- a/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts +++ b/web/src/app/administration/components/workbasket-access-items/workbasket-access-items.component.ts @@ -7,7 +7,8 @@ import { OnInit, QueryList, SimpleChanges, - ViewChildren + ViewChildren, + HostListener } from '@angular/core'; import { Observable, Subject } from 'rxjs'; import { Actions, ofActionCompleted, Select, Store } from '@ngxs/store'; @@ -51,12 +52,18 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest @Input() action: ACTION; + @Input() + expanded: boolean; + @ViewChildren('htmlInputElement') inputs: QueryList; badgeMessage = ''; + selectedRows: number[] = []; + workbasketClone: Workbasket; customFields$: Observable; + customFields: CustomField[]; accessItemsRepresentation: WorkbasketAccessItemsRepresentation; accessItemsClone: Array; @@ -99,6 +106,9 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest ngOnInit() { this.init(); this.customFields$ = this.accessItemsCustomization$.pipe(getCustomFields(customFieldCount)); + this.customFields$.subscribe((v) => { + this.customFields = v; + }); this.accessItemsRepresentation$.subscribe((accessItemsRepresentation) => { if (typeof accessItemsRepresentation !== 'undefined') { this.accessItemsRepresentation = accessItemsRepresentation; @@ -134,10 +144,42 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest }); } + ngAfterViewChecked() { + let elementIndex = 0; + let isTrue = true; + if (this.accessItemsGroups.controls) { + this.accessItemsGroups.controls.forEach((element) => { + for (let i in element.value) { + if (i.startsWith('perm')) { + if (this.accessItemsGroups.controls[elementIndex].value[i] === false) { + isTrue = false; + break; + } + } + } + if (isTrue) { + const checkbox = document.getElementById(`checkbox-${elementIndex}-00`) as HTMLInputElement; + if (checkbox) { + checkbox.checked = true; + elementIndex++; + } + } + }); + } + } + ngOnChanges(changes?: SimpleChanges) { + console.log('change'); if (changes.action) { this.setBadge(); } + if (this.workbasketClone) { + if (this.workbasketClone.workbasketId != this.workbasket.workbasketId) { + this.init(); + } + } + this.workbasketClone = this.workbasket; + //var offsetWidth = document.getElementById('container').offsetWidth; } init() { @@ -205,8 +247,8 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest workbasketAccessItems.permRead = true; const newForm = this.formBuilder.group(workbasketAccessItems); newForm.controls.accessId.setValidators(Validators.required); - this.accessItemsGroups.push(newForm); - this.accessItemsClone.push(workbasketAccessItems); + this.accessItemsGroups.insert(0, newForm); + this.accessItemsClone.unshift(workbasketAccessItems); this.added = true; } @@ -219,11 +261,6 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest this.notificationsService.showToast(NOTIFICATION_TYPES.INFO_ALERT); } - remove(index: number) { - this.accessItemsGroups.removeAt(index); - this.accessItemsClone.splice(index, 1); - } - isFieldValid(field: string, index: number): boolean { return this.formsValidatorService.isFieldValid(this.accessItemsGroups[index], field); } @@ -255,7 +292,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest } accessItemSelected(accessItem: AccessIdDefinition, row: number) { - this.accessItemsGroups.controls[row].get('accessId').setValue(accessItem.accessId); + this.accessItemsGroups.controls[row].get('accessId').setValue(accessItem?.accessId); this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem.name); } @@ -273,6 +310,33 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest }); } + checkboxClicked(index: number, value: any) { + if (value.currentTarget.checked) { + let isTrue = true; + let numbers = []; + const notVisibleFields = this.customFields.filter((v) => v.visible === false); + notVisibleFields.forEach((element) => { + const num = element.field.toString().replace('Custom ', 'permCustom'); + numbers.push(num); + }); + for (let i in this.accessItemsGroups.controls[index].value) { + if (i.startsWith('perm')) { + if (this.accessItemsGroups.controls[index].value[i] === false && !numbers.includes(i)) { + isTrue = false; + break; + } + } + } + if (isTrue) { + const checkbox = document.getElementById(`checkbox-${index}-00`) as HTMLInputElement; + checkbox.checked = true; + } + } else { + const checkbox = document.getElementById(`checkbox-${index}-00`) as HTMLInputElement; + checkbox.checked = false; + } + } + setBadge() { if (this.action === ACTION.COPY) { this.badgeMessage = `Copying workbasket: ${this.workbasket.key}`; @@ -296,12 +360,27 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest return `permCustom${customNumber}`; } - focusNewInput(input: ElementRef) { - if (this.added) { - input.nativeElement.focus(); + selectRow(value: any, index: number) { + if (value.target.checked) { + this.selectedRows.push(index); + } else { + this.selectedRows = this.selectedRows.filter(function (number) { + return number != index; + }); } } + deleteAccessItems() { + this.selectedRows.sort(function (a, b) { + return b - a; + }); + this.selectedRows.forEach((element) => { + this.accessItemsGroups.removeAt(element); + this.accessItemsClone.splice(element, 1); + }); + this.selectedRows = []; + } + ngOnDestroy() { this.destroy$.next(); this.destroy$.complete(); diff --git a/web/src/app/administration/components/workbasket-details/workbasket-details.component.html b/web/src/app/administration/components/workbasket-details/workbasket-details.component.html index ac2994146..507830393 100644 --- a/web/src/app/administration/components/workbasket-details/workbasket-details.component.html +++ b/web/src/app/administration/components/workbasket-details/workbasket-details.component.html @@ -4,49 +4,59 @@ {{ badgeMessage }} - - - - - - - - + - + + - + + - + + - - + \ No newline at end of file diff --git a/web/src/app/administration/components/workbasket-details/workbasket-details.component.spec.ts b/web/src/app/administration/components/workbasket-details/workbasket-details.component.spec.ts index fcf5e1f05..9310403f8 100644 --- a/web/src/app/administration/components/workbasket-details/workbasket-details.component.spec.ts +++ b/web/src/app/administration/components/workbasket-details/workbasket-details.component.spec.ts @@ -39,6 +39,7 @@ class WorkbasketAccessItemsStub { @Input() workbasket: Workbasket; @Input() action: ACTION; @Input() active: string; + @Input() expanded: boolean; } @Component({ selector: 'taskana-administration-workbasket-distribution-targets', template: '' }) diff --git a/web/src/app/administration/components/workbasket-details/workbasket-details.component.ts b/web/src/app/administration/components/workbasket-details/workbasket-details.component.ts index 01e78b655..41cabbe97 100644 --- a/web/src/app/administration/components/workbasket-details/workbasket-details.component.ts +++ b/web/src/app/administration/components/workbasket-details/workbasket-details.component.ts @@ -1,4 +1,4 @@ -import { Component, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; +import { Component, Input, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable, Subject } from 'rxjs'; import { Workbasket } from 'app/shared/models/workbasket'; @@ -45,6 +45,8 @@ export class WorkbasketDetailsComponent implements OnInit, OnDestroy, OnChanges destroy$ = new Subject(); + @Input() expanded: boolean; + constructor( private location: Location, private route: ActivatedRoute, diff --git a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.html b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.html index f7dbada56..638faf1cb 100644 --- a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.html +++ b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.html @@ -12,7 +12,7 @@
- +
@@ -23,4 +23,4 @@ - + \ No newline at end of file diff --git a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.spec.ts b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.spec.ts index 1f08824e9..256a3c2c1 100644 --- a/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.spec.ts +++ b/web/src/app/administration/components/workbasket-overview/workbasket-overview.component.spec.ts @@ -53,8 +53,9 @@ class WorkbasketListStub { } @Component({ selector: 'taskana-administration-workbasket-details', template: '' }) -class WorkbasketDetailsStub {} - +class WorkbasketDetailsStub { + @Input() expanded: boolean; +} @Component({ selector: 'svg-icon', template: '' }) class SvgIconStub {} diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.html b/web/src/app/shared/components/type-ahead/type-ahead.component.html index 16333b600..4dae8a0d6 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.html +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.html @@ -1,7 +1,10 @@
- {{dataSource.selected?.name ? 'Owner: ' + dataSource.selected?.name : placeHolderMessage}} + + {{dataSource.selected?.name ? 'Name: ' + dataSource.selected?.name : placeHolderMessage}} + + {{dataSource.selected?.name}} diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.ts b/web/src/app/shared/components/type-ahead/type-ahead.component.ts index 6bd702108..0adbb97be 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.ts +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.ts @@ -1,17 +1,8 @@ -import { - Component, - Input, - ViewChild, - forwardRef, - Output, - EventEmitter, - ElementRef, - AfterViewInit -} from '@angular/core'; +import { Component, Input, ViewChild, forwardRef, Output, EventEmitter } from '@angular/core'; import { Observable } from 'rxjs'; import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service'; -import { ControlValueAccessor, FormControl, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { highlight } from 'app/shared/animations/validation.animation'; import { mergeMap } from 'rxjs/operators'; import { AccessIdDefinition } from 'app/shared/models/access-id'; @@ -29,10 +20,10 @@ import { AccessIdDefinition } from 'app/shared/models/access-id'; } ] }) -export class TypeAheadComponent implements AfterViewInit, ControlValueAccessor { +export class TypeAheadComponent implements ControlValueAccessor { dataSource: any; typing = false; - + isFirst = false; items = []; @Input() @@ -56,12 +47,7 @@ export class TypeAheadComponent implements AfterViewInit, ControlValueAccessor { @Output() selectedItem = new EventEmitter(); - @Output() - inputField = new EventEmitter(); - @ViewChild('inputTypeAhead') - private inputTypeAhead; - typeaheadLoading = false; typeaheadMinLength = 3; typeaheadWaitMs = 500; @@ -91,6 +77,9 @@ export class TypeAheadComponent implements AfterViewInit, ControlValueAccessor { writeValue(value: any) { if (value !== this.innerValue) { this.innerValue = value; + if (this.value) { + this.isFirst = true; + } this.initializeDataSource(); } } @@ -107,16 +96,16 @@ export class TypeAheadComponent implements AfterViewInit, ControlValueAccessor { constructor(private accessIdsService: AccessIdsService) {} - ngAfterViewInit() { - this.inputField.emit(this.inputTypeAhead); - } - initializeDataSource() { this.dataSource = new Observable((observer: any) => { observer.next(this.value); }).pipe(mergeMap((token: string) => this.getUsersAsObservable(token))); this.accessIdsService.searchForAccessId(this.value).subscribe((items) => { this.items = items; + if (this.isFirst) { + this.dataSource.selected = this.items.find((item) => item.accessId.toLowerCase() === this.value.toLowerCase()); + this.selectedItem.emit(this.dataSource.selected); + } }); } diff --git a/web/src/environments/data-sources/taskana-customization.json b/web/src/environments/data-sources/taskana-customization.json index 297ac777c..d89fb7158 100644 --- a/web/src/environments/data-sources/taskana-customization.json +++ b/web/src/environments/data-sources/taskana-customization.json @@ -19,7 +19,7 @@ "lookupField": true }, "custom3": { - "field": "", + "field": "Custom 3", "visible": false }, "custom9": { @@ -27,15 +27,15 @@ "visible": true }, "custom10": { - "field": "", + "field": "Custom 10", "visible": false }, "custom11": { - "field": "", + "field": "Custom 11", "visible": false }, "custom12": { - "field": "", + "field": "Custom 12", "visible": false } }