mirror of
https://github.com/ceph/ceph
synced 2025-03-29 23:09:47 +00:00
mgr/dashboard: Change deletion link to modal only
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 <smueller@suse.com>
This commit is contained in:
parent
f2096e9378
commit
63ae858582
@ -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 { }
|
||||
|
@ -1,88 +0,0 @@
|
||||
<a (click)="showModal(deletionModal)">
|
||||
<i class="fa fa-fw fa-trash-o"></i>
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</a>
|
||||
|
||||
<ng-template #deletionModal>
|
||||
<cd-modal #modal
|
||||
[modalRef]="bsModalRef">
|
||||
<ng-container class="modal-title">
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container class="modal-content">
|
||||
<ng-container *ngTemplateOutlet="deletionContent"></ng-container>
|
||||
</ng-container>
|
||||
</cd-modal>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #deletionContent>
|
||||
<form name="deletionForm"
|
||||
#formDir="ngForm"
|
||||
(submit)="delete()"
|
||||
[formGroup]="deletionForm"
|
||||
novalidate>
|
||||
<div class="modal-body">
|
||||
<ng-template *ngTemplateOutlet="deletionDescription"></ng-template>
|
||||
<p>
|
||||
<ng-container i18n>
|
||||
To confirm the deletion, enter
|
||||
</ng-container>
|
||||
<kbd>{{ pattern }}</kbd>
|
||||
<ng-container i18n>
|
||||
and click on
|
||||
</ng-container>
|
||||
<kbd>
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</kbd>.
|
||||
</p>
|
||||
<div class="form-group"
|
||||
[ngClass]="{'has-error': invalidControl(formDir.submitted)}">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="confirmation"
|
||||
id="confirmation"
|
||||
[placeholder]="pattern"
|
||||
autocomplete="off"
|
||||
(keyup)="updateConfirmation($event)"
|
||||
formControlName="confirmation"
|
||||
autofocus>
|
||||
<span class="help-block"
|
||||
*ngIf="invalidControl(formDir.submitted,'required')"
|
||||
i18n>
|
||||
This field is required.
|
||||
</span>
|
||||
<span class="help-block"
|
||||
*ngIf="invalidControl(formDir.submitted, 'pattern')">
|
||||
'{{ confirmation.value }}'
|
||||
<span i18n>doesn't match</span>
|
||||
'{{ pattern }}'.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<cd-submit-button #submitButton
|
||||
[form]="deletionForm"
|
||||
(submitAction)="deletionCall()">
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</cd-submit-button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
(click)="hideModal()"
|
||||
i18n>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #deletionHeading>
|
||||
<ng-container i18n>
|
||||
Delete
|
||||
</ng-container>
|
||||
{{ metaType }}
|
||||
</ng-template>
|
||||
|
||||
<ng-template #deletionDescription>
|
||||
<ng-content></ng-content>
|
||||
</ng-template>
|
||||
|
@ -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: `
|
||||
<cd-deletion-link #ctrlDeleteButton
|
||||
metaType="Controller delete handling"
|
||||
pattern="ctrl-test"
|
||||
(toggleDeletion)="fakeDeleteController()">
|
||||
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.
|
||||
</cd-deletion-link>
|
||||
<cd-deletion-link #modalDeleteButton
|
||||
metaType="Modal delete handling"
|
||||
[deletionObserver]="fakeDelete()"
|
||||
pattern="modal-test">
|
||||
The spinner is handled by the modal if your given deletion function returns a Observable.
|
||||
</cd-deletion-link>
|
||||
`
|
||||
})
|
||||
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<any> => {
|
||||
return new Observable((observer: Subscriber<any>) => {
|
||||
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<MockComponent>;
|
||||
|
||||
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', <any>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', <any>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]);
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -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<any>;
|
||||
@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<any>) {
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
<cd-modal #modal
|
||||
[modalRef]="modalRef">
|
||||
<ng-container class="modal-title">
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</ng-container>
|
||||
|
||||
<ng-container class="modal-content">
|
||||
<form name="deletionForm"
|
||||
#formDir="ngForm"
|
||||
(submit)="delete()"
|
||||
[formGroup]="deletionForm"
|
||||
novalidate>
|
||||
<div class="modal-body">
|
||||
<ng-container *ngTemplateOutlet="description"></ng-container>
|
||||
<p>
|
||||
<ng-container i18n>
|
||||
To confirm the deletion, enter
|
||||
</ng-container>
|
||||
<kbd>{{ pattern }}</kbd>
|
||||
<ng-container i18n>
|
||||
and click on
|
||||
</ng-container>
|
||||
<kbd>
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</kbd>.
|
||||
</p>
|
||||
<div class="form-group"
|
||||
[ngClass]="{'has-error': invalidControl(formDir.submitted)}">
|
||||
<input type="text"
|
||||
class="form-control"
|
||||
name="confirmation"
|
||||
id="confirmation"
|
||||
[placeholder]="pattern"
|
||||
[pattern]="pattern"
|
||||
autocomplete="off"
|
||||
(keyup)="updateConfirmation($event)"
|
||||
formControlName="confirmation"
|
||||
autofocus>
|
||||
<span class="help-block"
|
||||
*ngIf="invalidControl(formDir.submitted,'required')"
|
||||
i18n>
|
||||
This field is required.
|
||||
</span>
|
||||
<span class="help-block"
|
||||
*ngIf="invalidControl(formDir.submitted, 'pattern')">
|
||||
'{{ confirmation.value }}'
|
||||
<span i18n>doesn't match</span>
|
||||
'{{ pattern }}'.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<cd-submit-button #submitButton
|
||||
[form]="deletionForm"
|
||||
(submitAction)="deletionCall()">
|
||||
<ng-container *ngTemplateOutlet="deletionHeading"></ng-container>
|
||||
</cd-submit-button>
|
||||
<button class="btn btn-link btn-sm"
|
||||
(click)="hideModal()"
|
||||
i18n>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</ng-container>
|
||||
</cd-modal>
|
||||
|
||||
<ng-template #deletionHeading>
|
||||
<ng-container i18n>
|
||||
Delete
|
||||
</ng-container>
|
||||
{{ metaType }}
|
||||
</ng-template>
|
@ -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: `
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="openCtrlDriven()">
|
||||
<i class="fa fa-fw fa-trash"></i>Deletion Ctrl-Test
|
||||
<ng-template #ctrlDescription>
|
||||
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.
|
||||
</ng-template>
|
||||
</button>
|
||||
|
||||
<button type="button"
|
||||
class="btn btn-sm btn-primary"
|
||||
(click)="openModalDriven()">
|
||||
<i class="fa fa-fw fa-trash"></i>Deletion Modal-Test
|
||||
<ng-template #modalDescription>
|
||||
The spinner is handled by the modal if your given deletion function returns a Observable.
|
||||
</ng-template>
|
||||
</button>
|
||||
`
|
||||
})
|
||||
class MockComponent {
|
||||
@ViewChild('ctrlDescription') ctrlDescription: TemplateRef<any>;
|
||||
@ViewChild('modalDescription') modalDescription: TemplateRef<any>;
|
||||
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<any> => {
|
||||
return new Observable((observer: Subscriber<any>) => {
|
||||
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<MockComponent>;
|
||||
let fixture: ComponentFixture<DeletionModalComponent>;
|
||||
|
||||
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', <any>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', <any>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();
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@ -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<any>;
|
||||
metaType: string;
|
||||
pattern = 'yes';
|
||||
deletionObserver: () => Observable<any>;
|
||||
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<any>, description?: TemplateRef<any>}) {
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user