[ACS-6842] [ACA] Clicking "Create" button multiple times in folder creation modal sends the same folder creation request multiple times (#9554)

* ACS-6842 add disable mode to submit button

* ACS-6842 refactor ts and spec files

* ACS-6842 add setFormValues function

---------

Co-authored-by: DaryaBalvanovich <darya.balvanovich1@hyland.com>
This commit is contained in:
Darya Blavanovich
2024-04-15 19:03:11 +02:00
committed by GitHub
parent 05884e829b
commit 86d3ca7892
3 changed files with 176 additions and 165 deletions

View File

@@ -63,7 +63,7 @@
id="adf-folder-create-button"
mat-button
(click)="submit()"
[disabled]="!form.valid">
[disabled]="!form.valid || disableSubmitButton">
{{
(editing
? 'CORE.FOLDER_DIALOG.UPDATE_BUTTON.LABEL'

View File

@@ -20,7 +20,7 @@ import { MatDialogRef } from '@angular/material/dialog';
import { NodesApiService } from '../common/services/nodes-api.service';
import { FolderDialogComponent } from './folder.dialog';
import { of, throwError } from 'rxjs';
import { BehaviorSubject, throwError } from 'rxjs';
import { ContentTestingModule } from '../testing/content.testing.module';
import { By } from '@angular/platform-browser';
@@ -28,9 +28,16 @@ describe('FolderDialogComponent', () => {
let fixture: ComponentFixture<FolderDialogComponent>;
let component: FolderDialogComponent;
let nodesApi: NodesApiService;
let submitButton: HTMLButtonElement;
const dialogRef = {
close: jasmine.createSpy('close')
};
let updateNodeSpy: jasmine.Spy;
let createFolderSpy: jasmine.Spy;
const updateNode$ = new BehaviorSubject(null);
const createFolderNode$ = new BehaviorSubject(null);
beforeEach(() => {
TestBed.configureTestingModule({
@@ -41,6 +48,10 @@ describe('FolderDialogComponent', () => {
fixture = TestBed.createComponent(FolderDialogComponent);
component = fixture.componentInstance;
nodesApi = TestBed.inject(NodesApiService);
createFolderSpy = spyOn(nodesApi, 'createFolder').and.returnValue(createFolderNode$);
updateNodeSpy = spyOn(nodesApi, 'updateNode').and.returnValue(updateNode$);
submitButton = fixture.nativeElement.querySelector('#adf-folder-create-button');
});
afterEach(() => {
@@ -77,9 +88,7 @@ describe('FolderDialogComponent', () => {
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['title'].setValue('folder-title-update');
component.form.controls['description'].setValue('folder-description-update');
setFormValues();
expect(component.name).toBe('folder-name-update');
expect(component.title).toBe('folder-title-update');
@@ -87,14 +96,17 @@ describe('FolderDialogComponent', () => {
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(of(null));
updateNode$.next(null);
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['title'].setValue('folder-title-update');
component.form.controls['description'].setValue('folder-description-update');
setFormValues();
component.submit();
fixture.detectChanges();
submitButton.click();
fixture.detectChanges();
expect(component.form.valid).toBeTrue();
expect(submitButton.disabled).toBeTrue();
expect(component.disableSubmitButton).toBeTrue();
expect(nodesApi.updateNode).toHaveBeenCalledWith('node-id', {
name: 'folder-name-update',
properties: {
@@ -104,49 +116,49 @@ describe('FolderDialogComponent', () => {
});
});
it('should call dialog to close with form data when submit is successfully', () => {
const folder: any = {
data: 'folder-data'
};
spyOn(nodesApi, 'updateNode').and.returnValue(of(folder));
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should emit success output event with folder when submit is successful', async () => {
const folder: any = { data: 'folder-data' };
let expectedNode = null;
spyOn(nodesApi, 'updateNode').and.returnValue(of(folder));
component.success.subscribe((node) => {
expectedNode = node;
});
component.submit();
fixture.detectChanges();
await fixture.whenStable();
expect(expectedNode).toBe(folder);
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'updateNode');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
fixture.detectChanges();
submitButton.click();
fixture.detectChanges();
expect(component.form.valid).toBe(false);
expect(nodesApi.updateNode).not.toHaveBeenCalled();
expect(submitButton.disabled).toBeTrue();
expect(component.form.valid).toBeFalse();
expect(updateNodeSpy).not.toHaveBeenCalled();
});
describe('when submit is successfully', () => {
const folder: any = { data: 'folder-data' };
beforeAll(() => {
updateNode$.next(folder);
});
it('should call dialog to close with form data', () => {
component.submit();
expect(dialogRef.close).toHaveBeenCalledWith(folder);
});
it('should emit success output event with folder', async () => {
let expectedNode = null;
component.success.subscribe((node) => {
expectedNode = node;
});
component.submit();
fixture.detectChanges();
await fixture.whenStable();
expect(expectedNode).toBe(folder);
});
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'updateNode').and.returnValue(throwError('error'));
updateNode$.error(throwError('error'));
spyOn(component, 'handleError').and.callFake((val) => val);
component.submit();
@@ -177,49 +189,50 @@ describe('FolderDialogComponent', () => {
});
it('should update form input', () => {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['description'].setValue('folder-description-update');
setFormValues();
expect(component.name).toBe('folder-name-update');
expect(component.description).toBe('folder-description-update');
});
it('should submit updated values if form is valid', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(of(null));
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['title'].setValue('folder-title-update');
component.form.controls['description'].setValue('folder-description-update');
component.submit();
expect(nodesApi.createFolder).toHaveBeenCalledWith('parentNodeId', {
name: 'folder-name-update',
properties: {
'cm:title': 'folder-title-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:folder'
describe('when form is valid', () => {
beforeEach(() => {
createFolderNode$.next(null);
setFormValues();
});
});
it('should submit updated values if form is valid (with custom nodeType)', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(of(null));
it('should submit updated values', () => {
component.submit();
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['title'].setValue('folder-title-update');
component.form.controls['description'].setValue('folder-description-update');
component.nodeType = 'cm:sushi';
expect(component.disableSubmitButton).toBeTrue();
expect(createFolderSpy).toHaveBeenCalledWith('parentNodeId', {
name: 'folder-name-update',
properties: {
'cm:title': 'folder-title-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:folder'
});
});
component.submit();
it('should submit updated values (with custom nodeType)', () => {
component.nodeType = 'cm:sushi';
expect(nodesApi.createFolder).toHaveBeenCalledWith('parentNodeId', {
name: 'folder-name-update',
properties: {
'cm:title': 'folder-title-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:sushi'
fixture.detectChanges();
submitButton.click();
fixture.detectChanges();
expect(component.form.valid).toBeTrue();
expect(submitButton.disabled).toBeTrue();
expect(component.disableSubmitButton).toBeTrue();
expect(createFolderSpy).toHaveBeenCalledWith('parentNodeId', {
name: 'folder-name-update',
properties: {
'cm:title': 'folder-title-update',
'cm:description': 'folder-description-update'
},
nodeType: 'cm:sushi'
});
});
});
@@ -228,11 +241,9 @@ describe('FolderDialogComponent', () => {
data: 'folder-data'
};
component.form.controls['name'].setValue('name');
component.form.controls['title'].setValue('title');
component.form.controls['description'].setValue('description');
setFormValues();
spyOn(nodesApi, 'createFolder').and.returnValue(of(folder));
createFolderNode$.next(folder);
component.submit();
@@ -240,19 +251,16 @@ describe('FolderDialogComponent', () => {
});
it('should not submit if form is invalid', () => {
spyOn(nodesApi, 'createFolder');
component.form.controls['name'].setValue('');
component.form.controls['description'].setValue('');
component.submit();
expect(component.form.valid).toBe(false);
expect(nodesApi.createFolder).not.toHaveBeenCalled();
expect(submitButton.disabled).toBeTrue();
expect(component.form.valid).toBeFalse();
expect(createFolderSpy).not.toHaveBeenCalled();
});
it('should not call dialog to close if submit fails', () => {
spyOn(nodesApi, 'createFolder').and.returnValue(throwError('error'));
createFolderNode$.error(throwError('error'));
spyOn(component, 'handleError').and.callFake((val) => val);
component.form.controls['name'].setValue('name');
@@ -264,19 +272,22 @@ describe('FolderDialogComponent', () => {
expect(dialogRef.close).not.toHaveBeenCalled();
});
describe('Error events ', () => {
describe('Error events', () => {
afterEach(() => {
createFolderNode$.next(null);
});
it('should raise error for 409', (done) => {
const error = {
message: '{ "error": { "statusCode" : 409 } }'
};
createFolderNode$.error(error);
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.EXISTENT_FOLDER');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(throwError(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
@@ -287,14 +298,13 @@ describe('FolderDialogComponent', () => {
const error = {
message: '{ "error": { "statusCode" : 123 } }'
};
createFolderNode$.error(error);
component.error.subscribe((message) => {
expect(message).toBe('CORE.MESSAGES.ERRORS.GENERIC');
done();
});
spyOn(nodesApi, 'createFolder').and.returnValue(throwError(error));
component.form.controls['name'].setValue('name');
component.form.controls['description'].setValue('description');
@@ -302,4 +312,13 @@ describe('FolderDialogComponent', () => {
});
});
});
/**
* Set mock values to form
*/
function setFormValues() {
component.form.controls['name'].setValue('folder-name-update');
component.form.controls['title'].setValue('folder-title-update');
component.form.controls['description'].setValue('folder-description-update');
}
});

View File

@@ -34,11 +34,6 @@ import { forbidEndingDot, forbidOnlySpaces, forbidSpecialCharacters } from './fo
encapsulation: ViewEncapsulation.None
})
export class FolderDialogComponent implements OnInit {
form: UntypedFormGroup;
folder: Node = null;
/**
* Emitted when the edit/create folder give error for example a folder with same name already exist
*/
@@ -49,12 +44,40 @@ export class FolderDialogComponent implements OnInit {
* Emitted when the edit/create folder is successfully created/modified
*/
@Output()
success: EventEmitter<any> = new EventEmitter<Node>();
success: EventEmitter<Node> = new EventEmitter<Node>();
form: UntypedFormGroup;
folder: Node = null;
editTitle = 'CORE.FOLDER_DIALOG.EDIT_FOLDER_TITLE';
createTitle = 'CORE.FOLDER_DIALOG.CREATE_FOLDER_TITLE';
nodeType = 'cm:folder';
disableSubmitButton = false;
get editing(): boolean {
return !!this.data.folder;
}
get name(): string {
return this.getTrimmedValue(this.form.value.name);
}
get title(): string {
return this.getTrimmedValue(this.form.value.title);
}
get description(): string {
return this.getTrimmedValue(this.form.value.description);
}
private get properties(): { [key: string]: string } {
return {
'cm:title': this.title,
'cm:description': this.description
};
}
constructor(
private formBuilder: UntypedFormBuilder,
private dialog: MatDialogRef<FolderDialogComponent>,
@@ -71,10 +94,6 @@ export class FolderDialogComponent implements OnInit {
}
}
get editing(): boolean {
return !!this.data.folder;
}
ngOnInit() {
const { folder } = this.data;
let name = '';
@@ -90,88 +109,61 @@ export class FolderDialogComponent implements OnInit {
}
const validators = {
name: [
Validators.required,
forbidSpecialCharacters,
forbidEndingDot,
forbidOnlySpaces
]
name: [Validators.required, forbidSpecialCharacters, forbidEndingDot, forbidOnlySpaces]
};
this.form = this.formBuilder.group({
name: [ name, validators.name ],
title: [ title ],
description: [ description ]
name: [name, validators.name],
title: [title],
description: [description]
});
}
get name(): string {
const { name } = this.form.value;
return (name || '').trim();
}
get title(): string {
const { title } = this.form.value;
return (title || '').trim();
}
get description(): string {
const { description } = this.form.value;
return (description || '').trim();
}
private get properties(): any {
const { title, description } = this;
return {
'cm:title': title,
'cm:description': description
};
}
private create(): Observable<Node> {
const { name, properties, nodeType, nodesApi, data: { parentNodeId} } = this;
return nodesApi.createFolder(parentNodeId, { name, properties, nodeType });
}
private edit(): Observable<Node> {
const { name, properties, nodesApi, data: { folder: { id: nodeId }} } = this;
return nodesApi.updateNode(nodeId, { name, properties });
}
submit() {
const { form, dialog, editing } = this;
this.disableSubmitButton = true;
if (!form.valid) {
return;
}
(editing ? this.edit() : this.create())
.subscribe(
(folder: Node) => {
this.success.emit(folder);
dialog.close(folder);
},
(error) => this.handleError(error)
);
(this.editing ? this.edit() : this.create()).subscribe(
(folder: Node) => {
this.success.emit(folder);
this.dialog.close(folder);
},
(error) => this.handleError(error)
);
}
handleError(error: any): any {
let errorMessage = 'CORE.MESSAGES.ERRORS.GENERIC';
try {
const { error: { statusCode } } = JSON.parse(error.message);
const {
error: { statusCode }
} = JSON.parse(error.message);
if (statusCode === 409) {
errorMessage = 'CORE.MESSAGES.ERRORS.EXISTENT_FOLDER';
}
} catch (err) { /* Do nothing, keep the original message */ }
} catch (err) {
/* Do nothing, keep the original message */
}
this.error.emit(this.translation.instant(errorMessage));
return error;
}
private create(): Observable<Node> {
const parentNodeId = this.data.parentNodeId;
return this.nodesApi.createFolder(parentNodeId, { name: this.name, properties: this.properties, nodeType: this.nodeType });
}
private edit(): Observable<Node> {
const nodeId = this.data.folder.id;
return this.nodesApi.updateNode(nodeId, { name: this.name, properties: this.properties });
}
private getTrimmedValue(value: string): string {
return (value || '').trim();
}
}