From: Stephan Müller Date: Tue, 7 Aug 2018 12:23:26 +0000 (+0200) Subject: mgr/dashboard: Enable custom badges X-Git-Tag: v14.0.1~96^2~12 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=bc7ead0bc655bed579cdcb215744c2b1746d4b7b;p=ceph.git mgr/dashboard: Enable custom badges 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 --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges-option.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges-option.model.ts index 0c28f1cc532c..49f29330fa39 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges-option.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges-option.model.ts @@ -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; + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html index 2fa9d1b52477..22ceb6901111 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.html @@ -1,7 +1,35 @@ + +
+
+ + + + {{ errorMessages.custom.validation[error] }} + + + + {{ errorMessages.custom.duplicate }} + +
+
+
+ (click)="triggerSelection(option)">
@@ -9,12 +37,19 @@
{{ option.name }} -
- - {{ option.description }}  - + +
+ + {{ option.description }}  + +
+ + {{ errorMessages.selectionLimit }} +
- {{ emptyMessage }} + {{ errorMessages.empty }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss index 8a1109be2e17..74814b636db3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.scss @@ -14,11 +14,11 @@ } .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; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts index ab8d72665b63..0c311d4a3895 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.spec.ts @@ -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']); + }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts index 38dc5cbae878..f42a89037280 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/select-badges/select-badges.component.ts @@ -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 = []; + @Input() options: Array = []; @Input() - data: Array = []; - @Input() - options: Array = []; - @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) + ); } }