TSK-1683: Saving Workbaskets and Classifications custom fields corrected

This commit is contained in:
Sofie Hofmann 2021-08-03 17:10:31 +02:00
parent be4caceb95
commit 0d41d615d9
7 changed files with 200 additions and 145 deletions

View File

@ -209,9 +209,9 @@
<!-- CUSTOM FIELDS -->
<h6 class="detailed-fields__subheading"> Custom Fields </h6>
<mat-divider class="detailed-fields__horizontal-line"> </mat-divider>
<div class="detailed-fields__custom-fields">
<div *ngFor="let customField of (customFields$ | async), let i = index" class="detailed-fields__input-custom-field">
<ng-container *ngFor="let customField of (customFields$ | async), let i = index">
<div *ngIf="customField.visible" class="detailed-fields__input-custom-field">
<mat-form-field appearance="outline" style="width: 100%">
<mat-label>{{customField.field}}</mat-label>
<label for="classification-custom-{{i + 1}}"></label>
@ -222,7 +222,8 @@
(input)="validateInputOverflow(custom, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(custom.name)" class="error">{{lengthError}}</div>
</div>
</div>
</ng-container>
</div>
</div>
</ng-form>

View File

@ -1,10 +1,10 @@
import { Component, DebugElement, Input } from '@angular/core';
import { ClassificationsService } from '../../../shared/services/classifications/classifications.service';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { ClassificationCategoriesService } from '../../../shared/services/classification-categories/classification-categories.service';
import { DomainService } from '../../../shared/services/domain/domain.service';
import { ImportExportService } from '../../services/import-export.service';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { ClassificationState } from '../../../shared/store/classification-store/classification.state';
import { EngineConfigurationState } from '../../../shared/store/engine-configuration-store/engine-configuration.state';
@ -56,26 +56,28 @@ class TextareaStub {
}
const classificationServiceSpy: Partial<ClassificationsService> = {
getClassification: jest.fn().mockReturnValue(of()),
getClassifications: jest.fn().mockReturnValue(of()),
postClassification: jest.fn().mockReturnValue(of()),
putClassification: jest.fn().mockReturnValue(of()),
deleteClassification: jest.fn().mockReturnValue(of())
getClassification: jest.fn().mockReturnValue(EMPTY),
getClassifications: jest.fn().mockReturnValue(EMPTY),
postClassification: jest.fn().mockReturnValue(EMPTY),
putClassification: jest.fn().mockReturnValue(EMPTY),
deleteClassification: jest.fn().mockReturnValue(EMPTY)
};
const classificationCategoriesServiceSpy: Partial<ClassificationCategoriesService> = {
getCustomisation: jest.fn().mockReturnValue(of())
getCustomisation: jest.fn().mockReturnValue(EMPTY)
};
const domainServiceSpy: Partial<DomainService> = {
getSelectedDomainValue: jest.fn().mockReturnValue(of('A')),
getSelectedDomain: jest.fn().mockReturnValue(of())
getSelectedDomain: jest.fn().mockReturnValue(EMPTY)
};
const getImportingFinishedFn = jest.fn().mockReturnValue(of(true));
const importExportServiceSpy: Partial<ImportExportService> = {
getImportingFinished: getImportingFinishedFn
getImportingFinished: jest.fn().mockReturnValue(of(true))
};
const requestInProgressServiceSpy: Partial<RequestInProgressService> = {
setRequestInProgress: jest.fn().mockReturnValue(of()),
setRequestInProgress: jest.fn(),
getRequestInProgress: jest.fn().mockReturnValue(of(false))
};
@ -90,9 +92,9 @@ const formsValidatorServiceSpy: Partial<FormsValidatorService> = {
};
const notificationServiceSpy: Partial<NotificationService> = {
showError: jest.fn().mockReturnValue(of()),
showSuccess: jest.fn().mockReturnValue(of()),
showDialog: jest.fn().mockReturnValue(of())
showError: jest.fn(),
showSuccess: jest.fn(),
showDialog: jest.fn()
};
describe('ClassificationDetailsComponent', () => {
@ -102,47 +104,49 @@ describe('ClassificationDetailsComponent', () => {
let store: Store;
let actions$: Observable<any>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
FormsModule,
MatIconModule,
MatToolbarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatOptionModule,
MatSelectModule,
MatProgressBarModule,
MatMenuModule,
MatTooltipModule,
BrowserAnimationsModule
],
declarations: [ClassificationDetailsComponent, InputStub, FieldErrorDisplayStub, SvgIconStub, TextareaStub],
providers: [
{ provide: ClassificationsService, useValue: classificationServiceSpy },
{ provide: ClassificationCategoriesService, useValue: classificationCategoriesServiceSpy },
{ provide: DomainService, useValue: domainServiceSpy },
{ provide: ImportExportService, useValue: importExportServiceSpy },
{ provide: RequestInProgressService, useValue: requestInProgressServiceSpy },
{ provide: FormsValidatorService, useValue: formsValidatorServiceSpy },
{ provide: NotificationService, useValue: notificationServiceSpy }
]
}).compileComponents();
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
NgxsModule.forRoot([ClassificationState, EngineConfigurationState]),
FormsModule,
MatIconModule,
MatToolbarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatOptionModule,
MatSelectModule,
MatProgressBarModule,
MatMenuModule,
MatTooltipModule,
BrowserAnimationsModule
],
declarations: [ClassificationDetailsComponent, InputStub, FieldErrorDisplayStub, SvgIconStub, TextareaStub],
providers: [
{ provide: ClassificationsService, useValue: classificationServiceSpy },
{ provide: ClassificationCategoriesService, useValue: classificationCategoriesServiceSpy },
{ provide: DomainService, useValue: domainServiceSpy },
{ provide: ImportExportService, useValue: importExportServiceSpy },
{ provide: RequestInProgressService, useValue: requestInProgressServiceSpy },
{ provide: FormsValidatorService, useValue: formsValidatorServiceSpy },
{ provide: NotificationService, useValue: notificationServiceSpy }
]
}).compileComponents();
fixture = TestBed.createComponent(ClassificationDetailsComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
classification: classificationStateMock,
engineConfiguration: engineConfigurationMock
});
fixture.detectChanges();
}));
fixture = TestBed.createComponent(ClassificationDetailsComponent);
debugElement = fixture.debugElement;
component = fixture.debugElement.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
classification: classificationStateMock,
engineConfiguration: engineConfigurationMock
});
fixture.detectChanges();
})
);
it('should create component', () => {
expect(component).toBeTruthy();
@ -380,4 +384,26 @@ describe('ClassificationDetailsComponent', () => {
button.click();
expect(component.classification.isValidInDomain).toBe(false);
});
it('should not show custom fields with attribute visible = false', () => {
const inputCustoms = debugElement.queryAll(By.css('.detailed-fields__input-custom-field'));
expect(inputCustoms).toHaveLength(7);
});
it('should save custom field input at position 4 when custom field at position 3 is not visible', fakeAsync(() => {
const newValue = 'New value';
let inputCustom3 = debugElement.nativeElement.querySelector('#classification-custom-3');
let inputCustom4 = debugElement.nativeElement.querySelector('#classification-custom-4');
expect(inputCustom3).toBeFalsy();
expect(inputCustom4).toBeTruthy();
inputCustom4.value = newValue;
inputCustom4.dispatchEvent(new Event('input'));
tick();
fixture.detectChanges();
expect(component.classification['custom3']).toBe(undefined);
expect(component.classification['custom4']).toBe(newValue);
}));
});

View File

@ -68,8 +68,7 @@ export class ClassificationDetailsComponent implements OnInit, OnDestroy {
ngOnInit() {
this.customFields$ = this.store.select(EngineConfigurationSelectors.classificationsCustomisation).pipe(
map((customisation) => customisation.information),
getCustomFields(customFieldCount),
map((customisationFields) => customisationFields.filter((customisation) => customisation.visible))
getCustomFields(customFieldCount)
);
this.selectedClassification$.pipe(takeUntil(this.destroy$)).subscribe((classification) => {

View File

@ -152,7 +152,8 @@
<mat-divider class="horizontal-line"> </mat-divider>
<div class="custom-fields">
<div *ngFor="let customField of customFields$ | async; let index = index" class="custom-fields__input">
<ng-container *ngFor="let customField of customFields$ | async; let index = index">
<div *ngIf="customField.visible" class="custom-fields__input">
<mat-form-field appearance="outline" class="custom-fields__form-field">
<mat-label>{{customField.field}}</mat-label>
<label for='wb-custom-{{index+1}}'></label>
@ -162,7 +163,8 @@
(input)="validateInputOverflow(custom, 255)">
</mat-form-field>
<div *ngIf="inputOverflowMap.get(custom.name)" class="error">{{lengthError}}</div>
</div>
</div>
</ng-container>
</div>
</div>

View File

@ -1,8 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
import { WorkbasketInformationComponent } from './workbasket-information.component';
import { Component, DebugElement, Input } from '@angular/core';
import { Actions, NgxsModule, ofActionDispatched, Store } from '@ngxs/store';
import { Observable, of } from 'rxjs';
import { EMPTY, Observable, of } from 'rxjs';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { WorkbasketType } from '../../../shared/models/workbasket-type';
import { MapValuesPipe } from '../../../shared/pipes/map-values.pipe';
@ -39,6 +39,7 @@ import { MatSelectModule } from '@angular/material/select';
import { MatInputModule } from '@angular/material/input';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatTooltipModule } from '@angular/material/tooltip';
import { By } from '@angular/platform-browser';
@Component({ selector: 'taskana-shared-field-error-display', template: '' })
class FieldErrorDisplayStub {
@ -53,21 +54,19 @@ class IconTypeStub {
@Input() text: string;
}
const triggerWorkbasketSavedFn = jest.fn().mockReturnValue(true);
const workbasketServiceMock: Partial<WorkbasketService> = {
triggerWorkBasketSaved: triggerWorkbasketSavedFn,
triggerWorkBasketSaved: jest.fn(),
updateWorkbasket: jest.fn().mockReturnValue(of(true)),
markWorkbasketForDeletion: jest.fn().mockReturnValue(of(true)),
createWorkbasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock })),
getWorkBasket: jest.fn().mockReturnValue(of({ ...selectedWorkbasketMock })),
getWorkBasketAccessItems: jest.fn().mockReturnValue(of()),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(of())
getWorkBasketAccessItems: jest.fn().mockReturnValue(EMPTY),
getWorkBasketsDistributionTargets: jest.fn().mockReturnValue(EMPTY)
};
const isFieldValidFn = jest.fn().mockReturnValue(true);
const validateFormInformationFn = jest.fn().mockImplementation((): Promise<any> => Promise.resolve(true));
const formValidatorServiceMock: Partial<FormsValidatorService> = {
isFieldValid: isFieldValidFn,
isFieldValid: jest.fn().mockReturnValue(true),
validateInputOverflow: jest.fn(),
validateFormInformation: validateFormInformationFn,
get inputOverflowObservable(): Observable<Map<string, boolean>> {
@ -75,10 +74,9 @@ const formValidatorServiceMock: Partial<FormsValidatorService> = {
}
};
const showDialogFn = jest.fn().mockReturnValue(true);
const notificationServiceMock: Partial<NotificationService> = {
showDialog: showDialogFn,
showSuccess: showDialogFn
showDialog: jest.fn(),
showSuccess: jest.fn()
};
describe('WorkbasketInformationComponent', () => {
@ -88,61 +86,63 @@ describe('WorkbasketInformationComponent', () => {
let store: Store;
let actions$: Observable<any>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
HttpClientTestingModule,
MatDialogModule,
NgxsModule.forRoot([EngineConfigurationState, WorkbasketState]),
TypeaheadModule.forRoot(),
ReactiveFormsModule,
RouterTestingModule.withRoutes([]),
BrowserAnimationsModule,
MatProgressBarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatAutocompleteModule,
MatTooltipModule
],
declarations: [
WorkbasketInformationComponent,
FieldErrorDisplayStub,
IconTypeStub,
TypeAheadComponent,
MapValuesPipe,
RemoveNoneTypePipe
],
providers: [
{ provide: WorkbasketService, useValue: workbasketServiceMock },
{ provide: FormsValidatorService, useValue: formValidatorServiceMock },
{ provide: NotificationService, useValue: notificationServiceMock },
RequestInProgressService,
DomainService,
SelectedRouteService,
ClassificationCategoriesService,
StartupService,
TaskanaEngineService,
WindowRefService
]
}).compileComponents();
beforeEach(
waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
HttpClientTestingModule,
MatDialogModule,
NgxsModule.forRoot([EngineConfigurationState, WorkbasketState]),
TypeaheadModule.forRoot(),
ReactiveFormsModule,
RouterTestingModule.withRoutes([]),
BrowserAnimationsModule,
MatProgressBarModule,
MatDividerModule,
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatAutocompleteModule,
MatTooltipModule
],
declarations: [
WorkbasketInformationComponent,
FieldErrorDisplayStub,
IconTypeStub,
TypeAheadComponent,
MapValuesPipe,
RemoveNoneTypePipe
],
providers: [
{ provide: WorkbasketService, useValue: workbasketServiceMock },
{ provide: FormsValidatorService, useValue: formValidatorServiceMock },
{ provide: NotificationService, useValue: notificationServiceMock },
RequestInProgressService,
DomainService,
SelectedRouteService,
ClassificationCategoriesService,
StartupService,
TaskanaEngineService,
WindowRefService
]
}).compileComponents();
fixture = TestBed.createComponent(WorkbasketInformationComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
engineConfiguration: engineConfigurationMock,
workbasket: workbasketReadStateMock
});
component.workbasket = selectedWorkbasketMock;
fixture = TestBed.createComponent(WorkbasketInformationComponent);
debugElement = fixture.debugElement;
component = fixture.componentInstance;
store = TestBed.inject(Store);
actions$ = TestBed.inject(Actions);
store.reset({
...store.snapshot(),
engineConfiguration: engineConfigurationMock,
workbasket: workbasketReadStateMock
});
component.workbasket = selectedWorkbasketMock;
fixture.detectChanges();
}));
fixture.detectChanges();
})
);
it('should create component', () => {
expect(component).toBeTruthy();
@ -174,23 +174,29 @@ describe('WorkbasketInformationComponent', () => {
expect(component.workbasket).toMatchObject(component.workbasketClone);
});
it('should save workbasket when workbasketId there', async(() => {
component.workbasket = { ...selectedWorkbasketMock };
component.workbasket.workbasketId = '1';
component.action = ACTION.COPY;
let actionDispatched = false;
actions$.pipe(ofActionDispatched(UpdateWorkbasket)).subscribe(() => (actionDispatched = true));
component.onSave();
expect(actionDispatched).toBe(true);
expect(component.workbasketClone).toMatchObject(component.workbasket);
}));
it(
'should save workbasket when workbasketId there',
waitForAsync(() => {
component.workbasket = { ...selectedWorkbasketMock };
component.workbasket.workbasketId = '1';
component.action = ACTION.COPY;
let actionDispatched = false;
actions$.pipe(ofActionDispatched(UpdateWorkbasket)).subscribe(() => (actionDispatched = true));
component.onSave();
expect(actionDispatched).toBe(true);
expect(component.workbasketClone).toMatchObject(component.workbasket);
})
);
it('should dispatch MarkWorkbasketforDeletion action when onRemoveConfirmed is called', async(() => {
let actionDispatched = false;
actions$.pipe(ofActionDispatched(MarkWorkbasketForDeletion)).subscribe(() => (actionDispatched = true));
component.onRemoveConfirmed();
expect(actionDispatched).toBe(true);
}));
it(
'should dispatch MarkWorkbasketforDeletion action when onRemoveConfirmed is called',
waitForAsync(() => {
let actionDispatched = false;
actions$.pipe(ofActionDispatched(MarkWorkbasketForDeletion)).subscribe(() => (actionDispatched = true));
component.onRemoveConfirmed();
expect(actionDispatched).toBe(true);
})
);
it('should create new workbasket when workbasketId is undefined', () => {
component.workbasket.workbasketId = undefined;
@ -198,4 +204,26 @@ describe('WorkbasketInformationComponent', () => {
component.onSave();
expect(postNewWorkbasketSpy).toHaveBeenCalled();
});
it('should not show custom fields with attribute visible = false', () => {
const inputCustoms = debugElement.queryAll(By.css('.custom-fields__input'));
expect(inputCustoms).toHaveLength(3);
});
it('should save custom field input at position 4 when custom field at position 3 is not visible', fakeAsync(() => {
const newValue = 'New value';
let inputCustom3 = debugElement.nativeElement.querySelector('#wb-custom-3');
let inputCustom4 = debugElement.nativeElement.querySelector('#wb-custom-4');
expect(inputCustom3).toBeFalsy();
expect(inputCustom4).toBeTruthy();
inputCustom4.value = newValue;
inputCustom4.dispatchEvent(new Event('input'));
tick();
fixture.detectChanges();
expect(component.workbasket['custom3']).toBe('');
expect(component.workbasket['custom4']).toBe(newValue);
}));
});

View File

@ -77,8 +77,7 @@ export class WorkbasketInformationComponent implements OnInit, OnChanges, OnDest
this.customFields$ = this.workbasketsCustomisation$.pipe(
map((customisation) => customisation.information),
getCustomFields(customFieldCount),
map((customFields) => customFields.filter((customisation) => customisation.visible))
getCustomFields(customFieldCount)
);
this.workbasketsCustomisation$.pipe(takeUntil(this.destroy$)).subscribe((workbasketsCustomization) => {
if (workbasketsCustomization.information.owner) {

View File

@ -91,7 +91,7 @@ export class FormsValidatorService {
return result;
}
isFieldValid(ngForm: NgForm, field: string) {
isFieldValid(ngForm: NgForm, field: string): boolean {
if (!ngForm || !ngForm.form.controls || !ngForm.form.controls[field]) {
return false;
}