From 86d3ca78923076a9b26c3241ada9324c67a47f15 Mon Sep 17 00:00:00 2001 From: Darya Blavanovich <166367848+DaryaBalvanovich@users.noreply.github.com> Date: Mon, 15 Apr 2024 19:03:11 +0200 Subject: [PATCH] [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 --- .../src/lib/dialogs/folder.dialog.html | 2 +- .../src/lib/dialogs/folder.dialog.spec.ts | 205 ++++++++++-------- .../src/lib/dialogs/folder.dialog.ts | 134 ++++++------ 3 files changed, 176 insertions(+), 165 deletions(-) diff --git a/lib/content-services/src/lib/dialogs/folder.dialog.html b/lib/content-services/src/lib/dialogs/folder.dialog.html index f1bcb70cf8..d64b61029a 100644 --- a/lib/content-services/src/lib/dialogs/folder.dialog.html +++ b/lib/content-services/src/lib/dialogs/folder.dialog.html @@ -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' diff --git a/lib/content-services/src/lib/dialogs/folder.dialog.spec.ts b/lib/content-services/src/lib/dialogs/folder.dialog.spec.ts index a9b577a195..1256de0820 100644 --- a/lib/content-services/src/lib/dialogs/folder.dialog.spec.ts +++ b/lib/content-services/src/lib/dialogs/folder.dialog.spec.ts @@ -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; 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'); + } }); diff --git a/lib/content-services/src/lib/dialogs/folder.dialog.ts b/lib/content-services/src/lib/dialogs/folder.dialog.ts index c0791714a3..1dc726eefd 100644 --- a/lib/content-services/src/lib/dialogs/folder.dialog.ts +++ b/lib/content-services/src/lib/dialogs/folder.dialog.ts @@ -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 = new EventEmitter(); + success: EventEmitter = new EventEmitter(); + + 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, @@ -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 { - const { name, properties, nodeType, nodesApi, data: { parentNodeId} } = this; - return nodesApi.createFolder(parentNodeId, { name, properties, nodeType }); - } - - private edit(): Observable { - 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 { + const parentNodeId = this.data.parentNodeId; + + return this.nodesApi.createFolder(parentNodeId, { name: this.name, properties: this.properties, nodeType: this.nodeType }); + } + + private edit(): Observable { + 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(); + } }