mirror of
https://github.com/ceph/ceph
synced 2025-03-11 02:39:05 +00:00
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 <smueller@suse.com>
This commit is contained in:
parent
bc9f34eacc
commit
bc7ead0bc6
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
@ -9,12 +37,19 @@
|
||||
</div>
|
||||
<div class="select-menu-item-content">
|
||||
{{ option.name }}
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
{{ option.description }}
|
||||
</small>
|
||||
<ng-container *ngIf="option.description">
|
||||
<br>
|
||||
<small class="text-muted">
|
||||
{{ option.description }}
|
||||
</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">
|
||||
|
@ -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;
|
||||
|
@ -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']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user