]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Make deletion dialog more touch device friendly 23897/head
authorVolker Theile <vtheile@suse.com>
Mon, 3 Sep 2018 14:29:29 +0000 (16:29 +0200)
committerVolker Theile <vtheile@suse.com>
Tue, 11 Sep 2018 11:05:31 +0000 (13:05 +0200)
* Refactor deletion dialog
* Add directives.module.ts to be able to use 'autofocus' in deletion dialog

Signed-off-by: Volker Theile <vtheile@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-list/rbd-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/rbd-snapshot-list/rbd-snapshot-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/role-list/role-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/core/auth/user-list/user-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/components.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/deletion-modal/deletion-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/shared.module.ts

index 1c14a8b6ebdba002ed065780ac26b0a0954d4a82..2a4ac07d79e4b465990e534f32ffa3ca9eeed860 100644 (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', {
index 09bfdaea5f9a78888244f1539f9d273098ae4f32..c22ace30b2e927ca0718aed8ee453ddfefe09e97 100644 (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
     });
index 5919d38f32b01ee936fc27a02662576403e64769..cc668434e3bacb9d3c4032047b03029dea36cb61 100644 (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
     });
index e4bbf9ff3e0bb138686280fd8f20e8acc03c6256..ea62fc24a6c6b0d637ade9f20abdb552bb602249 100644 (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
     });
index ae30c292ecafec9cb4e625a1b9568ea28b0a9c4f..6d1376b1a2a02269c37ebe236a0f3f73fd2d631c 100644 (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,
index ca2f4ca5138dea91e845f340c9aece53e95246e9..a6dc08babdad4eacdef90e65da53271970d9259b 100644 (file)
           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">
index e4dd255b9e11554cfef268ec02034bcf8a9d0d20..7e72bbf02f7509e460238b1e6e28a74832b1aed3 100644 (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', () => {
index e819fd1a6880407fb30fd2720817d6afa2014f85..cab3e31dba00b239b84a657d9286cefcc7abe727 100644 (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, '\\$&');
-  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/directives/directives.module.ts
new file mode 100644 (file)
index 0000000..e2d9f7d
--- /dev/null
@@ -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 {}
index a082f12f4324f6af3c9aa5ec29cc77c6b20042e2..a8ced08310221bf0b41c8a223a5a1b8ed52acfef 100644 (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]
 })