TSK-1630,TSK-1670: Enabled owner validiation and editing AccessItem name

This commit is contained in:
Sofie Hofmann 2021-09-01 10:24:05 +02:00
parent 0fcc08ddd2
commit f18a8dc932
24 changed files with 276 additions and 282 deletions

View File

@ -57,7 +57,10 @@
"serve": { "serve": {
"builder": "@angular-devkit/build-angular:dev-server", "builder": "@angular-devkit/build-angular:dev-server",
"options": { "options": {
"browserTarget": "taskana-web:build" "browserTarget": "taskana-web:build",
"sourceMap": {
"scripts": true
}
}, },
"configurations": { "configurations": {
"production": { "production": {

View File

@ -2,9 +2,8 @@
<!-- SEARCH --> <!-- SEARCH -->
<div class="access-items__typeahead"> <div class="access-items__typeahead">
<taskana-shared-type-ahead name="accessIdSelected" [(ngModel)]="accessIdSelected" <taskana-shared-type-ahead
placeHolderMessage="Search for access id..." (selectedItem)="onSelectAccessId($event)" displayError=true placeHolderMessage="Search for AccessId" (accessIdEventEmitter)="onSelectAccessId($event)">
isRequired="false">
</taskana-shared-type-ahead> </taskana-shared-type-ahead>
</div> </div>
<div *ngIf="!accessItemsForm" class="access-items__icon"> <div *ngIf="!accessItemsForm" class="access-items__icon">

View File

@ -120,7 +120,6 @@ describe('AccessItemsManagementComponent', () => {
...store.snapshot(), ...store.snapshot(),
engineConfiguration: engineConfigurationMock engineConfiguration: engineConfigurationMock
}); });
app.accessIdSelected = '1';
fixture.detectChanges(); fixture.detectChanges();
})); }));
@ -181,7 +180,6 @@ describe('AccessItemsManagementComponent', () => {
})); }));
it('should display a dialog when access is revoked', async(() => { it('should display a dialog when access is revoked', async(() => {
app.accessIdSelected = 'xyz';
app.accessId = { accessId: 'xyz', name: 'xyz' }; app.accessId = { accessId: 'xyz', name: 'xyz' };
const notificationService = TestBed.inject(NotificationService); const notificationService = TestBed.inject(NotificationService);
const showDialogSpy = jest.spyOn(notificationService, 'showDialog').mockImplementation(); const showDialogSpy = jest.spyOn(notificationService, 'showDialog').mockImplementation();

View File

@ -12,7 +12,7 @@ import {
} from 'app/shared/models/sorting'; } from 'app/shared/models/sorting';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { takeUntil } from 'rxjs/operators'; 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 { NotificationService } from '../../../shared/services/notifications/notification.service';
import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation'; import { AccessItemsCustomisation, CustomField, getCustomFields } from '../../../shared/models/customisation';
import { customFieldCount } from '../../../shared/models/workbasket-access-items'; import { customFieldCount } from '../../../shared/models/workbasket-access-items';
@ -31,14 +31,13 @@ import { WorkbasketAccessItemQueryFilterParameter } from '../../../shared/models
styleUrls: ['./access-items-management.component.scss'] styleUrls: ['./access-items-management.component.scss']
}) })
export class AccessItemsManagementComponent implements OnInit { export class AccessItemsManagementComponent implements OnInit {
accessIdSelected: string;
accessIdPrevious: string; accessIdPrevious: string;
isRequired: boolean = false; isRequired: boolean = false;
accessIdName: string; accessIdName: string;
panelState: boolean = false; panelState: boolean = false;
accessItemsForm: FormGroup; accessItemsForm: FormGroup;
accessId: AccessIdDefinition; accessId: AccessId;
groups: AccessIdDefinition[]; groups: AccessId[];
defaultSortBy: WorkbasketAccessItemQuerySortParameter = WorkbasketAccessItemQuerySortParameter.ACCESS_ID; defaultSortBy: WorkbasketAccessItemQuerySortParameter = WorkbasketAccessItemQuerySortParameter.ACCESS_ID;
sortingFields: Map<WorkbasketAccessItemQuerySortParameter, string> = WORKBASKET_ACCESS_ITEM_SORT_PARAMETER_NAMING; sortingFields: Map<WorkbasketAccessItemQuerySortParameter, string> = WORKBASKET_ACCESS_ITEM_SORT_PARAMETER_NAMING;
sortModel: Sorting<WorkbasketAccessItemQuerySortParameter> = { sortModel: Sorting<WorkbasketAccessItemQuerySortParameter> = {
@ -50,7 +49,7 @@ export class AccessItemsManagementComponent implements OnInit {
@Select(EngineConfigurationSelectors.accessItemsCustomisation) @Select(EngineConfigurationSelectors.accessItemsCustomisation)
accessItemsCustomization$: Observable<AccessItemsCustomisation>; accessItemsCustomization$: Observable<AccessItemsCustomisation>;
@Select(AccessItemsManagementSelector.groups) groups$: Observable<AccessIdDefinition[]>; @Select(AccessItemsManagementSelector.groups) groups$: Observable<AccessId[]>;
customFields$: Observable<CustomField[]>; customFields$: Observable<CustomField[]>;
destroy$ = new Subject<void>(); destroy$ = new Subject<void>();
@ -68,7 +67,7 @@ export class AccessItemsManagementComponent implements OnInit {
}); });
} }
onSelectAccessId(selected: AccessIdDefinition) { onSelectAccessId(selected: AccessId) {
if (selected) { if (selected) {
this.accessId = selected; this.accessId = selected;
if (this.accessIdPrevious !== selected.accessId) { if (this.accessIdPrevious !== selected.accessId) {

View File

@ -48,11 +48,12 @@
class="workbasket-access-items__typeahead" [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId), class="workbasket-access-items__typeahead" [ngClass]="{ 'has-warning': (accessItemsClone[index].accessId !== accessItem.value.accessId),
'has-error': !accessItem.value.accessId }"> 'has-error': !accessItem.value.accessId }">
<taskana-shared-type-ahead formControlName="accessId" <taskana-shared-type-ahead
placeHolderMessage="Access id *" [savedAccessId]="accessItem"
[validationValue]="toggleValidationAccessIdMap.get(index)" placeHolderMessage="Access id"
[displayError]="!isFieldValid('accessItem.value.accessId', index)" [displayError]="true"
(selectedItem)="accessItemSelected($event, index)"> [isRequired]="true"
(accessIdEventEmitter)="accessItemSelected($event, index)">
</taskana-shared-type-ahead> </taskana-shared-type-ahead>
</td> </td>

View File

@ -104,7 +104,7 @@
height: 58px; 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; padding: 0 0 0 0;
position: initial; position: initial;
font-size: medium; font-size: medium;

View File

@ -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 { RequestInProgressService } from 'app/shared/services/request-in-progress/request-in-progress.service';
import { highlight } from 'app/shared/animations/validation.animation'; import { highlight } from 'app/shared/animations/validation.animation';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; 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 { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors';
import { filter, take, takeUntil, tap } from 'rxjs/operators'; import { filter, take, takeUntil, tap } from 'rxjs/operators';
import { NotificationService } from '../../../shared/services/notifications/notification.service'; 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('accessId').setValue(accessItem?.accessId);
this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem?.name); this.accessItemsGroups.controls[row].get('accessName').setValue(accessItem?.name);
} }

View File

@ -40,13 +40,13 @@
</taskana-shared-field-error-display> </taskana-shared-field-error-display>
<!-- OWNER --> <!-- OWNER -->
<taskana-shared-type-ahead *ngIf="lookupField else ownerInput" isRequired="true" maxlength="128" <taskana-shared-type-ahead *ngIf="lookupField else ownerInput"
#owner="ngModel" name="workbasket.owner" [(ngModel)]="workbasket.owner" placeHolderMessage="Owner" [savedAccessId]="workbasket.owner"
[validationValue]="this.toggleValidationMap.get('workbasket.owner') " placeHolderMessage="Owner"
[displayError]="!isFieldValid('workbasket.owner')" [entityId]="workbasket.workbasketId"
(input)="validateInputOverflow(owner, 128)" [displayError]="true"
(selectedItem)="onSelectedOwner($event)"> (isFormValid)="isOwnerValid = $event"
<div *ngIf="inputOverflowMap.get(owner.name)" class="error">{{lengthError}}</div> (accessIdEventEmitter)="onSelectedOwner($event)">
</taskana-shared-type-ahead> </taskana-shared-type-ahead>
<ng-template #ownerInput> <ng-template #ownerInput>

View File

@ -21,7 +21,7 @@ import {
import { WorkbasketComponent } from '../../models/workbasket-component'; import { WorkbasketComponent } from '../../models/workbasket-component';
import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors'; import { WorkbasketSelectors } from '../../../shared/store/workbasket-store/workbasket.selectors';
import { ButtonAction } from '../../models/button-action'; import { ButtonAction } from '../../models/button-action';
import { AccessIdDefinition } from '../../../shared/models/access-id'; import { AccessId } from '../../../shared/models/access-id';
@Component({ @Component({
selector: 'taskana-administration-workbasket-information', selector: 'taskana-administration-workbasket-information',
@ -42,6 +42,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
allTypes: Map<string, string>; allTypes: Map<string, string>;
toggleValidationMap = new Map<string, boolean>(); toggleValidationMap = new Map<string, boolean>();
lookupField = false; lookupField = false;
isOwnerValid: boolean = true;
readonly lengthError = 'You have reached the maximum length for this field'; readonly lengthError = 'You have reached the maximum length for this field';
inputOverflowMap = new Map<string, boolean>(); inputOverflowMap = new Map<string, boolean>();
@ -98,7 +99,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
.subscribe((button) => { .subscribe((button) => {
switch (button) { switch (button) {
case ButtonAction.SAVE: case ButtonAction.SAVE:
this.onSave(); this.onSubmit();
break; break;
case ButtonAction.UNDO: case ButtonAction.UNDO:
this.onUndo(); this.onUndo();
@ -122,8 +123,10 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
onSubmit() { onSubmit() {
this.formsValidatorService.formSubmitAttempt = true; this.formsValidatorService.formSubmitAttempt = true;
this.formsValidatorService.validateFormInformation(this.workbasketForm, this.toggleValidationMap).then((value) => { this.formsValidatorService.validateFormInformation(this.workbasketForm, this.toggleValidationMap).then((value) => {
if (value) { if (value && this.isOwnerValid) {
this.onSave(); this.onSave();
} else {
this.notificationService.showError('WORKBASKET_SAVE');
} }
}); });
} }
@ -191,11 +194,9 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
}); });
} }
onSelectedOwner(owner: AccessIdDefinition) { onSelectedOwner(owner: AccessId) {
if (owner?.accessId) {
this.workbasket.owner = owner.accessId; this.workbasket.owner = owner.accessId;
} }
}
getWorkbasketCustomProperty(custom: number) { getWorkbasketCustomProperty(custom: number) {
return `custom${custom}`; return `custom${custom}`;

View File

@ -1,18 +1,29 @@
<div *ngIf="dataSource" class="typeahead"> <form [formGroup]="accessIdForm">
<form> <div [ngClass]="placeHolderMessage == 'Access id'? 'type-ahead--small' : 'type-ahead--large'">
<mat-form-field class="typeahead__form" appearance="outline"> <mat-form-field class="type-ahead__form-field" appearance="outline">
<mat-label> <mat-label>{{name || placeHolderMessage}}</mat-label>
{{dataSource.selected?.name && placeHolderMessage !== 'Owner' ? dataSource.selected?.name : placeHolderMessage}} <!-- TEXT INPUT -->
</mat-label> <input matInput
<input #inputTypeAhead [required]="isRequired" class="typeahead__form-input align" matInput type="text" type="text"
[matAutocomplete]="auto" placeholder="{{placeHolderMessage}}" [(ngModel)]="value" name="accessId" placeholder="{{placeHolderMessage}}"
(ngModelChange)="initializeDataSource()" matTooltip="{{value}}"/> formControlName="accessId"
<mat-autocomplete #autoComplete autoActiveFirstOption (optionSelected)="typeaheadOnSelect($event)" [required]="isRequired"
#auto="matAutocomplete"> [matAutocomplete]="auto"
<mat-option class="typeahead__form-options" *ngFor="let item of items" [value]="item.accessId" matTooltip="{{item.accessId}} {{item.name}}"> class="type-ahead__input-field">
<small>{{item.accessId}}&nbsp;&nbsp;{{item.name}}</small> <!-- ERROR MESSAGE -->
<mat-error
[ngClass]="placeHolderMessage == 'Access id' ? 'type-ahead__error--accessId' : 'type-ahead__error--general'"
*ngIf="displayError && !accessIdForm.valid">
Access id not valid
</mat-error>
<!-- AUTOCOMPLETE LIST -->
<mat-autocomplete #auto>
<mat-option class="type-ahead__form-options" *ngFor="let accessId of filteredAccessIds"
[value]="accessId.accessId" matTooltip="{{accessId.accessId}} {{accessId.name}}">
<small>{{accessId.accessId}}&nbsp;&nbsp;{{accessId.name}}</small>
</mat-option> </mat-option>
</mat-autocomplete> </mat-autocomplete>
</mat-form-field> </mat-form-field>
</form>
</div> </div>
</form>

View File

@ -1,26 +1,34 @@
@import '../../../../theme/colors'; @import '../../../../theme/colors';
::placeholder { .type-ahead {
/* Chrome, Firefox, Opera, Safari 10.1+ */ min-height: 0;
opacity: 1; /* Firefox */
&__form-field {
width: 100% !important;
} }
.disable { &__form-options {
cursor: not-allowed; 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;
}
}
::ng-deep .ng-invalid.ng-touched:not(form) {
box-shadow: unset;
} }
.invalid { .invalid {
color: $invalid; color: $invalid;
} }
.typeahead__form {
width: 100% !important;
}
.typeahead__form-options {
white-space: pre;
}
.align {
left: 50%;
}

View File

@ -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 { DebugElement } from '@angular/core';
import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service';
import { TypeAheadComponent } from './type-ahead.component'; import { TypeAheadComponent } from './type-ahead.component';
import { BrowserModule, By } from '@angular/platform-browser'; import { AccessIdsService } from '../../services/access-ids/access-ids.service';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { of } from 'rxjs';
import { HttpClientTestingModule } from '@angular/common/http/testing'; import { NgxsModule } from '@ngxs/store';
import { RouterModule } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { MatSelectModule } from '@angular/material/select';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; 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 { MatTooltipModule } from '@angular/material/tooltip';
import { EMPTY } from 'rxjs';
const AccessIdsServiceSpy: Partial<AccessIdsService> = { const accessIdService: Partial<AccessIdsService> = {
getAccessItems: jest.fn().mockReturnValue(EMPTY), searchForAccessId: jest.fn().mockReturnValue(of([{ accessId: 'user-g-1', name: 'Gerda' }]))
searchForAccessId: jest.fn().mockReturnValue(EMPTY)
}; };
describe('TypeAheadComponent', () => { describe('TypeAheadComponent with AccessId input', () => {
let component: TypeAheadComponent;
let fixture: ComponentFixture<TypeAheadComponent>; let fixture: ComponentFixture<TypeAheadComponent>;
let debugElement: DebugElement; let debugElement: DebugElement;
let component: TypeAheadComponent;
beforeEach(async(() => { beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [TypeAheadComponent],
imports: [ imports: [
BrowserModule, NgxsModule.forRoot([]),
RouterModule,
RouterTestingModule,
HttpClientTestingModule,
MatSelectModule,
MatAutocompleteModule,
MatFormFieldModule, MatFormFieldModule,
MatInputModule, MatInputModule,
MatAutocompleteModule,
MatTooltipModule, MatTooltipModule,
BrowserAnimationsModule,
FormsModule, FormsModule,
BrowserAnimationsModule ReactiveFormsModule
], ],
providers: [{ provide: AccessIdsService, useValue: AccessIdsServiceSpy }] declarations: [TypeAheadComponent],
providers: [{ provide: AccessIdsService, useValue: accessIdService }]
}).compileComponents(); }).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(TypeAheadComponent); fixture = TestBed.createComponent(TypeAheadComponent);
debugElement = fixture.debugElement; debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance; component = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
}); })
);
it('should create component', () => { it('should create component', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should change value via the input field', async(() => { it('should fetch name when typing in an access id', fakeAsync(() => {
component.value = 'val_1'; const input = debugElement.nativeElement.querySelector('.type-ahead__input-field');
component.initializeDataSource(); 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.detectChanges();
fixture.whenStable().then(() => { expect(emitSpy).toHaveBeenCalledWith(false);
let input = debugElement.query(By.css('.typeahead__form-input')); }));
let el = input.nativeElement;
expect(el.value).toBe('val_1'); it('should emit true when a valid access id is set', fakeAsync(() => {
el.value = 'val_2'; const emitSpy = jest.spyOn(component.isFormValid, 'emit');
el.dispatchEvent(new Event('input')); component.accessIdForm.get('accessId').setValue('user-g-1');
expect(component.value).toBe('val_2'); component.accessIdForm.get('accessId').updateValueAndValidity({ emitEvent: true });
component.initializeDataSource();
expect(component.items.length).toBeNull; tick();
}); fixture.detectChanges();
expect(emitSpy).toHaveBeenCalledWith(true);
})); }));
}); });

View File

@ -1,127 +1,115 @@
import { Component, Input, ViewChild, forwardRef, Output, EventEmitter } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, SimpleChanges } from '@angular/core';
import { Observable } from 'rxjs'; import { AccessIdsService } from '../../services/access-ids/access-ids.service';
import { Observable, Subject } from 'rxjs';
import { AccessIdsService } from 'app/shared/services/access-ids/access-ids.service'; import { FormControl, FormGroup } from '@angular/forms';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { AccessId } from '../../models/access-id';
import { highlight } from 'app/shared/animations/validation.animation'; import { take, takeUntil } from 'rxjs/operators';
import { mergeMap } from 'rxjs/operators'; import { Select } from '@ngxs/store';
import { AccessIdDefinition } from 'app/shared/models/access-id'; import { WorkbasketSelectors } from '../../store/workbasket-store/workbasket.selectors';
import { ButtonAction } from '../../../administration/models/button-action';
@Component({ @Component({
selector: 'taskana-shared-type-ahead', selector: 'taskana-shared-type-ahead',
templateUrl: './type-ahead.component.html', templateUrl: './type-ahead.component.html',
styleUrls: ['./type-ahead.component.scss'], styleUrls: ['./type-ahead.component.scss']
animations: [highlight],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TypeAheadComponent),
multi: true
}
]
}) })
export class TypeAheadComponent implements ControlValueAccessor { export class TypeAheadComponent implements OnInit, OnDestroy {
dataSource: any; @Input() savedAccessId;
typing = false; @Input() placeHolderMessage;
isFirst = false; @Input() entityId;
items = []; @Input() isRequired = false;
@Input() isDisabled = false;
@Input() displayError = false;
@Input() @Output() accessIdEventEmitter = new EventEmitter<AccessId>();
placeHolderMessage; @Output() isFormValid = new EventEmitter<boolean>();
@Input() @Select(WorkbasketSelectors.buttonAction)
validationValue; buttonAction$: Observable<ButtonAction>;
@Input() name: string = '';
displayError; lastSavedAccessId: string = '';
filteredAccessIds: AccessId[] = [];
destroy$ = new Subject<void>();
accessIdForm = new FormGroup({
accessId: new FormControl('')
});
emptyAccessId: AccessId = { accessId: '', name: '' };
@Input() constructor(private accessIdService: AccessIdsService) {}
width;
@Input() ngOnChanges(changes: SimpleChanges) {
disable; // currently needed because when saving, workbasket-details components sends old workbasket which reverts changes in this component
if (changes.entityId) {
@Input() this.setAccessIdFromInput();
isRequired;
@Output()
selectedItem = new EventEmitter<AccessIdDefinition>();
@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;
} }
} }
// From ControlValueAccessor interface ngOnInit() {
writeValue(value: any) { if (this.isDisabled) {
if (value !== this.innerValue) { this.accessIdForm.controls['accessId'].disable();
this.innerValue = value;
if (this.value) {
this.isFirst = true;
} }
this.initializeDataSource();
// 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);
}
});
this.accessIdForm.controls['accessId'].valueChanges.pipe(takeUntil(this.destroy$)).subscribe(() => {
const value = this.accessIdForm.controls['accessId'].value;
if (value === '') {
this.handleEmptyAccessId();
return;
}
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 (this.isRequired) {
this.accessIdForm.controls['accessId'].setErrors({ incorrect: true });
} }
} }
// From ControlValueAccessor interface searchForAccessId(value: string) {
registerOnChange(fn: any) { this.accessIdService
this.onChangeCallback = fn; .searchForAccessId(value)
} .pipe(take(1))
.subscribe((accessIds) => {
this.filteredAccessIds = accessIds;
const accessId = accessIds.find((accessId) => accessId.accessId === value);
// From ControlValueAccessor interface if (typeof accessId !== 'undefined') {
registerOnTouched(fn: any) { this.name = accessId?.name;
this.onTouchedCallback = fn; this.isFormValid.emit(true);
} this.accessIdEventEmitter.emit(accessId);
} else if (this.displayError) {
constructor(private accessIdsService: AccessIdsService) {} this.isFormValid.emit(false);
this.accessIdEventEmitter.emit(this.emptyAccessId);
initializeDataSource() { this.accessIdForm.controls['accessId'].setErrors({ incorrect: true });
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);
} }
}); });
} }
getUsersAsObservable(accessId: string): Observable<any> { setAccessIdFromInput() {
return this.accessIdsService.searchForAccessId(accessId); 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 || '';
} }
typeaheadOnSelect(event): void { ngOnDestroy() {
if (event) { this.destroy$.next();
if (this.items.length > 0) { this.destroy$.complete();
this.dataSource.selected = this.items.find((item) => item.accessId.toLowerCase() === this.value.toLowerCase());
}
this.selectedItem.emit(this.dataSource.selected);
}
if (document.activeElement instanceof HTMLElement) {
document.activeElement.blur();
}
} }
} }

