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": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"browserTarget": "taskana-web:build"
"browserTarget": "taskana-web:build",
"sourceMap": {
"scripts": true
}
},
"configurations": {
"production": {

View File

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

View File

@ -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();

View File

@ -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<WorkbasketAccessItemQuerySortParameter, string> = WORKBASKET_ACCESS_ITEM_SORT_PARAMETER_NAMING;
sortModel: Sorting<WorkbasketAccessItemQuerySortParameter> = {
@ -50,7 +49,7 @@ export class AccessItemsManagementComponent implements OnInit {
@Select(EngineConfigurationSelectors.accessItemsCustomisation)
accessItemsCustomization$: Observable<AccessItemsCustomisation>;
@Select(AccessItemsManagementSelector.groups) groups$: Observable<AccessIdDefinition[]>;
@Select(AccessItemsManagementSelector.groups) groups$: Observable<AccessId[]>;
customFields$: Observable<CustomField[]>;
destroy$ = new Subject<void>();
@ -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) {

View File

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

View File

@ -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;

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 { 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);
}

View File

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

View File

@ -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<string, string>;
toggleValidationMap = new Map<string, boolean>();
lookupField = false;
isOwnerValid: boolean = true;
readonly lengthError = 'You have reached the maximum length for this field';
inputOverflowMap = new Map<string, boolean>();
@ -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) {

View File

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

View File

@ -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%;
}

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 { 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<AccessIdsService> = {
getAccessItems: jest.fn().mockReturnValue(EMPTY),
searchForAccessId: jest.fn().mockReturnValue(EMPTY)
const accessIdService: Partial<AccessIdsService> = {
searchForAccessId: jest.fn().mockReturnValue(of([{ accessId: 'user-g-1', name: 'Gerda' }]))
};
describe('TypeAheadComponent', () => {
let component: TypeAheadComponent;
describe('TypeAheadComponent with AccessId input', () => {
let fixture: ComponentFixture<TypeAheadComponent>;
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);
}));
});

View File

@ -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<AccessId>();
@Output() isFormValid = new EventEmitter<boolean>();
@Input()
validationValue;
@Select(WorkbasketSelectors.buttonAction)
buttonAction$: Observable<ButtonAction>;
@Input()
displayError;
name: string = '';
lastSavedAccessId: string = '';
filteredAccessIds: AccessId[] = [];
destroy$ = new Subject<void>();
accessIdForm = new FormGroup({
accessId: new FormControl('')
});
emptyAccessId: AccessId = { accessId: '', name: '' };
@Input()
width;
constructor(private accessIdService: AccessIdsService) {}
@Input()
disable;
@Input()
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;
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<any> {
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();
}
}

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 {
constructor(public accessId?: string, public name?: string) {}
export interface AccessId {
accessId?: string;
name?: string;
}

View File

@ -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<AccessIdDefinition[]> {
searchForAccessId(accessId: string): Observable<AccessId[]> {
if (!accessId || accessId.length < 3) {
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) {
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(

View File

@ -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:

View File

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

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

View File

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

View File

@ -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[];
}

View File

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

View File

@ -77,10 +77,15 @@
<!-- OWNER -->
<taskana-shared-type-ahead *ngIf="(tasksCustomisation$ |async)?.information.owner.lookupField else ownerInput"
#owner="ngModel" name="task.owner"
[(ngModel)]="task.owner" width="100%" placeHolderMessage="Owner"
[isRequired]="false" (selectedItem)="onSelectedOwner($event)">
<taskana-shared-type-ahead *ngIf="(tasksCustomisation$ | async)?.information.owner.lookupField else ownerInput"
[savedAccessId]="task.owner"
placeHolderMessage="Owner"
[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>
<ng-template #ownerInput>
<mat-form-field appearance="outline">

View File

@ -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<string, boolean>();
@ -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;
}