From f18a8dc9324ab41226bb2853d9098bfd060ed7cb Mon Sep 17 00:00:00 2001 From: Sofie Hofmann <29145005+sofie29@users.noreply.github.com> Date: Wed, 1 Sep 2021 10:24:05 +0200 Subject: [PATCH] TSK-1630,TSK-1670: Enabled owner validiation and editing AccessItem name --- web/angular.json | 5 +- .../access-items-management.component.html | 5 +- .../access-items-management.component.spec.ts | 2 - .../access-items-management.component.ts | 11 +- .../workbasket-access-items.component.html | 11 +- .../workbasket-access-items.component.scss | 2 +- .../workbasket-access-items.component.ts | 4 +- .../workbasket-information.component.html | 14 +- .../workbasket-information.component.ts | 15 +- .../type-ahead/type-ahead.component.html | 47 +++-- .../type-ahead/type-ahead.component.scss | 42 ++-- .../type-ahead/type-ahead.component.spec.ts | 116 ++++++----- .../type-ahead/type-ahead.component.ts | 196 ++++++++---------- .../type-ahead/type-ahead.mock.component.ts | 29 --- web/src/app/shared/models/access-id.ts | 5 +- .../services/access-ids/access-ids.service.ts | 10 +- .../obtain-message/message-by-error-code.ts | 1 + web/src/app/shared/shared.module.ts | 2 +- .../access-items-management.actions.ts | 4 +- .../access-items-management.selector.ts | 4 +- .../access-items-management.state.ts | 8 +- .../workbasket-store/workbasket.state.ts | 1 + .../task-information.component.html | 13 +- .../task-information.component.ts | 11 +- 24 files changed, 276 insertions(+), 282 deletions(-) delete mode 100644 web/src/app/shared/components/type-ahead/type-ahead.mock.component.ts diff --git a/web/angular.json b/web/angular.json index 6307b64e1..770e219f2 100644 --- a/web/angular.json +++ b/web/angular.json @@ -57,7 +57,10 @@ "serve": { "builder": "@angular-devkit/build-angular:dev-server", "options": { - "browserTarget": "taskana-web:build" + "browserTarget": "taskana-web:build", + "sourceMap": { + "scripts": true + } }, "configurations": { "production": { diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.html b/web/src/app/administration/components/access-items-management/access-items-management.component.html index 6460eac8a..bc0917319 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.html +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.html @@ -2,9 +2,8 @@
- +
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 f43b0d10a..18adf98e3 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 @@ -120,7 +120,6 @@ describe('AccessItemsManagementComponent', () => { ...store.snapshot(), engineConfiguration: engineConfigurationMock }); - app.accessIdSelected = '1'; fixture.detectChanges(); })); @@ -181,7 +180,6 @@ describe('AccessItemsManagementComponent', () => { })); it('should display a dialog when access is revoked', async(() => { - app.accessIdSelected = 'xyz'; app.accessId = { accessId: 'xyz', name: 'xyz' }; const notificationService = TestBed.inject(NotificationService); const showDialogSpy = jest.spyOn(notificationService, 'showDialog').mockImplementation(); diff --git a/web/src/app/administration/components/access-items-management/access-items-management.component.ts b/web/src/app/administration/components/access-items-management/access-items-management.component.ts index f33bd647e..1c920fa5c 100644 --- a/web/src/app/administration/components/access-items-management/access-items-management.component.ts +++ b/web/src/app/administration/components/access-items-management/access-items-management.component.ts @@ -12,7 +12,7 @@ import { } from 'app/shared/models/sorting'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { takeUntil } from 'rxjs/operators'; -import { AccessIdDefinition } from '../../../shared/models/access-id'; +import { AccessId } from '../../../shared/models/access-id'; import { NotificationService } from '../../../shared/services/notifications/notification.service'; import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation'; import { customFieldCount } from '../../../shared/models/workbasket-access-items'; @@ -31,14 +31,13 @@ import { WorkbasketAccessItemQueryFilterParameter } from '../../../shared/models styleUrls: ['./access-items-management.component.scss'] }) export class AccessItemsManagementComponent implements OnInit { - accessIdSelected: string; accessIdPrevious: string; isRequired: boolean = false; accessIdName: string; panelState: boolean = false; accessItemsForm: FormGroup; - accessId: AccessIdDefinition; - groups: AccessIdDefinition[]; + accessId: AccessId; + groups: AccessId[]; defaultSortBy: WorkbasketAccessItemQuerySortParameter = WorkbasketAccessItemQuerySortParameter.ACCESS_ID; sortingFields: Map = WORKBASKET_ACCESS_ITEM_SORT_PARAMETER_NAMING; sortModel: Sorting = { @@ -50,7 +49,7 @@ export class AccessItemsManagementComponent implements OnInit { @Select(EngineConfigurationSelectors.accessItemsCustomisation) accessItemsCustomization$: Observable; - @Select(AccessItemsManagementSelector.groups) groups$: Observable; + @Select(AccessItemsManagementSelector.groups) groups$: Observable; customFields$: Observable; destroy$ = new Subject(); @@ -68,7 +67,7 @@ export class AccessItemsManagementComponent implements OnInit { }); } - onSelectAccessId(selected: AccessIdDefinition) { + onSelectAccessId(selected: AccessId) { if (selected) { this.accessId = selected; if (this.accessIdPrevious !== selected.accessId) { 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 5ea84af6f..23283550d 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 @@ -48,11 +48,12 @@ class="workbasket-access-items__typeahead" [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId), 'has-error': !accessItem.value.accessId }"> - + 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 85c4b45c6..a1b02ccb2 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 @@ -104,7 +104,7 @@ height: 58px; } -::ng-deep .workbasket-access-items__typeahead .typeahead__form .mat-form-field-infix { +::ng-deep .workbasket-access-items__typeahead .type-ahead__form-field .mat-form-field-infix { padding: 0 0 0 0; position: initial; font-size: medium; 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 b12372c3d..2b24d1ecf 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 @@ -19,7 +19,7 @@ import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbaske import { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service'; import { highlight } from 'app/shared/animations/validation.animation'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; -import { AccessIdDefinition } from 'app/shared/models/access-id'; +import { AccessId } from 'app/shared/models/access-id'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { filter, take, takeUntil, tap } from 'rxjs/operators'; import { NotificationService } from '../../../shared/services/notifications/notification.service'; @@ -303,7 +303,7 @@ export class WorkbasketAccessItemsComponent implements OnInit, OnChanges, OnDest }); } - accessItemSelected(accessItem: AccessIdDefinition, row: number) { + accessItemSelected(accessItem: AccessId, row: number) { this.accessItemsGroups.controls[row].get('accessId').setValue(accessItem?.accessId); this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem?.name); } diff --git a/web/src/app/administration/components/workbasket-information/workbasket-information.component.html b/web/src/app/administration/components/workbasket-information/workbasket-information.component.html index 4cf5101b0..09dcd5860 100644 --- a/web/src/app/administration/components/workbasket-information/workbasket-information.component.html +++ b/web/src/app/administration/components/workbasket-information/workbasket-information.component.html @@ -40,13 +40,13 @@ - -
{{lengthError}}
+ diff --git a/web/src/app/administration/components/workbasket-information/workbasket-information.component.ts b/web/src/app/administration/components/workbasket-information/workbasket-information.component.ts index 1966a3975..42cb6070e 100644 --- a/web/src/app/administration/components/workbasket-information/workbasket-information.component.ts +++ b/web/src/app/administration/components/workbasket-information/workbasket-information.component.ts @@ -21,7 +21,7 @@ import { import { WorkbasketComponent } from '../../models/workbasket-component'; import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors'; import { ButtonAction } from '../../models/button-action'; -import { AccessIdDefinition } from '../../../shared/models/access-id'; +import { AccessId } from '../../../shared/models/access-id'; @Component({ selector: 'taskana-administration-workbasket-information', @@ -42,6 +42,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest allTypes: Map; toggleValidationMap = new Map(); lookupField = false; + isOwnerValid: boolean = true; readonly lengthError = 'You have reached the maximum length for this field'; inputOverflowMap = new Map(); @@ -98,7 +99,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest .subscribe((button) => { switch (button) { case ButtonAction.SAVE: - this.onSave(); + this.onSubmit(); break; case ButtonAction.UNDO: this.onUndo(); @@ -122,8 +123,10 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest onSubmit() { this.formsValidatorService.formSubmitAttempt = true; this.formsValidatorService.validateFormInformation(this.workbasketForm, this.toggleValidationMap).then((value) => { - if (value) { + if (value && this.isOwnerValid) { this.onSave(); + } else { + this.notificationService.showError('WORKBASKET_SAVE'); } }); } @@ -191,10 +194,8 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest }); } - onSelectedOwner(owner: AccessIdDefinition) { - if (owner?.accessId) { - this.workbasket.owner = owner.accessId; - } + onSelectedOwner(owner: AccessId) { + this.workbasket.owner = owner.accessId; } getWorkbasketCustomProperty(custom: number) { 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 9373fdf24..69fc5aa51 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,18 +1,29 @@ -
-
- - - {{dataSource.selected?.name && placeHolderMessage !== 'Owner' ? dataSource.selected?.name : placeHolderMessage}} - - - - - {{item.accessId}}  {{item.name}} - - - -
-
+
+
+ + {{name || placeHolderMessage}} + + + + + Access id not valid + + + + + {{accessId.accessId}}  {{accessId.name}} + + + +
+
+ diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.scss b/web/src/app/shared/components/type-ahead/type-ahead.component.scss index 8855b7576..702b9d82f 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.scss +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.scss @@ -1,26 +1,34 @@ @import '../../../../theme/colors'; -::placeholder { - /* Chrome, Firefox, Opera, Safari 10.1+ */ - opacity: 1; /* Firefox */ +.type-ahead { + min-height: 0; + + &__form-field { + width: 100% !important; + } + + &__form-options { + white-space: pre; + } + + &__input-field { + left: 50%; + } + + &__error--accessId { + white-space: nowrap; + padding-top: 8px; + } + + ::ng-deep &--small > .mat-form-field-appearance-outline div.mat-form-field-infix { + padding: 0.25em; + } } -.disable { - cursor: not-allowed; +::ng-deep .ng-invalid.ng-touched:not(form) { + box-shadow: unset; } .invalid { color: $invalid; } - -.typeahead__form { - width: 100% !important; -} - -.typeahead__form-options { - white-space: pre; -} - -.align { - left: 50%; -} diff --git a/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts b/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts index 9961cc11b..3cadca741 100644 --- a/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts +++ b/web/src/app/shared/components/type-ahead/type-ahead.component.spec.ts @@ -1,74 +1,82 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing'; import { DebugElement } from '@angular/core'; -import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service'; import { TypeAheadComponent } from './type-ahead.component'; -import { BrowserModule, By } from '@angular/platform-browser'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { HttpClientTestingModule } from '@angular/common/http/testing'; -import { RouterModule } from '@angular/router'; -import { RouterTestingModule } from '@angular/router/testing'; -import { MatSelectModule } from '@angular/material/select'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { AccessIdsService } from '../../services/access-ids/access-ids.service'; +import { of } from 'rxjs'; +import { NgxsModule } from '@ngxs/store'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { FormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatTooltipModule } from '@angular/material/tooltip'; -import { EMPTY } from 'rxjs'; -const AccessIdsServiceSpy: Partial = { - getAccessItems: jest.fn().mockReturnValue(EMPTY), - searchForAccessId: jest.fn().mockReturnValue(EMPTY) +const accessIdService: Partial = { + searchForAccessId: jest.fn().mockReturnValue(of([{ accessId: 'user-g-1', name: 'Gerda' }])) }; -describe('TypeAheadComponent', () => { - let component: TypeAheadComponent; +describe('TypeAheadComponent with AccessId input', () => { let fixture: ComponentFixture; let debugElement: DebugElement; + let component: TypeAheadComponent; - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [TypeAheadComponent], - imports: [ - BrowserModule, - RouterModule, - RouterTestingModule, - HttpClientTestingModule, - MatSelectModule, - MatAutocompleteModule, - MatFormFieldModule, - MatInputModule, - MatTooltipModule, - FormsModule, - BrowserAnimationsModule - ], - providers: [{ provide: AccessIdsService, useValue: AccessIdsServiceSpy }] - }).compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [ + NgxsModule.forRoot([]), + MatFormFieldModule, + MatInputModule, + MatAutocompleteModule, + MatTooltipModule, + BrowserAnimationsModule, + FormsModule, + ReactiveFormsModule + ], + declarations: [TypeAheadComponent], + providers: [{ provide: AccessIdsService, useValue: accessIdService }] + }).compileComponents(); - beforeEach(() => { - fixture = TestBed.createComponent(TypeAheadComponent); - debugElement = fixture.debugElement; - component = fixture.debugElement.componentInstance; - fixture.detectChanges(); - }); + fixture = TestBed.createComponent(TypeAheadComponent); + debugElement = fixture.debugElement; + component = fixture.componentInstance; + fixture.detectChanges(); + }) + ); it('should create component', () => { expect(component).toBeTruthy(); }); - it('should change value via the input field', async(() => { - component.value = 'val_1'; - component.initializeDataSource(); + it('should fetch name when typing in an access id', fakeAsync(() => { + const input = debugElement.nativeElement.querySelector('.type-ahead__input-field'); + expect(input).toBeTruthy(); + input.value = 'user-g-1'; + input.dispatchEvent(new Event('input')); + component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); + + tick(); + expect(component.name).toBe('Gerda'); + })); + + it('should emit false when an invalid access id is set', fakeAsync(() => { + const emitSpy = jest.spyOn(component.isFormValid, 'emit'); + component.displayError = true; + component.accessIdForm.get('accessId').setValue('invalid-user'); + component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); + + tick(); fixture.detectChanges(); - fixture.whenStable().then(() => { - let input = debugElement.query(By.css('.typeahead__form-input')); - let el = input.nativeElement; - expect(el.value).toBe('val_1'); - el.value = 'val_2'; - el.dispatchEvent(new Event('input')); - expect(component.value).toBe('val_2'); - component.initializeDataSource(); - expect(component.items.length).toBeNull; - }); + expect(emitSpy).toHaveBeenCalledWith(false); + })); + + it('should emit true when a valid access id is set', fakeAsync(() => { + const emitSpy = jest.spyOn(component.isFormValid, 'emit'); + component.accessIdForm.get('accessId').setValue('user-g-1'); + component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true }); + + tick(); + fixture.detectChanges(); + expect(emitSpy).toHaveBeenCalledWith(true); })); }); 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 0adbb97be..7062e797e 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,127 +1,115 @@ -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, 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'; +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core'; +import { AccessIdsService } from '../../services/access-ids/access-ids.service'; +import { Observable, Subject } from 'rxjs'; +import { FormControl, FormGroup } from '@angular/forms'; +import { AccessId } from '../../models/access-id'; +import { take, takeUntil } from 'rxjs/operators'; +import { Select } from '@ngxs/store'; +import { WorkbasketSelectors } from '../../store/workbasket-store/workbasket.selectors'; +import { ButtonAction } from '../../../administration/models/button-action'; @Component({ selector: 'taskana-shared-type-ahead', templateUrl: './type-ahead.component.html', - styleUrls: ['./type-ahead.component.scss'], - animations: [highlight], - providers: [ - { - provide: NG_VALUE_ACCESSOR, - useExisting: forwardRef(() => TypeAheadComponent), - multi: true - } - ] + styleUrls: ['./type-ahead.component.scss'] }) -export class TypeAheadComponent implements ControlValueAccessor { - dataSource: any; - typing = false; - isFirst = false; - items = []; +export class TypeAheadComponent implements OnInit, OnDestroy { + @Input() savedAccessId; + @Input() placeHolderMessage; + @Input() entityId; + @Input() isRequired = false; + @Input() isDisabled = false; + @Input() displayError = false; - @Input() - placeHolderMessage; + @Output() accessIdEventEmitter = new EventEmitter(); + @Output() isFormValid = new EventEmitter(); - @Input() - validationValue; + @Select(WorkbasketSelectors.buttonAction) + buttonAction$: Observable; - @Input() - displayError; + name: string = ''; + lastSavedAccessId: string = ''; + filteredAccessIds: AccessId[] = []; + destroy$ = new Subject(); + accessIdForm = new FormGroup({ + accessId: new FormControl('') + }); + emptyAccessId: AccessId = { accessId: '', name: '' }; - @Input() - width; + constructor(private accessIdService: AccessIdsService) {} - @Input() - disable; - - @Input() - isRequired; - - @Output() - selectedItem = new EventEmitter(); - - @ViewChild('inputTypeAhead') - typeaheadLoading = false; - typeaheadMinLength = 3; - typeaheadWaitMs = 500; - typeaheadOptionsInScrollableView = 6; - - // The internal data model - private innerValue: any; - - // Placeholders for the callbacks which are later provided - // by the Control Value Accessor - private onTouchedCallback: () => {}; - private onChangeCallback: (_: any) => {}; - - // get accessor - get value(): any { - return this.innerValue; - } - - // set accessor including call the onchange callback - set value(v: any) { - if (v !== this.innerValue) { - this.innerValue = v; + ngOnChanges(changes: SimpleChanges) { + // currently needed because when saving, workbasket-details components sends old workbasket which reverts changes in this component + if (changes.entityId) { + this.setAccessIdFromInput(); } } - // From ControlValueAccessor interface - writeValue(value: any) { - if (value !== this.innerValue) { - this.innerValue = value; - if (this.value) { - this.isFirst = true; - } - this.initializeDataSource(); + ngOnInit() { + if (this.isDisabled) { + this.accessIdForm.controls['accessId'].disable(); } - } - // From ControlValueAccessor interface - registerOnChange(fn: any) { - this.onChangeCallback = fn; - } - - // From ControlValueAccessor interface - registerOnTouched(fn: any) { - this.onTouchedCallback = fn; - } - - constructor(private accessIdsService: AccessIdsService) {} - - 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); + // currently needed because this component cannot obtain changes of the current workbasket from workbasket-information component + this.buttonAction$.pipe(takeUntil(this.destroy$)).subscribe((button) => { + if (button == ButtonAction.UNDO) { + this.accessIdForm.controls['accessId'].setValue(this.lastSavedAccessId); } }); - } - getUsersAsObservable(accessId: string): Observable { - return this.accessIdsService.searchForAccessId(accessId); - } - - typeaheadOnSelect(event): void { - if (event) { - if (this.items.length > 0) { - this.dataSource.selected = this.items.find((item) => item.accessId.toLowerCase() === this.value.toLowerCase()); + this.accessIdForm.controls['accessId'].valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => { + const value = this.accessIdForm.controls['accessId'].value; + if (value === '') { + this.handleEmptyAccessId(); + return; } - this.selectedItem.emit(this.dataSource.selected); + this.searchForAccessId(value); + }); + + this.setAccessIdFromInput(); + } + + handleEmptyAccessId() { + this.name = ''; + this.isFormValid.emit(!this.isRequired); + if (this.placeHolderMessage !== 'Search for AccessId') { + this.accessIdEventEmitter.emit(this.emptyAccessId); } - if (document.activeElement instanceof HTMLElement) { - document.activeElement.blur(); + if (this.isRequired) { + this.accessIdForm.controls['accessId'].setErrors({ incorrect: true }); } } + + searchForAccessId(value: string) { + this.accessIdService + .searchForAccessId(value) + .pipe(take(1)) + .subscribe((accessIds) => { + this.filteredAccessIds = accessIds; + const accessId = accessIds.find((accessId) => accessId.accessId === value); + + if (typeof accessId !== 'undefined') { + this.name = accessId?.name; + this.isFormValid.emit(true); + this.accessIdEventEmitter.emit(accessId); + } else if (this.displayError) { + this.isFormValid.emit(false); + this.accessIdEventEmitter.emit(this.emptyAccessId); + this.accessIdForm.controls['accessId'].setErrors({ incorrect: true }); + } + }); + } + + setAccessIdFromInput() { + const accessId = this.savedAccessId?.value; + const access = accessId?.accessId || accessId?.accessId == '' ? accessId.accessId : this.savedAccessId || ''; + this.accessIdForm.controls['accessId'].setValue(access); + this.lastSavedAccessId = access; + this.name = accessId?.accessName || ''; + } + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + } } diff --git a/web/src/app/shared/components/type-ahead/type-ahead.mock.component.ts b/web/src/app/shared/components/type-ahead/type-ahead.mock.component.ts deleted file mode 100644 index ad5386a8b..000000000 --- a/web/src/app/shared/components/type-ahead/type-ahead.mock.component.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Component, forwardRef, Input } from '@angular/core'; -import { NG_VALUE_ACCESSOR, ControlValueAccessor } from '@angular/forms'; - -@Component({ - selector: 'taskana-shared-type-ahead', - template: 'dummydetail', - providers: [ - { - provide: NG_VALUE_ACCESSOR, - multi: true, - useExisting: forwardRef(() => TaskanaTypeAheadMockComponent) - } - ] -}) -export class TaskanaTypeAheadMockComponent implements ControlValueAccessor { - @Input() - placeHolderMessage; - - @Input() - validationValue; - - writeValue(obj: any): void {} - - registerOnChange(fn: any): void {} - - registerOnTouched(fn: any): void {} - - setDisabledState?(isDisabled: boolean): void {} -} diff --git a/web/src/app/shared/models/access-id.ts b/web/src/app/shared/models/access-id.ts index 75c026fd0..1ead44f11 100644 --- a/web/src/app/shared/models/access-id.ts +++ b/web/src/app/shared/models/access-id.ts @@ -1,3 +1,4 @@ -export class AccessIdDefinition { - constructor(public accessId?: string, public name?: string) {} +export interface AccessId { + accessId?: string; + name?: string; } diff --git a/web/src/app/shared/services/access-ids/access-ids.service.ts b/web/src/app/shared/services/access-ids/access-ids.service.ts index 9b7582a6a..c5e97c204 100644 --- a/web/src/app/shared/services/access-ids/access-ids.service.ts +++ b/web/src/app/shared/services/access-ids/access-ids.service.ts @@ -1,7 +1,7 @@ import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { environment } from 'environments/environment'; -import { AccessIdDefinition } from 'app/shared/models/access-id'; +import { AccessId } from 'app/shared/models/access-id'; import { Observable, of } from 'rxjs'; import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation'; import { Sorting, WorkbasketAccessItemQuerySortParameter } from 'app/shared/models/sorting'; @@ -20,18 +20,18 @@ export class AccessIdsService { return this.startupService.getTaskanaRestUrl() + '/v1/access-ids'; } - searchForAccessId(accessId: string): Observable { + searchForAccessId(accessId: string): Observable { if (!accessId || accessId.length < 3) { return of([]); } - return this.httpClient.get(`${this.url}?search-for=${accessId}`); + return this.httpClient.get(`${this.url}?search-for=${accessId}`); } - getGroupsByAccessId(accessId: string): Observable { + getGroupsByAccessId(accessId: string): Observable { if (!accessId || accessId.length < 3) { return of([]); } - return this.httpClient.get(`${this.url}/groups?access-id=${accessId}`); + return this.httpClient.get(`${this.url}/groups?access-id=${accessId}`); } getAccessItems( diff --git a/web/src/app/shared/services/obtain-message/message-by-error-code.ts b/web/src/app/shared/services/obtain-message/message-by-error-code.ts index 028403a25..3237e875d 100644 --- a/web/src/app/shared/services/obtain-message/message-by-error-code.ts +++ b/web/src/app/shared/services/obtain-message/message-by-error-code.ts @@ -29,6 +29,7 @@ export const messageByErrorCode = { CLASSIFICATION_WITH_ID_NOT_FOUND: 'Classification with id {classificationId} cannot be found', CLASSIFICATION_COPY_NOT_CREATED: 'Cannot copy a not created Classification', + WORKBASKET_SAVE: 'The Workbasket cannot be saved since the Workbasket Information contains invalid values', WORKBASKET_WITH_ID_NOT_FOUND: 'Workbasket with id {workbasketId} cannot be found', WORKBASKET_WITH_KEY_NOT_FOUND: 'Workbasket with key {workbasketKey} cannot be found in domain {domain}', WORKBASKET_ALREADY_EXISTS: diff --git a/web/src/app/shared/shared.module.ts b/web/src/app/shared/shared.module.ts index c5a2283fb..c52cb7d70 100644 --- a/web/src/app/shared/shared.module.ts +++ b/web/src/app/shared/shared.module.ts @@ -17,7 +17,6 @@ import { AccordionModule } from 'ngx-bootstrap/accordion'; import { SpinnerComponent } from 'app/shared/components/spinner/spinner.component'; import { MasterAndDetailComponent } from 'app/shared/components/master-and-detail/master-and-detail.component'; import { TaskanaTreeComponent } from 'app/administration/components/tree/tree.component'; -import { TypeAheadComponent } from 'app/shared/components/type-ahead/type-ahead.component'; import { IconTypeComponent } from 'app/administration/components/type-icon/icon-type.component'; import { FieldErrorDisplayComponent } from 'app/shared/components/field-error-display/field-error-display.component'; import { MatDialogModule } from '@angular/material/dialog'; @@ -26,6 +25,7 @@ import { MatRadioModule } from '@angular/material/radio'; import { SortComponent } from './components/sort/sort.component'; import { PaginationComponent } from './components/pagination/pagination.component'; import { ProgressSpinnerComponent } from './components/progress-spinner/progress-spinner.component'; +import { TypeAheadComponent } from './components/type-ahead/type-ahead.component'; /** * Pipes diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts index 46eb19d66..ff367ae92 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.actions.ts @@ -1,11 +1,11 @@ -import { AccessIdDefinition } from '../../models/access-id'; +import { AccessId } from '../../models/access-id'; import { Sorting, WorkbasketAccessItemQuerySortParameter } from '../../models/sorting'; import { WorkbasketAccessItemQueryFilterParameter } from '../../models/workbasket-access-item-query-filter-parameter'; import { QueryPagingParameter } from '../../models/query-paging-parameter'; export class SelectAccessId { static readonly type = '[Access Items Management] Select access ID'; - constructor(public accessIdDefinition: AccessIdDefinition) {} + constructor(public accessIdDefinition: AccessId) {} } export class GetGroupsByAccessId { diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts index 3cb3a742f..6d87d727c 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.selector.ts @@ -1,10 +1,10 @@ import { Selector } from '@ngxs/store'; import { AccessItemsManagementState, AccessItemsManagementStateModel } from './access-items-management.state'; -import { AccessIdDefinition } from '../../models/access-id'; +import { AccessId } from '../../models/access-id'; export class AccessItemsManagementSelector { @Selector([AccessItemsManagementState]) - static groups(state: AccessItemsManagementStateModel): AccessIdDefinition[] { + static groups(state: AccessItemsManagementStateModel): AccessId[] { return state.groups; } } diff --git a/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts b/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts index 64674ee4a..300b12d1a 100644 --- a/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts +++ b/web/src/app/shared/store/access-items-management-store/access-items-management.state.ts @@ -8,7 +8,7 @@ import { import { Observable, of } from 'rxjs'; import { AccessIdsService } from '../../services/access-ids/access-ids.service'; import { take, tap } from 'rxjs/operators'; -import { AccessIdDefinition } from '../../models/access-id'; +import { AccessId } from '../../models/access-id'; import { NotificationService } from '../../services/notifications/notification.service'; import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation'; import { RequestInProgressService } from '../../services/request-in-progress/request-in-progress.service'; @@ -44,7 +44,7 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap { return this.accessIdsService.getGroupsByAccessId(action.accessId).pipe( take(1), tap( - (groups: AccessIdDefinition[]) => { + (groups: AccessId[]) => { ctx.patchState({ groups }); @@ -106,6 +106,6 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap { export interface AccessItemsManagementStateModel { accessItemsResource: WorkbasketAccessItemsRepresentation; - selectedAccessId: AccessIdDefinition; - groups: AccessIdDefinition[]; + selectedAccessId: AccessId; + groups: AccessId[]; } 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 19eec12ed..59a118e9f 100644 --- a/web/src/app/shared/store/workbasket-store/workbasket.state.ts +++ b/web/src/app/shared/store/workbasket-store/workbasket.state.ts @@ -241,6 +241,7 @@ export class WorkbasketState implements NgxsAfterBootstrap { const date = TaskanaDate.getDate(); emptyWorkbasket.created = date; emptyWorkbasket.modified = date; + emptyWorkbasket.owner = ''; const accessItems = { accessItems: [], _links: {} }; const distributionTargets = { distributionTargets: [], _links: {} }; diff --git a/web/src/app/workplace/components/task-information/task-information.component.html b/web/src/app/workplace/components/task-information/task-information.component.html index 3f549057c..195d677ad 100644 --- a/web/src/app/workplace/components/task-information/task-information.component.html +++ b/web/src/app/workplace/components/task-information/task-information.component.html @@ -77,10 +77,15 @@ - + diff --git a/web/src/app/workplace/components/task-information/task-information.component.ts b/web/src/app/workplace/components/task-information/task-information.component.ts index 31cf7a99e..2d47a27bc 100644 --- a/web/src/app/workplace/components/task-information/task-information.component.ts +++ b/web/src/app/workplace/components/task-information/task-information.component.ts @@ -12,7 +12,6 @@ import { import { Task } from 'app/workplace/models/task'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { NgForm } from '@angular/forms'; -import { DomainService } from 'app/shared/services/domain/domain.service'; import { Select } from '@ngxs/store'; import { Observable, Subject } from 'rxjs'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; @@ -20,7 +19,7 @@ import { ClassificationsService } from '../../../shared/services/classifications import { Classification } from '../../../shared/models/classification'; import { TasksCustomisation } from '../../../shared/models/customisation'; import { takeUntil } from 'rxjs/operators'; -import { AccessIdDefinition } from '../../../shared/models/access-id'; +import { AccessId } from '../../../shared/models/access-id'; @Component({ selector: 'taskana-task-information', @@ -45,6 +44,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy { requestInProgress = false; classifications: Classification[]; isClassificationEmpty: boolean; + isOwnerValid: boolean = true; readonly lengthError = 'You have reached the maximum length'; inputOverflowMap = new Map(); @@ -55,8 +55,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy { constructor( private classificationService: ClassificationsService, - private formsValidatorService: FormsValidatorService, - private domainService: DomainService + private formsValidatorService: FormsValidatorService ) {} ngOnInit() { @@ -102,7 +101,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy { this.isClassificationEmpty = typeof this.task.classificationSummary === 'undefined'; this.formsValidatorService.formSubmitAttempt = true; this.formsValidatorService.validateFormInformation(this.taskForm, this.toggleValidationMap).then((value) => { - if (value && !this.isClassificationEmpty) { + if (value && !this.isClassificationEmpty && this.isOwnerValid) { this.formValid.emit(true); } }); @@ -120,7 +119,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy { }); } - onSelectedOwner(owner: AccessIdDefinition) { + onSelectedOwner(owner: AccessId) { if (owner?.accessId) { this.task.owner = owner.accessId; }