View File

@ -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 {}
}

View File

@ -1,3 +1,4 @@
export class AccessIdDefinition { export interface AccessId {
constructor(public accessId?: string, public name?: string) {} accessId?: string;
name?: string;
} }

View File

@ -1,7 +1,7 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { environment } from 'environments/environment'; 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 { Observable, of } from 'rxjs';
import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation'; import { WorkbasketAccessItemsRepresentation } from 'app/shared/models/workbasket-access-items-representation';
import { Sorting, WorkbasketAccessItemQuerySortParameter } from 'app/shared/models/sorting'; import { Sorting, WorkbasketAccessItemQuerySortParameter } from 'app/shared/models/sorting';
@ -20,18 +20,18 @@ export class AccessIdsService {
return this.startupService.getTaskanaRestUrl() + '/v1/access-ids'; return this.startupService.getTaskanaRestUrl() + '/v1/access-ids';
} }
searchForAccessId(accessId: string): Observable<AccessIdDefinition[]> { searchForAccessId(accessId: string): Observable<AccessId[]> {
if (!accessId || accessId.length < 3) { if (!accessId || accessId.length < 3) {
return of([]); return of([]);
} }
return this.httpClient.get<AccessIdDefinition[]>(`${this.url}?search-for=${accessId}`); return this.httpClient.get<AccessId[]>(`${this.url}?search-for=${accessId}`);
} }
getGroupsByAccessId(accessId: string): Observable<AccessIdDefinition[]> { getGroupsByAccessId(accessId: string): Observable<AccessId[]> {
if (!accessId || accessId.length < 3) { if (!accessId || accessId.length < 3) {
return of([]); return of([]);
} }
return this.httpClient.get<AccessIdDefinition[]>(`${this.url}/groups?access-id=${accessId}`); return this.httpClient.get<AccessId[]>(`${this.url}/groups?access-id=${accessId}`);
} }
getAccessItems( getAccessItems(

View File

@ -29,6 +29,7 @@ export const messageByErrorCode = {
CLASSIFICATION_WITH_ID_NOT_FOUND: 'Classification with id {classificationId} cannot be found', CLASSIFICATION_WITH_ID_NOT_FOUND: 'Classification with id {classificationId} cannot be found',
CLASSIFICATION_COPY_NOT_CREATED: 'Cannot copy a not created Classification', 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_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_WITH_KEY_NOT_FOUND: 'Workbasket with key {workbasketKey} cannot be found in domain {domain}',
WORKBASKET_ALREADY_EXISTS: WORKBASKET_ALREADY_EXISTS:

View File

@ -17,7 +17,6 @@ import { AccordionModule } from 'ngx-bootstrap/accordion';
import { SpinnerComponent } from 'app/shared/components/spinner/spinner.component'; import { SpinnerComponent } from 'app/shared/components/spinner/spinner.component';
import { MasterAndDetailComponent } from 'app/shared/components/master-and-detail/master-and-detail.component'; import { MasterAndDetailComponent } from 'app/shared/components/master-and-detail/master-and-detail.component';
import { TaskanaTreeComponent } from 'app/administration/components/tree/tree.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 { IconTypeComponent } from 'app/administration/components/type-icon/icon-type.component';
import { FieldErrorDisplayComponent } from 'app/shared/components/field-error-display/field-error-display.component'; import { FieldErrorDisplayComponent } from 'app/shared/components/field-error-display/field-error-display.component';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
@ -26,6 +25,7 @@ import { MatRadioModule } from '@angular/material/radio';
import { SortComponent } from './components/sort/sort.component'; import { SortComponent } from './components/sort/sort.component';
import { PaginationComponent } from './components/pagination/pagination.component'; import { PaginationComponent } from './components/pagination/pagination.component';
import { ProgressSpinnerComponent } from './components/progress-spinner/progress-spinner.component'; import { ProgressSpinnerComponent } from './components/progress-spinner/progress-spinner.component';
import { TypeAheadComponent } from './components/type-ahead/type-ahead.component';
/** /**
* Pipes * Pipes

View File

@ -1,11 +1,11 @@
import { AccessIdDefinition } from '../../models/access-id'; import { AccessId } from '../../models/access-id';
import { Sorting, WorkbasketAccessItemQuerySortParameter } from '../../models/sorting'; import { Sorting, WorkbasketAccessItemQuerySortParameter } from '../../models/sorting';
import { WorkbasketAccessItemQueryFilterParameter } from '../../models/workbasket-access-item-query-filter-parameter'; import { WorkbasketAccessItemQueryFilterParameter } from '../../models/workbasket-access-item-query-filter-parameter';
import { QueryPagingParameter } from '../../models/query-paging-parameter'; import { QueryPagingParameter } from '../../models/query-paging-parameter';
export class SelectAccessId { export class SelectAccessId {
static readonly type = '[Access Items Management] Select access ID'; static readonly type = '[Access Items Management] Select access ID';
constructor(public accessIdDefinition: AccessIdDefinition) {} constructor(public accessIdDefinition: AccessId) {}
} }
export class GetGroupsByAccessId { export class GetGroupsByAccessId {

View File

@ -1,10 +1,10 @@
import { Selector } from '@ngxs/store'; import { Selector } from '@ngxs/store';
import { AccessItemsManagementState, AccessItemsManagementStateModel } from './access-items-management.state'; import { AccessItemsManagementState, AccessItemsManagementStateModel } from './access-items-management.state';
import { AccessIdDefinition } from '../../models/access-id'; import { AccessId } from '../../models/access-id';
export class AccessItemsManagementSelector { export class AccessItemsManagementSelector {
@Selector([AccessItemsManagementState]) @Selector([AccessItemsManagementState])
static groups(state: AccessItemsManagementStateModel): AccessIdDefinition[] { static groups(state: AccessItemsManagementStateModel): AccessId[] {
return state.groups; return state.groups;
} }
} }

View File

@ -8,7 +8,7 @@ import {
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { AccessIdsService } from '../../services/access-ids/access-ids.service'; import { AccessIdsService } from '../../services/access-ids/access-ids.service';
import { take, tap } from 'rxjs/operators'; 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 { NotificationService } from '../../services/notifications/notification.service';
import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation'; import { WorkbasketAccessItemsRepresentation } from '../../models/workbasket-access-items-representation';
import { RequestInProgressService } from '../../services/request-in-progress/request-in-progress.service'; 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( return this.accessIdsService.getGroupsByAccessId(action.accessId).pipe(
take(1), take(1),
tap( tap(
(groups: AccessIdDefinition[]) => { (groups: AccessId[]) => {
ctx.patchState({ ctx.patchState({
groups groups
}); });
@ -106,6 +106,6 @@ export class AccessItemsManagementState implements NgxsAfterBootstrap {
export interface AccessItemsManagementStateModel { export interface AccessItemsManagementStateModel {
accessItemsResource: WorkbasketAccessItemsRepresentation; accessItemsResource: WorkbasketAccessItemsRepresentation;
selectedAccessId: AccessIdDefinition; selectedAccessId: AccessId;
groups: AccessIdDefinition[]; groups: AccessId[];
} }

View File

@ -241,6 +241,7 @@ export class WorkbasketState implements NgxsAfterBootstrap {
const date = TaskanaDate.getDate(); const date = TaskanaDate.getDate();
emptyWorkbasket.created = date; emptyWorkbasket.created = date;
emptyWorkbasket.modified = date; emptyWorkbasket.modified = date;
emptyWorkbasket.owner = '';
const accessItems = { accessItems: [], _links: {} }; const accessItems = { accessItems: [], _links: {} };
const distributionTargets = { distributionTargets: [], _links: {} }; const distributionTargets = { distributionTargets: [], _links: {} };

View File

@ -78,9 +78,14 @@
<!-- OWNER --> <!-- OWNER -->
<taskana-shared-type-ahead *ngIf="(tasksCustomisation$ | async)?.information.owner.lookupField else ownerInput" <taskana-shared-type-ahead *ngIf="(tasksCustomisation$ | async)?.information.owner.lookupField else ownerInput"
#owner="ngModel" name="task.owner" [savedAccessId]="task.owner"
[(ngModel)]="task.owner" width="100%" placeHolderMessage="Owner" placeHolderMessage="Owner"
[isRequired]="false" (selectedItem)="onSelectedOwner($event)"> [isDisabled]="task.state && task.state !== 'READY'"
[entityId]="task.taskId"
[displayError]="true"
(accessIdEventEmitter)="onSelectedOwner($event)"
(isFormValid)="isOwnerValid = $event"
matTooltip="{{task.state && task.state !== 'READY'? 'Cannot be modified since Task is not in state READY' : '' }}">
</taskana-shared-type-ahead> </taskana-shared-type-ahead>
<ng-template #ownerInput> <ng-template #ownerInput>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">

View File

@ -12,7 +12,6 @@ import {
import { Task } from 'app/workplace/models/task'; import { Task } from 'app/workplace/models/task';
import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service'; import { FormsValidatorService } from 'app/shared/services/forms-validator/forms-validator.service';
import { NgForm } from '@angular/forms'; import { NgForm } from '@angular/forms';
import { DomainService } from 'app/shared/services/domain/domain.service';
import { Select } from '@ngxs/store'; import { Select } from '@ngxs/store';
import { Observable, Subject } from 'rxjs'; import { Observable, Subject } from 'rxjs';
import { EngineConfigurationSelectors } from 'app/shared/store/engine-configuration-store/engine-configuration.selectors'; 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 { Classification } from '../../../shared/models/classification';
import { TasksCustomisation } from '../../../shared/models/customisation'; import { TasksCustomisation } from '../../../shared/models/customisation';
import { takeUntil } from 'rxjs/operators'; import { takeUntil } from 'rxjs/operators';
import { AccessIdDefinition } from '../../../shared/models/access-id'; import { AccessId } from '../../../shared/models/access-id';
@Component({ @Component({
selector: 'taskana-task-information', selector: 'taskana-task-information',
@ -45,6 +44,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy {
requestInProgress = false; requestInProgress = false;
classifications: Classification[]; classifications: Classification[];
isClassificationEmpty: boolean; isClassificationEmpty: boolean;
isOwnerValid: boolean = true;
readonly lengthError = 'You have reached the maximum length'; readonly lengthError = 'You have reached the maximum length';
inputOverflowMap = new Map<string, boolean>(); inputOverflowMap = new Map<string, boolean>();
@ -55,8 +55,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy {
constructor( constructor(
private classificationService: ClassificationsService, private classificationService: ClassificationsService,
private formsValidatorService: FormsValidatorService, private formsValidatorService: FormsValidatorService
private domainService: DomainService
) {} ) {}
ngOnInit() { ngOnInit() {
@ -102,7 +101,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy {
this.isClassificationEmpty = typeof this.task.classificationSummary === 'undefined'; this.isClassificationEmpty = typeof this.task.classificationSummary === 'undefined';
this.formsValidatorService.formSubmitAttempt = true; this.formsValidatorService.formSubmitAttempt = true;
this.formsValidatorService.validateFormInformation(this.taskForm, this.toggleValidationMap).then((value) => { this.formsValidatorService.validateFormInformation(this.taskForm, this.toggleValidationMap).then((value) => {
if (value && !this.isClassificationEmpty) { if (value && !this.isClassificationEmpty && this.isOwnerValid) {
this.formValid.emit(true); this.formValid.emit(true);
} }
}); });
@ -120,7 +119,7 @@ export class TaskInformationComponent implements OnInit, OnChanges, OnDestroy {
}); });
} }
onSelectedOwner(owner: AccessIdDefinition) { onSelectedOwner(owner: AccessId) {
if (owner?.accessId) { if (owner?.accessId) {
this.task.owner = owner.accessId; this.task.owner = owner.accessId;
} }