mirror of
https://github.com/ceph/ceph
synced 2025-02-23 02:57:21 +00:00
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:
parent
a0c6b83334
commit
4574407a9e
@ -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', {
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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
|
||||
});
|
||||
|
@ -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,
|
||||
|
@ -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">
|
||||
|
@ -0,0 +1,6 @@
|
||||
.modal-body .question {
|
||||
font-weight: bold;
|
||||
}
|
||||
.modal-body .question .checkbox {
|
||||
padding-top: 7px;
|
||||
}
|
@ -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', () => {
|
||||
|
@ -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, '\\$&');
|
||||
}
|
||||
}
|
||||
|
@ -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 {}
|
@ -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]
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user