mgr/dashboard: Make deletion dialog more touch device friendly

* Refactor deletion dialog
* Add directives.module.ts to be able to use 'autofocus' in deletion dialog

Signed-off-by: Volker Theile <vtheile@suse.com>
This commit is contained in:
Volker Theile 2018-09-03 16:29:29 +02:00
parent a0c6b83334
commit 4574407a9e
11 changed files with 86 additions and 134 deletions

View File

@ -201,7 +201,6 @@ export class RbdListComponent implements OnInit {
this.modalRef = this.modalService.show(DeletionModalComponent);
this.modalRef.content.setUp({
metaType: 'RBD',
pattern: `${poolName}/${imageName}`,
deletionObserver: () =>
this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('rbd/delete', {

View File

@ -256,7 +256,6 @@ export class RbdSnapshotListComponent implements OnInit, OnChanges {
this.modalRef = this.modalService.show(DeletionModalComponent);
this.modalRef.content.setUp({
metaType: 'RBD snapshot',
pattern: snapshotName,
deletionMethod: () => this._asyncTask('deleteSnapshot', 'rbd/snap/delete', snapshotName),
modalRef: this.modalRef
});

View File

@ -94,7 +94,6 @@ export class RoleListComponent implements OnInit {
const name = this.selection.first().name;
this.modalRef.content.setUp({
metaType: 'Role',
pattern: `${name}`,
deletionMethod: () => this.deleteRole(name),
modalRef: this.modalRef
});

View File

@ -103,7 +103,6 @@ export class UserListComponent implements OnInit {
this.modalRef = this.modalService.show(DeletionModalComponent);
this.modalRef.content.setUp({
metaType: 'User',
pattern: `${username}`,
deletionMethod: () => this.deleteUser(username),
modalRef: this.modalRef
});

View File

@ -5,6 +5,7 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { ChartsModule } from 'ng2-charts/ng2-charts';
import { AlertModule, ModalModule, PopoverModule, TooltipModule } from 'ngx-bootstrap';
import { DirectivesModule } from '../directives/directives.module';
import { PipesModule } from '../pipes/pipes.module';
import { ConfirmationModalComponent } from './confirmation-modal/confirmation-modal.component';
import { DeletionModalComponent } from './deletion-modal/deletion-modal.component';
@ -31,7 +32,8 @@ import { WarningPanelComponent } from './warning-panel/warning-panel.component';
ChartsModule,
ReactiveFormsModule,
PipesModule,
ModalModule.forRoot()
ModalModule.forRoot(),
DirectivesModule
],
declarations: [
ViewCacheComponent,

View File

@ -11,41 +11,27 @@
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': deletionForm.showError('confirmation', formDir)}">
<input type="text"
class="form-control"
name="confirmation"
id="confirmation"
[placeholder]="pattern"
[pattern]="escapeRegExp(pattern)"
autocomplete="off"
(keyup)="updateConfirmation($event)"
formControlName="confirmation"
autofocus>
<span class="help-block"
*ngIf="deletionForm.showError('confirmation', formDir, 'required')"
i18n>
This field is required.
</span>
<span class="help-block"
*ngIf="deletionForm.showError('confirmation', formDir, 'pattern')">
'{{ confirmation.value }}'
<span i18n>doesn't match</span>
'{{ pattern }}'.
</span>
<div class="question">
<p>
<ng-container i18n>
Are you sure you want to delete the selected
</ng-container>
{{ metaType }}?
</p>
<div class="form-group"
[ngClass]="{'has-error': deletionForm.showError('confirmation', formDir)}">
<div class="checkbox checkbox-primary">
<input type="checkbox"
name="confirmation"
id="confirmation"
formControlName="confirmation"
autofocus>
<label i18n
for="confirmation">
I'm sure I want to proceed with the deletion.
</label>
</div>
</div>
</div>
</div>
<div class="modal-footer">

View File

@ -0,0 +1,6 @@
.modal-body .question {
font-weight: bold;
}
.modal-body .question .checkbox {
padding-top: 7px;
}

View File

@ -1,11 +1,13 @@
import { Component, NgModule, NO_ERRORS_SCHEMA, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { NgForm, ReactiveFormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import { BsModalRef, BsModalService, ModalModule } from 'ngx-bootstrap';
import { Observable, Subscriber, timer as observableTimer } from 'rxjs';
import { configureTestBed } from '../../../../testing/unit-test-helper';
import { DirectivesModule } from '../../directives/directives.module';
import { DeletionModalComponent } from './deletion-modal.component';
@NgModule({
@ -52,7 +54,6 @@ class MockComponent {
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
@ -63,7 +64,6 @@ class MockComponent {
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
@ -102,7 +102,7 @@ describe('DeletionModalComponent', () => {
configureTestBed({
declarations: [MockComponent, DeletionModalComponent],
schemas: [NO_ERRORS_SCHEMA],
imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule]
imports: [ModalModule.forRoot(), ReactiveFormsModule, MockModule, DirectivesModule]
});
beforeEach(() => {
@ -125,27 +125,31 @@ describe('DeletionModalComponent', () => {
expect(component).toBeTruthy();
});
it('should focus the checkbox form field', () => {
fixture = TestBed.createComponent(DeletionModalComponent);
fixture.detectChanges();
fixture.whenStable().then(() => {
const focused = fixture.debugElement.query(By.css(':focus'));
expect(focused.attributes.id).toBe('confirmation');
expect(focused.attributes.type).toBe('checkbox');
const element = document.getElementById('confirmation');
expect(element === document.activeElement).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
) => {
const expectSetup = (metaType, observer: boolean, method: boolean, 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);
};
@ -186,47 +190,29 @@ describe('DeletionModalComponent', () => {
modalRef: mockComponent.ctrlRef,
deletionObserver: mockComponent.fakeDelete()
});
expectSetup('Observer', true, false, 'yes', false);
expectSetup('Observer', true, false, false);
clearSetup();
component.setUp({
metaType: 'Controller',
modalRef: mockComponent.ctrlRef,
deletionMethod: mockComponent.fakeDeleteController
});
expectSetup('Controller', false, true, 'yes', false);
expectSetup('Controller', false, true, 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();
it('should test optional parameters - description', () => {
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);
expectSetup('Description only', true, false, 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();
@ -236,7 +222,6 @@ describe('DeletionModalComponent', () => {
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();
@ -245,9 +230,10 @@ describe('DeletionModalComponent', () => {
describe('component functions', () => {
const changeValue = (value) => {
component.confirmation.setValue(value);
component.confirmation.markAsDirty();
component.confirmation.updateValueAndValidity();
const ctrl = component.deletionForm.get('confirmation');
ctrl.setValue(value);
ctrl.markAsDirty();
ctrl.updateValueAndValidity();
fixture.detectChanges();
};
@ -276,26 +262,10 @@ describe('DeletionModalComponent', () => {
testValidation(false, undefined, false);
testValidation(true, 'required', true);
component.deletionForm.reset();
changeValue('let-me-pass');
changeValue('');
changeValue(true);
changeValue(false);
testValidation(true, 'required', true);
});
it('should test pattern', () => {
changeValue('let-me-pass');
testValidation(false, 'pattern', true);
changeValue('ctrl-test');
testValidation(false, undefined, false);
testValidation(true, undefined, false);
});
it('should test regex pattern', () => {
component.pattern = 'a+b';
changeValue('ab');
testValidation(false, 'pattern', true);
changeValue('a+b');
testValidation(false, 'pattern', false);
});
});
describe('deletion call', () => {

View File

@ -17,27 +17,23 @@ export class DeletionModalComponent implements OnInit {
submitButton: SubmitButtonComponent;
description: TemplateRef<any>;
metaType: string;
pattern = 'yes';
deletionObserver: () => Observable<any>;
deletionMethod: Function;
modalRef: BsModalRef;
deletionForm: CdFormGroup;
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>;
}) {
@ -51,30 +47,16 @@ export class DeletionModalComponent implements OnInit {
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 CdFormGroup({
confirmation: this.confirmation
confirmation: new FormControl(false, [Validators.requiredTrue])
});
}
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(
@ -94,8 +76,4 @@ export class DeletionModalComponent implements OnInit {
stopLoadingSpinner() {
this.deletionForm.setErrors({ cdSubmitButton: true });
}
escapeRegExp(text) {
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
}

View File

@ -0,0 +1,24 @@
import { NgModule } from '@angular/core';
import { AutofocusDirective } from './autofocus.directive';
import { Copy2ClipboardButtonDirective } from './copy2clipboard-button.directive';
import { DimlessBinaryDirective } from './dimless-binary.directive';
import { PasswordButtonDirective } from './password-button.directive';
@NgModule({
imports: [],
declarations: [
AutofocusDirective,
Copy2ClipboardButtonDirective,
DimlessBinaryDirective,
PasswordButtonDirective
],
exports: [
AutofocusDirective,
Copy2ClipboardButtonDirective,
DimlessBinaryDirective,
PasswordButtonDirective
],
providers: []
})
export class DirectivesModule {}

View File

@ -4,10 +4,7 @@ import { NgModule } from '@angular/core';
import { ApiModule } from './api/api.module';
import { ComponentsModule } from './components/components.module';
import { DataTableModule } from './datatable/datatable.module';
import { AutofocusDirective } from './directives/autofocus.directive';
import { Copy2ClipboardButtonDirective } from './directives/copy2clipboard-button.directive';
import { DimlessBinaryDirective } from './directives/dimless-binary.directive';
import { PasswordButtonDirective } from './directives/password-button.directive';
import { DirectivesModule } from './directives/directives.module';
import { PipesModule } from './pipes/pipes.module';
import { AuthGuardService } from './services/auth-guard.service';
import { AuthStorageService } from './services/auth-storage.service';
@ -21,24 +18,17 @@ import { ServicesModule } from './services/services.module';
ComponentsModule,
ServicesModule,
DataTableModule,
ApiModule
],
declarations: [
PasswordButtonDirective,
DimlessBinaryDirective,
Copy2ClipboardButtonDirective,
AutofocusDirective
ApiModule,
DirectivesModule
],
declarations: [],
exports: [
ComponentsModule,
PipesModule,
ServicesModule,
PasswordButtonDirective,
Copy2ClipboardButtonDirective,
DimlessBinaryDirective,
DataTableModule,
ApiModule,
AutofocusDirective
DirectivesModule
],
providers: [AuthStorageService, AuthGuardService, FormatterService]
})