TSK-1630,TSK-1670: Enabled owner validiation and editing AccessItem name
This commit is contained in:
parent
0fcc08ddd2
commit
f18a8dc932
|
@ -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": {
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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}} {{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}} {{accessId.name}}</small>
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}));
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export class AccessIdDefinition {
|
||||
constructor(public accessId?: string, public name?: string) {}
|
||||
export interface AccessId {
|
||||
accessId?: string;
|
||||
name?: string;
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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: {} };
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue