From 63ae858582ec48487e28acf59a4dc315bd423303 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Stephan=20M=C3=BCller?= Date: Mon, 16 Apr 2018 13:44:38 +0200 Subject: [PATCH] mgr/dashboard: Change deletion link to modal only MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Due to CSS problems the link solution wasn't the best way, now it will represent only the modal content. The downside of this solution is that it put's the burden on the developer to use it the right way and import a view things to get it working. But on the upside CSS styles will work as expected. The unit test example was updated accordingly this way it should be easy to understand how it can be implemented the right way. Signed-off-by: Stephan Müller --- .../shared/components/components.module.ts | 9 +- .../deletion-link.component.html | 88 ----- .../deletion-link.component.spec.ts | 210 ----------- .../deletion-link/deletion-link.component.ts | 87 ----- .../deletion-modal.component.html | 73 ++++ .../deletion-modal.component.scss} | 0 .../deletion-modal.component.spec.ts | 342 ++++++++++++++++++ .../deletion-modal.component.ts | 100 +++++ 8 files changed, 521 insertions(+), 388 deletions(-) delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts delete mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html rename src/pybind/mgr/dashboard/frontend/src/app/shared/components/{deletion-link/deletion-link.component.scss => deletion-modal/deletion-modal.component.scss} (100%) create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts create mode 100644 src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts index cc3dd5bbfebe3..63840d6f2bb9e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts @@ -9,7 +9,7 @@ import { PipesModule } from '../pipes/pipes.module'; import { DeleteConfirmationComponent } from './delete-confirmation-modal/delete-confirmation-modal.component'; -import { DeletionLinkComponent } from './deletion-link/deletion-link.component'; +import { DeletionModalComponent } from './deletion-modal/deletion-modal.component'; import { HelperComponent } from './helper/helper.component'; import { ModalComponent } from './modal/modal.component'; import { SparklineComponent } from './sparkline/sparkline.component'; @@ -38,7 +38,10 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; UsageBarComponent, DeleteConfirmationComponent, ModalComponent, - DeletionLinkComponent + DeletionModalComponent + ], + entryComponents: [ + DeletionModalComponent ], providers: [], exports: [ @@ -52,7 +55,7 @@ import { ViewCacheComponent } from './view-cache/view-cache.component'; entryComponents: [ DeleteConfirmationComponent, ModalComponent, - DeletionLinkComponent + DeletionModalComponent ] }) export class ComponentsModule { } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html deleted file mode 100644 index 9910fe9d7b3c6..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.html +++ /dev/null @@ -1,88 +0,0 @@ - - - - - - - - - - - - - - - - - - -
- - -
-
- - - - Delete - - {{ metaType }} - - - - - - diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts deleted file mode 100644 index 9ee3f608ba0fb..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.spec.ts +++ /dev/null @@ -1,210 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { FormGroupDirective, ReactiveFormsModule } from '@angular/forms'; - -import { ModalModule } from 'ngx-bootstrap'; -import { Observable } from 'rxjs/Observable'; -import { Subscriber } from 'rxjs/Subscriber'; - -import { ModalComponent } from '../modal/modal.component'; -import { SubmitButtonComponent } from '../submit-button/submit-button.component'; -import { DeletionLinkComponent } from './deletion-link.component'; - -@Component({ - template: ` - - The spinner is handled by the controller if you have use the modal as ViewChild in order to - use it's functions to stop the spinner or close the dialog. - - - The spinner is handled by the modal if your given deletion function returns a Observable. - - ` -}) -class MockComponent { - @ViewChild('ctrlDeleteButton') ctrlDeleteButton: DeletionLinkComponent; - @ViewChild('modalDeleteButton') modalDeleteButton: DeletionLinkComponent; - someData = [1, 2, 3, 4, 5]; - finished: number[]; - - finish() { - this.finished = [6, 7, 8, 9]; - } - - fakeDelete() { - return (): Observable => { - return new Observable((observer: Subscriber) => { - Observable.timer(100).subscribe(() => { - observer.next(this.finish()); - observer.complete(); - }); - }); - }; - } - - fakeDeleteController() { - Observable.timer(100).subscribe(() => { - this.finish(); - this.ctrlDeleteButton.hideModal(); - }); - } -} - -describe('DeletionLinkComponent', () => { - let mockComponent: MockComponent; - let component: DeletionLinkComponent; - let fixture: ComponentFixture; - - beforeEach(async(() => { - TestBed.configureTestingModule({ - declarations: [ MockComponent, DeletionLinkComponent, ModalComponent, - SubmitButtonComponent], - imports: [ModalModule.forRoot(), ReactiveFormsModule], - }) - .compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(MockComponent); - mockComponent = fixture.componentInstance; - component = mockComponent.ctrlDeleteButton; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - describe('component functions', () => { - - const mockShowModal = () => { - component.showModal(null); - }; - - const changeValue = (value) => { - component.confirmation.setValue(value); - component.confirmation.markAsDirty(); - component.confirmation.updateValueAndValidity(); - }; - - beforeEach(() => { - spyOn(component.modalService, 'show').and.returnValue({ - hide: () => true - }); - }); - - it('should test showModal', () => { - changeValue('something'); - expect(mockShowModal).toBeTruthy(); - expect(component.confirmation.value).toBe('something'); - expect(component.modalService.show).not.toHaveBeenCalled(); - mockShowModal(); - expect(component.modalService.show).toHaveBeenCalled(); - expect(component.confirmation.value).toBe(null); - expect(component.confirmation.pristine).toBe(true); - }); - - it('should test hideModal', () => { - expect(component.bsModalRef).not.toBeTruthy(); - mockShowModal(); - expect(component.bsModalRef).toBeTruthy(); - expect(component.hideModal).toBeTruthy(); - spyOn(component.bsModalRef, 'hide').and.stub(); - expect(component.bsModalRef.hide).not.toHaveBeenCalled(); - component.hideModal(); - expect(component.bsModalRef.hide).toHaveBeenCalled(); - }); - - describe('invalid control', () => { - - const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => { - expect(component.invalidControl(submitted, error)).toBe(expected); - }; - - beforeEach(() => { - component.deletionForm.reset(); - }); - - it('should test empty values', () => { - expect(component.invalidControl).toBeTruthy(); - component.deletionForm.reset(); - testInvalidControl(false, undefined, false); - testInvalidControl(true, 'required', true); - component.deletionForm.reset(); - changeValue('let-me-pass'); - changeValue(''); - testInvalidControl(true, 'required', true); - }); - - it('should test pattern', () => { - changeValue('let-me-pass'); - testInvalidControl(false, 'pattern', true); - changeValue('ctrl-test'); - testInvalidControl(false, undefined, false); - testInvalidControl(true, undefined, false); - }); - }); - - describe('deletion call', () => { - beforeEach(() => { - spyOn(component.toggleDeletion, 'emit'); - spyOn(component, 'stopLoadingSpinner'); - spyOn(component, 'hideModal').and.stub(); - }); - - describe('Controller driven', () => { - beforeEach(() => { - mockShowModal(); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - }); - - it('should delete without doing anything but call emit', () => { - component.deletionCall(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(component.toggleDeletion.emit).toHaveBeenCalled(); - }); - - it('should test fake deletion that closes modal', fakeAsync(() => { - mockComponent.fakeDeleteController(); - expect(component.hideModal).not.toHaveBeenCalled(); - expect(mockComponent.finished).toBe(undefined); - tick(2000); - expect(component.hideModal).toHaveBeenCalled(); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - })); - }); - - describe('Modal driven', () => { - it('should delete and close modal', fakeAsync(() => { - component = mockComponent.modalDeleteButton; - mockShowModal(); - spyOn(component.toggleDeletion, 'emit'); - spyOn(component, 'stopLoadingSpinner'); - spyOn(component, 'hideModal').and.stub(); - spyOn(mockComponent, 'fakeDelete'); - - component.deletionCall(); - expect(mockComponent.finished).toBe(undefined); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.hideModal).not.toHaveBeenCalled(); - - tick(2000); - expect(component.toggleDeletion.emit).not.toHaveBeenCalled(); - expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); - expect(component.hideModal).toHaveBeenCalled(); - expect(mockComponent.finished).toEqual([6, 7, 8, 9]); - })); - }); - }); - }); - -}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts deleted file mode 100644 index cd79da24fc0d4..0000000000000 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { - Component, EventEmitter, Input, OnInit, Output, TemplateRef, ViewChild -} from '@angular/core'; -import { FormControl, FormGroup, FormGroupDirective, Validators } from '@angular/forms'; - -import { BsModalRef, BsModalService } from 'ngx-bootstrap'; -import { Observable } from 'rxjs/Observable'; - -import { SubmitButtonComponent } from '../submit-button/submit-button.component'; - -@Component({ - selector: 'cd-deletion-link', - templateUrl: './deletion-link.component.html', - styleUrls: ['./deletion-link.component.scss'] -}) -export class DeletionLinkComponent implements OnInit { - @ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent; - @Input() metaType: string; - @Input() pattern = 'yes'; - @Input() deletionObserver: () => Observable; - @Output() toggleDeletion = new EventEmitter(); - bsModalRef: BsModalRef; - deletionForm: FormGroup; - confirmation: FormControl; - delete: Function; - - constructor(public modalService: BsModalService) {} - - ngOnInit() { - this.confirmation = new FormControl('', { - validators: [ - Validators.required, - Validators.pattern(this.pattern) - ], - updateOn: 'blur' - }); - this.deletionForm = new FormGroup({ - confirmation: this.confirmation - }); - } - - showModal(template: TemplateRef) { - this.deletionForm.reset(); - this.bsModalRef = this.modalService.show(template); - this.delete = () => { - this.submitButton.submit(); - }; - } - - invalidControl(submitted: boolean, error?: string): boolean { - const control = this.confirmation; - return !!( - (submitted || control.dirty) && - control.invalid && - (error ? control.errors[error] : true) - ); - } - - updateConfirmation($e) { - if ($e.key !== 'Enter') { - return; - } - this.confirmation.setValue($e.target.value); - this.confirmation.markAsDirty(); - this.confirmation.updateValueAndValidity(); - } - - deletionCall() { - if (this.deletionObserver) { - this.deletionObserver().subscribe( - undefined, - () => this.stopLoadingSpinner(), - () => this.hideModal() - ); - } else { - this.toggleDeletion.emit(); - } - } - - hideModal() { - this.bsModalRef.hide(); - } - - stopLoadingSpinner() { - this.submitButton.loading = false; - } -} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html new file mode 100644 index 0000000000000..59132d457ebaa --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html @@ -0,0 +1,73 @@ + + + + + + +
+ + +
+
+
+ + + + Delete + + {{ metaType }} + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss similarity index 100% rename from src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-link/deletion-link.component.scss rename to src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts new file mode 100644 index 0000000000000..bce74a2be3fc5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts @@ -0,0 +1,342 @@ +import { Component, NgModule, TemplateRef, ViewChild } from '@angular/core'; +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap'; +import { Observable } from 'rxjs/Observable'; +import { Subscriber } from 'rxjs/Subscriber'; + +import { ModalComponent } from '../modal/modal.component'; +import { SubmitButtonComponent } from '../submit-button/submit-button.component'; +import { DeletionModalComponent } from './deletion-modal.component'; + +@NgModule({ + entryComponents: [DeletionModalComponent] +}) +export class MockModule {} + +@Component({ + template: ` + + + + ` +}) +class MockComponent { + @ViewChild('ctrlDescription') ctrlDescription: TemplateRef; + @ViewChild('modalDescription') modalDescription: TemplateRef; + someData = [1, 2, 3, 4, 5]; + finished: number[]; + ctrlRef: BsModalRef; + modalRef: BsModalRef; + + // Normally private - public was needed for the tests + constructor(public modalService: BsModalService) {} + + openCtrlDriven() { + this.ctrlRef = this.modalService.show(DeletionModalComponent); + this.ctrlRef.content.setUp({ + metaType: 'Controller delete handling', + pattern: 'ctrl-test', + deletionMethod: this.fakeDeleteController.bind(this), + description: this.ctrlDescription, + modalRef: this.ctrlRef + }); + } + + openModalDriven() { + this.modalRef = this.modalService.show(DeletionModalComponent); + this.modalRef.content.setUp({ + metaType: 'Modal delete handling', + pattern: 'modal-test', + deletionObserver: this.fakeDelete(), + description: this.modalDescription, + modalRef: this.modalRef + }); + } + + finish() { + this.finished = [6, 7, 8, 9]; + } + + fakeDelete() { + return (): Observable => { + return new Observable((observer: Subscriber) => { + Observable.timer(100).subscribe(() => { + observer.next(this.finish()); + observer.complete(); + }); + }); + }; + } + + fakeDeleteController() { + Observable.timer(100).subscribe(() => { + this.finish(); + this.ctrlRef.hide(); + }); + } +} + +describe('DeletionModalComponent', () => { + let mockComponent: MockComponent; + let component: DeletionModalComponent; + let mockFixture: ComponentFixture; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ MockComponent, DeletionModalComponent, ModalComponent, + SubmitButtonComponent], + imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule], + }) + .compileComponents(); + })); + + beforeEach(() => { + mockFixture = TestBed.createComponent(MockComponent); + mockComponent = mockFixture.componentInstance; + // Mocking the modals as a lot would be left over + spyOn(mockComponent.modalService, 'show').and.callFake(() => { + const ref = new BsModalRef(); + fixture = TestBed.createComponent(DeletionModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + ref.content = component; + return ref; + }); + mockComponent.openCtrlDriven(); + mockFixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('setUp', () => { + const clearSetup = () => { + component.metaType = undefined; + component.pattern = 'yes'; + component.deletionObserver = undefined; + component.description = undefined; + component.modalRef = undefined; + }; + + const expectSetup = (metaType, observer: boolean, method: boolean, pattern, + template: boolean) => { + expect(component.modalRef).toBeTruthy(); + expect(component.metaType).toBe(metaType); + expect(!!component.deletionObserver).toBe(observer); + expect(!!component.deletionMethod).toBe(method); + expect(component.pattern).toBe(pattern); + expect(!!component.description).toBe(template); + }; + + beforeEach(() => { + clearSetup(); + }); + + it('should throw error if no modal reference is given', () => { + expect(() => component.setUp({ + metaType: undefined, + modalRef: undefined + })).toThrowError('No modal reference'); + }); + + it('should throw error if no meta type is given', () => { + expect(() => component.setUp({ + metaType: undefined, + modalRef: mockComponent.ctrlRef + })).toThrowError('No meta type'); + }); + + it('should throw error if no deletion method is given', () => { + expect(() => component.setUp({ + metaType: 'Sth', + modalRef: mockComponent.ctrlRef + })).toThrowError('No deletion method'); + }); + + it('should throw no errors if metaType, modalRef and a deletion method were given', + () => { + component.setUp({ + metaType: 'Observer', + modalRef: mockComponent.ctrlRef, + deletionObserver: mockComponent.fakeDelete() + }); + expectSetup('Observer', true, false, 'yes', false); + clearSetup(); + component.setUp({ + metaType: 'Controller', + modalRef: mockComponent.ctrlRef, + deletionMethod: mockComponent.fakeDeleteController + }); + expectSetup('Controller', false, true, 'yes', false); + }); + + it('should test optional parameters - pattern and description', + () => { + component.setUp({ + metaType: 'Pattern only', + modalRef: mockComponent.ctrlRef, + deletionObserver: mockComponent.fakeDelete(), + pattern: '{sth/!$_8()' + }); + expectSetup('Pattern only', true, false, '{sth/!$_8()', false); + clearSetup(); + component.setUp({ + metaType: 'Description only', + modalRef: mockComponent.ctrlRef, + deletionObserver: mockComponent.fakeDelete(), + description: mockComponent.modalDescription + }); + expectSetup('Description only', true, false, 'yes', true); + clearSetup(); + component.setUp({ + metaType: 'Description and pattern', + modalRef: mockComponent.ctrlRef, + deletionObserver: mockComponent.fakeDelete(), + description: mockComponent.modalDescription, + pattern: '{sth/!$_8()' + }); + expectSetup('Description and pattern', true, false, '{sth/!$_8()', true); + }); + }); + + it('should test if the ctrl driven mock is set correctly through mock component', () => { + expect(component.metaType).toBe('Controller delete handling'); + expect(component.pattern).toBe('ctrl-test'); + expect(component.description).toBeTruthy(); + expect(component.modalRef).toBeTruthy(); + expect(component.deletionMethod).toBeTruthy(); + expect(component.deletionObserver).not.toBeTruthy(); + }); + + it('should test if the modal driven mock is set correctly through mock component', () => { + mockComponent.openModalDriven(); + expect(component.metaType).toBe('Modal delete handling'); + expect(component.pattern).toBe('modal-test'); + expect(component.description).toBeTruthy(); + expect(component.modalRef).toBeTruthy(); + expect(component.deletionObserver).toBeTruthy(); + expect(component.deletionMethod).not.toBeTruthy(); + }); + + describe('component functions', () => { + const changeValue = (value) => { + component.confirmation.setValue(value); + component.confirmation.markAsDirty(); + component.confirmation.updateValueAndValidity(); + fixture.detectChanges(); + }; + + it('should test hideModal', () => { + expect(component.modalRef).toBeTruthy(); + expect(component.hideModal).toBeTruthy(); + spyOn(component.modalRef, 'hide').and.callThrough(); + expect(component.modalRef.hide).not.toHaveBeenCalled(); + component.hideModal(); + expect(component.modalRef.hide).toHaveBeenCalled(); + }); + + describe('invalid control', () => { + const testInvalidControl = (submitted: boolean, error: string, expected: boolean) => { + expect(component.invalidControl(submitted, error)).toBe(expected); + }; + + beforeEach(() => { + component.deletionForm.reset(); + }); + + it('should test empty values', () => { + expect(component.invalidControl).toBeTruthy(); + component.deletionForm.reset(); + testInvalidControl(false, undefined, false); + testInvalidControl(true, 'required', true); + component.deletionForm.reset(); + changeValue('let-me-pass'); + changeValue(''); + testInvalidControl(true, 'required', true); + }); + + it('should test pattern', () => { + changeValue('let-me-pass'); + testInvalidControl(false, 'pattern', true); + changeValue('ctrl-test'); + testInvalidControl(false, undefined, false); + testInvalidControl(true, undefined, false); + }); + }); + + describe('deletion call', () => { + beforeEach(() => { + spyOn(component, 'stopLoadingSpinner').and.callThrough(); + spyOn(component, 'hideModal').and.callThrough(); + }); + + describe('Controller driven', () => { + beforeEach(() => { + spyOn(component, 'deletionMethod').and.callThrough(); + spyOn(mockComponent.ctrlRef, 'hide').and.callThrough(); + }); + + it('should test fake deletion that closes modal', fakeAsync(() => { + // Before deletionCall + expect(component.deletionMethod).not.toHaveBeenCalled(); + // During deletionCall + component.deletionCall(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.ctrlRef.hide).not.toHaveBeenCalled(); + expect(component.deletionMethod).toHaveBeenCalled(); + expect(mockComponent.finished).toBe(undefined); + // After deletionCall + tick(2000); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.ctrlRef.hide).toHaveBeenCalled(); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + })); + }); + + describe('Modal driven', () => { + beforeEach(() => { + mockComponent.openModalDriven(); + spyOn(mockComponent.modalRef, 'hide').and.callThrough(); + spyOn(component, 'stopLoadingSpinner').and.callThrough(); + spyOn(component, 'hideModal').and.callThrough(); + spyOn(mockComponent, 'fakeDelete').and.callThrough(); + }); + + it('should delete and close modal', fakeAsync(() => { + // During deletionCall + component.deletionCall(); + expect(mockComponent.finished).toBe(undefined); + expect(component.hideModal).not.toHaveBeenCalled(); + expect(mockComponent.modalRef.hide).not.toHaveBeenCalled(); + // After deletionCall + tick(2000); + expect(mockComponent.finished).toEqual([6, 7, 8, 9]); + expect(mockComponent.modalRef.hide).toHaveBeenCalled(); + expect(component.stopLoadingSpinner).not.toHaveBeenCalled(); + expect(component.hideModal).toHaveBeenCalled(); + })); + }); + }); + }); + +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts new file mode 100644 index 0000000000000..d27963cfe5f4d --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts @@ -0,0 +1,100 @@ +import { + Component, OnInit, TemplateRef, ViewChild +} from '@angular/core'; +import { FormControl, FormGroup, Validators } from '@angular/forms'; + +import { BsModalRef, BsModalService } from 'ngx-bootstrap'; +import { Observable } from 'rxjs/Observable'; + +import { SubmitButtonComponent } from '../submit-button/submit-button.component'; + +@Component({ + selector: 'cd-deletion-modal', + templateUrl: './deletion-modal.component.html', + styleUrls: ['./deletion-modal.component.scss'] +}) +export class DeletionModalComponent implements OnInit { + @ViewChild(SubmitButtonComponent) submitButton: SubmitButtonComponent; + description: TemplateRef; + metaType: string; + pattern = 'yes'; + deletionObserver: () => Observable; + deletionMethod: Function; + modalRef: BsModalRef; + + deletionForm: FormGroup; + confirmation: FormControl; + + // Parameters are destructed here than assigned to specific types and marked as optional + setUp({modalRef, metaType, deletionMethod, pattern, deletionObserver, description}: + { modalRef: BsModalRef, metaType: string, deletionMethod?: Function, pattern?: string, + deletionObserver?: () => Observable, description?: TemplateRef}) { + if (!modalRef) { + throw new Error('No modal reference'); + } else if (!metaType) { + throw new Error('No meta type'); + } else if (!(deletionMethod || deletionObserver)) { + throw new Error('No deletion method'); + } + this.metaType = metaType; + this.modalRef = modalRef; + this.deletionMethod = deletionMethod; + this.pattern = pattern || this.pattern; + this.deletionObserver = deletionObserver; + this.description = description; + } + + ngOnInit() { + this.confirmation = new FormControl('', { + validators: [ + Validators.required + ], + updateOn: 'blur' + }); + this.deletionForm = new FormGroup({ + confirmation: this.confirmation + }); + } + + invalidControl(submitted: boolean, error?: string): boolean { + const control = this.confirmation; + return !!( + (submitted || control.dirty) && + control.invalid && + (error ? control.errors[error] : true) + ); + } + + updateConfirmation($e) { + if ($e.key !== 'Enter') { + return; + } + this.confirmation.setValue($e.target.value); + this.confirmation.markAsDirty(); + this.confirmation.updateValueAndValidity(); + } + + delete () { + this.submitButton.submit(); + } + + deletionCall() { + if (this.deletionObserver) { + this.deletionObserver().subscribe( + undefined, + this.stopLoadingSpinner.bind(this), + this.hideModal.bind(this) + ); + } else { + this.deletionMethod(); + } + } + + hideModal() { + this.modalRef.hide(); + } + + stopLoadingSpinner() { + this.submitButton.loading = false; + } +} -- 2.39.5