]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: Enable custom badges
authorStephan Müller <smueller@suse.com>
Tue, 7 Aug 2018 12:23:26 +0000 (14:23 +0200)
committerStephan Müller <smueller@suse.com>
Tue, 9 Oct 2018 12:51:31 +0000 (14:51 +0200)
Enables custom badges within badges component.

It's possible to use custom validations and custom error messages.

Fixes: https://tracker.ceph.com/issues/36357
Signed-off-by: Stephan Müller <smueller@suse.com>
src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges-option.model.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss
src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts

index 0c28f1cc532c3aa937ebbd0bbdb09a54bf99cd90..49f29330fa39c7e4848249ac495453c7d4a77a85 100644 (file)
@@ -1,5 +1,11 @@
-interface SelectBadgesOption {
+export class SelectBadgesOption {
   selected: boolean;
   name: string;
   description: string;
+
+  constructor(selected: boolean, name: string, description: string) {
+    this.selected = selected;
+    this.name = name;
+    this.description = description;
+  }
 }
index 2fa9d1b5247760e757882cee862fbd0e7ddc8070..22ceb6901111da11ac58a8bec4d503d32b12a3c5 100644 (file)
@@ -1,7 +1,35 @@
 <ng-template #popTemplate>
+  <ng-container *ngIf="customBadges">
+    <form name="form"
+          #formDir="ngForm"
+          [formGroup]="form"
+          novalidate>
+      <div [ngClass]="{'has-error': form.showError('customBadge', formDir)}">
+        <input type="text"
+               formControlName="customBadge"
+               i18n-placeholder
+               [placeholder]="customBadgeMessage"
+               (keyup)="$event.keyCode == 13 ? addCustomOption(customBadge) : null"
+               class="form-control text-center"/>
+        <ng-container *ngFor="let error of Object.keys(errorMessages.custom.validation)">
+          <span
+            i18n
+            class="help-block text-center"
+            *ngIf="form.showError('customBadge', formDir) && customBadge.hasError(error)">
+            {{ errorMessages.custom.validation[error] }}
+          </span>
+        </ng-container>
+        <span i18n
+              class="help-block text-center"
+              *ngIf="form.showError('customBadge', formDir) && customBadge.hasError('duplicate')">
+          {{ errorMessages.custom.duplicate }}
+        </span>
+      </div>
+    </form>
+  </ng-container>
   <div *ngFor="let option of options"
        class="select-menu-item"
-       (click)="selectOption(option)">
+       (click)="triggerSelection(option)">
     <div class="select-menu-item-icon">
       <i class="fa fa-check" aria-hidden="true"
          *ngIf="option.selected"></i>
     </div>
     <div class="select-menu-item-content">
       {{ option.name }}
-      <br>
-      <small class="text-muted">
-        {{ option.description }}&nbsp;
-      </small>
+      <ng-container *ngIf="option.description">
+        <br>
+        <small class="text-muted">
+          {{ option.description }}&nbsp;
+        </small>
+      </ng-container>
     </div>
   </div>
+  <span i18n
+        class="help-block text-center"
+        *ngIf="data.length === selectionLimit">
+          {{ errorMessages.selectionLimit }}
+  </span>
 </ng-template>
 
 <a  class="margin-right-sm select-menu-edit"
@@ -27,7 +62,7 @@
 <span class="text-muted"
       *ngIf="data.length === 0"
       i18n>
-  {{ emptyMessage }}
+  {{ errorMessages.empty }}
 </span>
 <span *ngFor="let dataItem of data">
   <span class="badge badge-pill badge-primary margin-right-sm">
index 8a1109be2e1759fe09fb9490d2a927b1737d3c0e..74814b636db384fcb9d49b9f256f8451eb7b080a 100644 (file)
 }
 .select-menu-item-icon {
   float: left;
-  padding: 8px 8px 8px 8px;
-  width: 30px;
+  padding: 0.5em;
+  width: 3em;
 }
 .select-menu-item-content {
-  padding: 8px 8px 8px 8px;
+  padding: 0.5em;
 }
 .badge-remove {
   color: $color-solid-white;
index ab8d72665b638096c48cf65bf138a074488bacb3..0c311d4a3895249f985f5feff93d8b531f4fceff 100644 (file)
@@ -2,7 +2,9 @@ import { ComponentFixture, TestBed } from '@angular/core/testing';
 
 import { PopoverModule } from 'ngx-bootstrap';
 
+import { FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
 import { configureTestBed } from '../../../../testing/unit-test-helper';
+import { SelectBadgesOption } from './select-badges-option.model';
 import { SelectBadgesComponent } from './select-badges.component';
 
 describe('SelectBadgesComponent', () => {
@@ -11,13 +13,18 @@ describe('SelectBadgesComponent', () => {
 
   configureTestBed({
     declarations: [SelectBadgesComponent],
-    imports: [PopoverModule.forRoot()]
+    imports: [PopoverModule.forRoot(), FormsModule, ReactiveFormsModule]
   });
 
   beforeEach(() => {
     fixture = TestBed.createComponent(SelectBadgesComponent);
     component = fixture.componentInstance;
     fixture.detectChanges();
+    component.options = [
+      { name: 'option1', description: '', selected: false },
+      { name: 'option2', description: '', selected: false },
+      { name: 'option3', description: '', selected: false }
+    ];
   });
 
   it('should create', () => {
@@ -25,20 +32,12 @@ describe('SelectBadgesComponent', () => {
   });
 
   it('should add item', () => {
-    component.options = [
-      { name: 'option1', description: '', selected: false },
-      { name: 'option2', description: '', selected: false }
-    ];
     component.data = [];
-    component.selectOption(component.options[1]);
+    component.triggerSelection(component.options[1]);
     expect(component.data).toEqual(['option2']);
   });
 
   it('should update selected', () => {
-    component.options = [
-      { name: 'option1', description: '', selected: false },
-      { name: 'option2', description: '', selected: false }
-    ];
     component.data = ['option2'];
     component.ngOnChanges();
     expect(component.options[0].selected).toBe(false);
@@ -46,12 +45,130 @@ describe('SelectBadgesComponent', () => {
   });
 
   it('should remove item', () => {
-    component.options = [
-      { name: 'option1', description: '', selected: true },
-      { name: 'option2', description: '', selected: true }
-    ];
-    component.data = ['option1', 'option2'];
+    component.options.map((option) => {
+      option.selected = true;
+      return option;
+    });
+    component.data = ['option1', 'option2', 'option3'];
     component.removeItem('option1');
-    expect(component.data).toEqual(['option2']);
+    expect(component.data).toEqual(['option2', 'option3']);
+  });
+
+  it('should not remove item that is not selected', () => {
+    component.options[0].selected = true;
+    component.data = ['option1'];
+    component.removeItem('option2');
+    expect(component.data).toEqual(['option1']);
+  });
+
+  describe('automatically add selected options if not in options array', () => {
+    beforeEach(() => {
+      component.data = ['option1', 'option4'];
+      expect(component.options.length).toBe(3);
+    });
+
+    const expectedResult = () => {
+      expect(component.options.length).toBe(4);
+      expect(component.options[3]).toEqual(new SelectBadgesOption(true, 'option4', ''));
+    };
+
+    it('with no extra settings', () => {
+      component.ngOnInit();
+      expectedResult();
+    });
+
+    it('with custom badges', () => {
+      component.customBadges = true;
+      component.ngOnInit();
+      expectedResult();
+    });
+
+    it('with limit higher than selected', () => {
+      component.selectionLimit = 3;
+      component.ngOnInit();
+      expectedResult();
+    });
+
+    it('with limit equal to selected', () => {
+      component.selectionLimit = 2;
+      component.ngOnInit();
+      expectedResult();
+    });
+
+    it('with limit lower than selected', () => {
+      component.selectionLimit = 1;
+      component.ngOnInit();
+      expectedResult();
+    });
+  });
+
+  describe('with custom options', () => {
+    beforeEach(() => {
+      component.customBadges = true;
+      component.customBadgeValidators = [Validators.pattern('[A-Za-z0-9_]+')];
+      component.ngOnInit();
+      component.customBadge.setValue('customOption');
+      component.addCustomOption();
+    });
+
+    it('adds custom option', () => {
+      expect(component.options[3]).toEqual({
+        name: 'customOption',
+        description: '',
+        selected: true
+      });
+      expect(component.data).toEqual(['customOption']);
+    });
+
+    it('will not add an option that did not pass the validation', () => {
+      component.customBadge.setValue(' this does not pass ');
+      component.addCustomOption();
+      expect(component.options.length).toBe(4);
+      expect(component.data).toEqual(['customOption']);
+      expect(component.customBadge.invalid).toBeTruthy();
+    });
+
+    it('removes custom item selection by name', () => {
+      component.removeItem('customOption');
+      expect(component.data).toEqual([]);
+      expect(component.options[3]).toEqual({
+        name: 'customOption',
+        description: '',
+        selected: false
+      });
+    });
+
+    it('will not add an option that is already there', () => {
+      component.customBadge.setValue('option2');
+      component.addCustomOption();
+      expect(component.options.length).toBe(4);
+      expect(component.data).toEqual(['customOption']);
+    });
+
+    it('will not add an option twice after each other', () => {
+      component.customBadge.setValue('onlyOnce');
+      component.addCustomOption();
+      component.addCustomOption();
+      expect(component.data).toEqual(['customOption', 'onlyOnce']);
+      expect(component.options.length).toBe(5);
+    });
+  });
+
+  describe('if the selection limit is reached', function() {
+    beforeEach(() => {
+      component.selectionLimit = 2;
+      component.triggerSelection(component.options[0]);
+      component.triggerSelection(component.options[1]);
+    });
+
+    it('will not select more options', () => {
+      component.triggerSelection(component.options[2]);
+      expect(component.data).toEqual(['option1', 'option2']);
+    });
+
+    it('will unselect options that are selected', () => {
+      component.triggerSelection(component.options[1]);
+      expect(component.data).toEqual(['option1']);
+    });
   });
 });
index 38dc5cbae878259ed1e4197c03f531a141f90504..f42a89037280718695345f68633ac9edb385cf56 100644 (file)
@@ -1,30 +1,78 @@
-import { Component, OnChanges } from '@angular/core';
+import { Component, OnChanges, OnInit } from '@angular/core';
 import { Input } from '@angular/core';
+import { FormControl, ValidatorFn } from '@angular/forms';
+import { CdFormGroup } from '../../forms/cd-form-group';
+import { CdValidators } from '../../forms/cd-validators';
+import { SelectBadgesOption } from './select-badges-option.model';
 
 @Component({
   selector: 'cd-select-badges',
   templateUrl: './select-badges.component.html',
   styleUrls: ['./select-badges.component.scss']
 })
-export class SelectBadgesComponent implements OnChanges {
+export class SelectBadgesComponent implements OnInit, OnChanges {
+  @Input() data: Array<string> = [];
+  @Input() options: Array<SelectBadgesOption> = [];
   @Input()
-  data: Array<string> = [];
-  @Input()
-  options: Array<SelectBadgesOption> = [];
-  @Input()
-  emptyMessage = 'There are no items.';
+  errorMessages = {
+    empty: 'There are no items.',
+    selectionLimit: 'Selection limit reached',
+    custom: {
+      validation: {},
+      duplicate: 'Already exits'
+    }
+  };
+  @Input() selectionLimit: number;
+  @Input() customBadges = false;
+  @Input() customBadgeValidators: ValidatorFn[] = [];
+  @Input() customBadgeMessage = 'Use custom tag';
+  form: CdFormGroup;
+  customBadge: FormControl;
+  Object = Object;
 
   constructor() {}
 
-  ngOnChanges() {
-    if (!this.options || !this.data || this.data.length === 0) {
+  ngOnInit() {
+    if (this.customBadges) {
+      this.initCustomBadges();
+    }
+    if (this.data.length > 0) {
+      this.initMissingOptions();
+    }
+  }
+
+  private initCustomBadges() {
+    this.customBadgeValidators.push(
+      CdValidators.custom(
+        'duplicate',
+        (badge) => this.options && this.options.some((option) => option.name === badge)
+      )
+    );
+    this.customBadge = new FormControl('', { validators: this.customBadgeValidators });
+    this.form = new CdFormGroup({ customBadge: this.customBadge });
+  }
+
+  private initMissingOptions() {
+    const options = this.options.map((option) => option.name);
+    const needToCreate = this.data.filter((option) => options.indexOf(option) === -1);
+    needToCreate.forEach((option) => this.addOption(option));
+    this.forceOptionsToReflectData();
+  }
+
+  private addOption(name: string) {
+    this.options.push(new SelectBadgesOption(false, name, ''));
+    this.triggerSelection(this.options[this.options.length - 1]);
+  }
+
+  private triggerSelection(option: SelectBadgesOption) {
+    if (
+      !option ||
+      (this.selectionLimit && !option.selected && this.data.length >= this.selectionLimit)
+    ) {
       return;
     }
-    this.options.forEach((option) => {
-      if (this.data.indexOf(option.name) !== -1) {
-        option.selected = true;
-      }
-    });
+    option.selected = !option.selected;
+    this.updateOptions();
   }
 
   private updateOptions() {
@@ -36,16 +84,32 @@ export class SelectBadgesComponent implements OnChanges {
     });
   }
 
-  selectOption(option: SelectBadgesOption) {
-    option.selected = !option.selected;
-    this.updateOptions();
+  private forceOptionsToReflectData() {
+    this.options.forEach((option) => {
+      if (this.data.indexOf(option.name) !== -1) {
+        option.selected = true;
+      }
+    });
+  }
+
+  ngOnChanges() {
+    if (!this.options || !this.data || this.data.length === 0) {
+      return;
+    }
+    this.forceOptionsToReflectData();
+  }
+
+  addCustomOption() {
+    if (this.customBadge.invalid || this.customBadge.value.length === 0) {
+      return;
+    }
+    this.addOption(this.customBadge.value);
+    this.customBadge.setValue('');
   }
 
   removeItem(item: string) {
-    const optionToRemove = this.options.find((option: SelectBadgesOption) => {
-      return option.name === item;
-    });
-    optionToRemove.selected = false;
-    this.updateOptions();
+    this.triggerSelection(
+      this.options.find((option: SelectBadgesOption) => option.name === item && option.selected)
+    );
   }
 }