diff --git a/src/app/search/components/criteria/criteria-by-family.component.html b/src/app/search/components/criteria/criteria-by-family.component.html index a87c4078e9973dec42dcaef0fcd17095ce979001..cfc4953ccbb30312fadfb191551994a2c80f7128 100644 --- a/src/app/search/components/criteria/criteria-by-family.component.html +++ b/src/app/search/components/criteria/criteria-by-family.component.html @@ -59,6 +59,16 @@ (deleteCriterion)="emitDelete($event)"> </app-datalist> </div> + <div *ngSwitchCase="'list'"> + <app-list class="criteria" + [id]="attribute.id" + [label]="attribute.form_label" + [placeholder]="attribute.placeholder_min" + [criterion]="getCriterion(attribute.id)" + (addCriterion)="emitAdd($event)" + (deleteCriterion)="emitDelete($event)"> + </app-list> + </div> <div *ngSwitchCase="'radio'"> <app-radio class="criteria" [id]="attribute.id" diff --git a/src/app/search/components/criteria/search-type/index.ts b/src/app/search/components/criteria/search-type/index.ts index 6095595aab42e0aabfe5830735fd6896b8b1833a..cd9f4218bb09c6bb59680094785b3cb885a37721 100644 --- a/src/app/search/components/criteria/search-type/index.ts +++ b/src/app/search/components/criteria/search-type/index.ts @@ -3,6 +3,7 @@ import { BetweenComponent } from './between.component'; import { SelectComponent } from './select.component'; import { SelectMultipleComponent } from './select-multiple.component'; import { DatalistComponent } from './datalist.component'; +import { ListComponent } from './list.component'; import { RadioComponent } from './radio.component'; import { CheckboxComponent } from './checkbox.component'; import { DateComponent } from './date.component'; @@ -18,6 +19,7 @@ export const SearchTypeComponents = [ SelectComponent, SelectMultipleComponent, DatalistComponent, + ListComponent, RadioComponent, CheckboxComponent, DateComponent, diff --git a/src/app/search/components/criteria/search-type/list.component.html b/src/app/search/components/criteria/search-type/list.component.html new file mode 100644 index 0000000000000000000000000000000000000000..f717fca2831b02ffca413315dee791a28daef337 --- /dev/null +++ b/src/app/search/components/criteria/search-type/list.component.html @@ -0,0 +1,22 @@ +<div class="row"> + <div class="col form-group"> + <label>{{ label }}</label> + <div class="row"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="readonly">in</div> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <textarea class="form-control" rows="3" [formControl]="list" autocomplete="off"></textarea> + </div> + </div> + </div> + <div class="col-2 text-center align-self-end pb-3"> + <button class="btn btn-outline-success" *ngIf="!list.disabled" [hidden]="!list.value" (click)="emitAdd()"> + <span class="fas fa-plus fa-fw"></span> + </button> + <button class="btn btn-outline-danger" *ngIf="list.disabled" (click)="deleteCriterion.emit(id)"> + <span class="fa fa-times fa-fw"></span> + </button> + </div> +</div> \ No newline at end of file diff --git a/src/app/search/components/criteria/search-type/list.component.spec.ts b/src/app/search/components/criteria/search-type/list.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..00eca4c134bf5b882e443227aecde90e879f3501 --- /dev/null +++ b/src/app/search/components/criteria/search-type/list.component.spec.ts @@ -0,0 +1,73 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { Component, ViewChild } from '@angular/core'; +import { FormsModule, ReactiveFormsModule, FormControl } from '@angular/forms'; + +import { ListComponent } from './list.component'; +import { ListCriterion } from '../../../store/model'; + +describe('[Search][Criteria][SearchType] Component: ListComponent', () => { + @Component({ + selector: `app-host`, + template: `<app-list [criterion]="criterion"></app-list>` + }) + class TestHostComponent { + @ViewChild(ListComponent, { static: false }) + public testedComponent: ListComponent; + public criterion: ListComponent = undefined; + } + + let testHostComponent: TestHostComponent; + let testHostFixture: ComponentFixture<TestHostComponent>; + let testedComponent: ListComponent; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ + ListComponent, + TestHostComponent + ], + imports: [FormsModule, ReactiveFormsModule] + }); + testHostFixture = TestBed.createComponent(TestHostComponent); + testHostComponent = testHostFixture.componentInstance; + testHostFixture.detectChanges(); + testedComponent = testHostComponent.testedComponent; + })); + + it('should create the component', () => { + expect(testedComponent).toBeTruthy(); + }); + + it('#getDefault() should enable and not fill form as criterion not defined in host component', () => { + expect(testedComponent.list.value).toBeNull(); + expect(testedComponent.list.enabled).toBeTruthy(); + }); + + it('#getDefault() should fill and disable form if criterion is defined', () => { + const values = ['1', '2']; + const criterion = { id: 1, type: 'list', values } as ListCriterion; + const expectedListValues = values.join('\n'); + testedComponent.getDefault(criterion); + expect(testedComponent.list.value).toBe(expectedListValues); + expect(testedComponent.list.disabled).toBeTruthy(); + }); + + it('#getPlaceholder() should fill the placeholder if defined', () => { + const placeholder = 'placeholder'; + testedComponent.placeholder = placeholder; + expect(testedComponent.getPlaceholder()).toBe(placeholder); + }); + + it('#getPlaceholder() should not fill the placeholder if not defined', () => { + expect(testedComponent.getPlaceholder()).toBe(''); + }); + + it('raises the add criterion event when clicked', () => { + testedComponent.id = 1; + const values = '1\n2'; + testedComponent.list = new FormControl(values); + const expectedCriterion = { id: testedComponent.id, type: 'list', values: ['1', '2'] } as ListCriterion; + testedComponent.addCriterion.subscribe((event: ListCriterion) => expect(event).toEqual(expectedCriterion)); + testedComponent.emitAdd(); + }); +}); diff --git a/src/app/search/components/criteria/search-type/list.component.ts b/src/app/search/components/criteria/search-type/list.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..97addf719bfe0ac40288f9cca4ca612710c78951 --- /dev/null +++ b/src/app/search/components/criteria/search-type/list.component.ts @@ -0,0 +1,48 @@ +import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { FormControl } from '@angular/forms'; + +import { ListCriterion, Criterion } from '../../../store/model'; + +@Component({ + selector: 'app-list', + templateUrl: 'list.component.html', + styleUrls: ['operator.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class ListComponent { + @Input() id: number; + @Input() label: string; + @Input() placeholder: string; + @Input() + set criterion(criterion: Criterion) { + this.getDefault(criterion); + } + @Output() addCriterion: EventEmitter<ListCriterion> = new EventEmitter(); + @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); + + list = new FormControl(''); + + getDefault(criterion: Criterion): void { + if (!criterion) { + this.list.reset(); + this.list.enable(); + } else { + const c = criterion as ListCriterion; + this.list.setValue(c.values.join('\n')); + this.list.disable(); + } + } + + getPlaceholder(): string { + if (!this.placeholder) { + return ''; + } else { + return this.placeholder; + } + } + + emitAdd() { + const ls = { id: this.id, type: 'list', values: this.list.value.split('\n') }; + this.addCriterion.emit(ls); + } +} diff --git a/src/app/search/store/model/index.ts b/src/app/search/store/model/index.ts index 65b1e366734185b96175101d8dee8dc79f77746b..718fefdc19cd762455805ce0fda3c022d501008d 100644 --- a/src/app/search/store/model/index.ts +++ b/src/app/search/store/model/index.ts @@ -5,3 +5,4 @@ export * from './select-multiple-criterion.model'; export * from './search-query-params.model'; export * from './json-criterion.model'; export * from './cone-search.model'; +export * from './list-criterion.model'; diff --git a/src/app/search/store/model/list-criterion.model.ts b/src/app/search/store/model/list-criterion.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..9d834c5ad9d2d5c3894e4195d8e1f74561a8a9e2 --- /dev/null +++ b/src/app/search/store/model/list-criterion.model.ts @@ -0,0 +1,7 @@ +import { Criterion } from './criterion.model'; + +export class ListCriterion implements Criterion { + id: number; + type: string; + values: string[]; +} diff --git a/src/app/search/store/search.effects.ts b/src/app/search/store/search.effects.ts index 40589fff1a5c7a4d692f89455c39cffcf6531e08..20bf7b7d64c694e23044766ccf9080ff5404fc59 100644 --- a/src/app/search/store/search.effects.ts +++ b/src/app/search/store/search.effects.ts @@ -98,18 +98,20 @@ export class SearchEffects { case 'date': case 'date-time': case 'time': - return {id: attribute.id, type: 'field', value: attribute.min.toString(), operator: attribute.operator}; + return { id: attribute.id, type: 'field', value: attribute.min.toString(), operator: attribute.operator }; + case 'list': + return { id: attribute.id, type: 'list', values: attribute.min.toString().split('|') }; case 'between': case 'between-date': - return {id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString()}; + return { id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString() }; case 'select-multiple': case 'checkbox': const msValues = attribute.min.toString().split('|'); const options = attribute.options.filter(option => msValues.includes(option.value)); - return {id: attribute.id, type: 'multiple', options}; + return { id: attribute.id, type: 'multiple', options }; case 'json': const [path, operator, value] = attribute.min.toString().split('|'); - return {id: attribute.id, type: 'json', path, operator, value}; + return { id: attribute.id, type: 'json', path, operator, value }; default: return null; @@ -128,19 +130,21 @@ export class SearchEffects { case 'date': case 'date-time': case 'time': - return {id: parseInt(params[0], 10), type: 'field', operator: params[1], value: params[2]}; + return { id: parseInt(params[0], 10), type: 'field', operator: params[1], value: params[2] }; + case 'list': + return { id: parseInt(params[0], 10), type: 'list', values: params[2].split('|') }; case 'between': case 'between-date': const bwValues = params[2].split('|'); - return {id: parseInt(params[0], 10), type: 'between', min: bwValues[0], max: bwValues[1]}; + return { id: parseInt(params[0], 10), type: 'between', min: bwValues[0], max: bwValues[1] }; case 'select-multiple': case 'checkbox': const msValues = params[2].split('|'); const options = attribute.options.filter(option => msValues.includes(option.value)); - return {id: parseInt(params[0], 10), type: 'multiple', options}; + return { id: parseInt(params[0], 10), type: 'multiple', options }; case 'json': const [path, operator, value] = params[2].split('|'); - return {id: parseInt(params[0], 10), type: 'json', path, operator, value}; + return { id: parseInt(params[0], 10), type: 'json', path, operator, value }; default: return null; diff --git a/src/app/shared/utils.ts b/src/app/shared/utils.ts index 70d64115b667906b73fe9189385a2da0f1e98bf8..abdee53173da6c3c576339d19ea16df3c43ca821 100644 --- a/src/app/shared/utils.ts +++ b/src/app/shared/utils.ts @@ -1,6 +1,13 @@ import { RouterStateSerializer } from '@ngrx/router-store'; import { RouterStateSnapshot, Params } from '@angular/router'; -import {Â Criterion, BetweenCriterion, FieldCriterion, JsonCriterion, SelectMultipleCriterion } from '../search/store/model'; +import { + Criterion, + BetweenCriterion, + FieldCriterion, + JsonCriterion, + SelectMultipleCriterion, + ListCriterion +} from '../search/store/model'; export interface RouterStateUrl { url: string; @@ -43,7 +50,10 @@ export const printCriterion = (criterion: Criterion): string => { } case 'field': const fd = criterion as FieldCriterion; - return getPrettyOperator(fd.operator) + ' ' + fd.value; + return getPrettyOperator(fd.operator) + ' ' + fd.value.split('|').join(', '); + case 'list': + const ls = criterion as ListCriterion; + return '[' + ls.values.join(',') + ']'; case 'json' : const json = criterion as JsonCriterion; return json.path + ' ' + json.operator + ' ' + json.value; @@ -71,6 +81,10 @@ export const getCriterionStr = (criterion: Criterion): string => { const fd = criterion as FieldCriterion; str += '::' + fd.operator + '::' + fd.value; } + if (criterion.type === 'list') { + const ls = criterion as ListCriterion; + str += '::in::' + ls.values.join('|'); + } if (criterion.type === 'json') { const json = criterion as JsonCriterion; str += '::js::' + json.path + '|' + json.operator + '|' + json.value;