diff --git a/client/src/app/instance/search/components/criteria/abstract-search-type.component.ts b/client/src/app/instance/search/components/criteria/abstract-search-type.component.ts deleted file mode 100644 index 3f2f316803480b304f57ca1cb767b3434f2d8995..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/abstract-search-type.component.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface AbstractSearchTypeComponent { - data: any; -} diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html index cdf6dbf0fd601d2b8c5a9cd2d1eaddebcec666ac..fa4bc0823c2528089f3fa980ec930dacec8997eb 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.html +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.html @@ -2,6 +2,7 @@ <app-criterion [attribute]="attribute" [criterion]="getCriterion(attribute.id)" + [criteriaList]="criteriaList" (addCriterion)="emitAdd($event)" (deleteCriterion)="emitDelete($event)"> </app-criterion> diff --git a/client/src/app/instance/search/components/criteria/criterion.component.html b/client/src/app/instance/search/components/criteria/criterion.component.html index 3a9c1c003dd5b2b390d676d2ff866dc1c8328e9b..f2229d60c77545672ca338be7804676419533b60 100644 --- a/client/src/app/instance/search/components/criteria/criterion.component.html +++ b/client/src/app/instance/search/components/criteria/criterion.component.html @@ -5,12 +5,12 @@ </label> <ng-template searchType></ng-template> </div> - <div class="col-2 text-center align-self-end pb-3"> - <!-- <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> + <div class="col-2 text-center align-self-center"> + <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!searchTypeComponent.isValid()" (click)="emitAdd()"> <span class="fas fa-plus fa-fw"></span> </button> <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> <span class="fa fa-times fa-fw"></span> - </button> --> + </button> </div> </div> diff --git a/client/src/app/instance/search/components/criteria/criterion.component.ts b/client/src/app/instance/search/components/criteria/criterion.component.ts index fc2f97fd5ae975d3703e00272d0472b92f49b4f1..6145ccb5a8bd5f777ad765cec2fe1efa35d61f80 100644 --- a/client/src/app/instance/search/components/criteria/criterion.component.ts +++ b/client/src/app/instance/search/components/criteria/criterion.component.ts @@ -7,30 +7,54 @@ * file that was distributed with this source code. */ -import { Component, Input, Output, EventEmitter, ViewChild, ElementRef, ContentChild } from '@angular/core'; +import { Component, Input, Output, EventEmitter, ViewChild, SimpleChanges, OnInit, OnChanges } from '@angular/core'; import { Attribute } from 'src/app/metamodel/models'; import { Criterion } from 'src/app/instance/store/models'; -import { SearchTypeLoaderDirective } from './search-type-loader.directive'; -import { AbstractSearchTypeComponent } from './abstract-search-type.component'; -import { TestSearchTypeComponent } from './test-search-type.component'; +import { SearchTypeLoaderDirective } from './search-type/search-type-loader.directive'; +import { AbstractSearchTypeComponent } from './search-type/abstract-search-type.component'; import { FieldComponent } from './search-type/field.component'; +import { BetweenComponent } from './search-type/between.component'; +import { SelectComponent } from './search-type/select.component'; +import { SelectMultipleComponent } from './search-type/select-multiple.component'; +import { DatalistComponent } from './search-type/datalist.component'; +import { ListComponent } from './search-type/list.component'; +import { RadioComponent } from './search-type/radio.component'; +import { CheckboxComponent } from './search-type/checkbox.component'; +import { BetweenDateComponent } from './search-type/between-date.component'; +import { DateComponent } from './search-type/date.component'; +import { TimeComponent } from './search-type/time.component'; +import { DateTimeComponent } from './search-type/datetime.component'; +import { JsonComponent } from './search-type/json.component'; @Component({ selector: 'app-criterion', templateUrl: 'criterion.component.html' }) -export class CriterionComponent { +export class CriterionComponent implements OnInit, OnChanges { @Input() attribute: Attribute; @Input() criterion: Criterion; + @Input() criteriaList: Criterion[]; @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - @ViewChild(SearchTypeLoaderDirective, {static: true}) searchType!: SearchTypeLoaderDirective; + @ViewChild(SearchTypeLoaderDirective, {static: true}) SearchTypeLoaderDirective!: SearchTypeLoaderDirective; + + public searchTypeComponent: AbstractSearchTypeComponent; ngOnInit() { - const componentRef = this.searchType.viewContainerRef.createComponent<AbstractSearchTypeComponent>(TestSearchTypeComponent); - componentRef.instance.data = { text: 'Bonjour Yannick !' }; + const componentRef = this.SearchTypeLoaderDirective.viewContainerRef.createComponent<AbstractSearchTypeComponent>(this.getSearchTypeComponent()); + console.log(componentRef.instance); + componentRef.instance.setAttribute(this.attribute); + componentRef.instance.setCriterion(this.criterion); + componentRef.instance.setCriteriaList(this.criteriaList); + this.searchTypeComponent = componentRef.instance; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.criterion && !changes.criterion.firstChange) { + this.searchTypeComponent.setCriterion(changes.criterion.currentValue); + } } /** @@ -38,7 +62,70 @@ export class CriterionComponent { * * @fires EventEmitter<Criterion> */ - emitAdd(criterion: Criterion): void { - this.addCriterion.emit(criterion); + emitAdd(): void { + this.addCriterion.emit(this.searchTypeComponent.getCriterion()); + } + + getSearchTypeComponent() { + let nameOfSearchTypeComponent = null; + switch(this.attribute.search_type) { + case 'field': { + nameOfSearchTypeComponent = FieldComponent; + break; + } + case 'between': { + nameOfSearchTypeComponent = BetweenComponent; + break; + } + case 'select': { + nameOfSearchTypeComponent = SelectComponent; + break; + } + case 'select-multiple': { + nameOfSearchTypeComponent = SelectMultipleComponent; + break; + } + case 'datalist': { + nameOfSearchTypeComponent = DatalistComponent; + break; + } + case 'list': { + nameOfSearchTypeComponent = ListComponent; + break; + } + case 'radio': { + nameOfSearchTypeComponent = RadioComponent; + break; + } + case 'checkbox': { + nameOfSearchTypeComponent = CheckboxComponent; + break; + } + case 'between-date': { + nameOfSearchTypeComponent = BetweenDateComponent; + break; + } + case 'date': { + nameOfSearchTypeComponent = DateComponent; + break; + } + case 'time': { + nameOfSearchTypeComponent = TimeComponent; + break; + } + case 'date-time': { + nameOfSearchTypeComponent = DateTimeComponent; + break; + } + case 'json': { + nameOfSearchTypeComponent = JsonComponent; + break; + } + default: { + nameOfSearchTypeComponent = null; + break; + } + } + return nameOfSearchTypeComponent; } } diff --git a/client/src/app/instance/search/components/criteria/index.ts b/client/src/app/instance/search/components/criteria/index.ts index 7dd250f7a445fe5837f101d14ec3f25ee4fe10b5..384fec9fedf22ed74f2127a6d7272fbee81c43df 100644 --- a/client/src/app/instance/search/components/criteria/index.ts +++ b/client/src/app/instance/search/components/criteria/index.ts @@ -1,8 +1,6 @@ import { ConeSearchTabComponent } from './cone-search-tab.component'; import { CriteriaTabsComponent } from './criteria-tabs.component'; import { CriteriaByFamilyComponent } from './criteria-by-family.component'; -import { SearchTypeLoaderDirective } from './search-type-loader.directive'; -import { TestSearchTypeComponent } from './test-search-type.component'; import { CriterionComponent } from './criterion.component'; import { searchTypeComponents } from './search-type'; @@ -10,8 +8,6 @@ export const criteriaComponents = [ ConeSearchTabComponent, CriteriaTabsComponent, CriteriaByFamilyComponent, - SearchTypeLoaderDirective, - TestSearchTypeComponent, CriterionComponent, searchTypeComponents ]; diff --git a/client/src/app/instance/search/components/criteria/search-type/abstract-search-type.component.ts b/client/src/app/instance/search/components/criteria/search-type/abstract-search-type.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..adfd77078836e4d919a62d7a79202f184f87bbb6 --- /dev/null +++ b/client/src/app/instance/search/components/criteria/search-type/abstract-search-type.component.ts @@ -0,0 +1,55 @@ +import { Directive, Input } from '@angular/core'; +import { FormGroup } from '@angular/forms'; + +import { Attribute } from 'src/app/metamodel/models'; +import { Criterion } from 'src/app/instance/store/models'; +import { searchTypeOperators } from 'src/app/shared/utils'; + +@Directive() +export abstract class AbstractSearchTypeComponent { + attribute: Attribute; + criteriaList: Criterion[]; + + form: FormGroup; + operators = searchTypeOperators; + + constructor() { } + + setAttribute(attribute: Attribute) { + this.attribute = attribute; + } + + setCriterion(criterion: Criterion) { + if (criterion) { + this.form.patchValue(criterion); + this.form.disable(); + } else { + this.form.enable(); + this.form.reset(); + } + } + + setCriteriaList(criteriaList: Criterion[]) { + this.criteriaList = criteriaList; + } + + abstract getCriterion(): Criterion; + + isValid() { + return this.form.valid; + } + + /** + * Return field type. + * + * @return string + */ + getType(): string { + const numberTypeList = ['smallint', 'integer', 'decimal', 'float']; + if (this.attribute.operator === 'in' || this.attribute.operator === 'nin' || !numberTypeList.includes(this.attribute.type)) { + return 'text'; + } else { + return 'number'; + } + } +} diff --git a/client/src/app/instance/search/components/criteria/search-type/between-date.component.html b/client/src/app/instance/search/components/criteria/search-type/between-date.component.html index 0b10358d0d29c2d9f72afe7c1c218d17dc3d0159..a0f849148797c06e26c69c89b936836ae7e82028 100644 --- a/client/src/app/instance/search/components/criteria/search-type/between-date.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/between-date.component.html @@ -1,33 +1,18 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> - <div class="readonly">bw</div> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <input type="text" - placeholder="Pick a date range..." - class="form-control" - formControlName="dateRange" - [bsValue]="form.controls.dateRange.value" - [bsConfig]="{ rangeInputFormat: 'YYYY-MM-DD', isAnimated: true }" - autocomplete="off" - bsDaterangepicker> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> + <div class="operator_readonly">bw</div> </div> - <div class="col-2 text-center align-self-end mb-sm-1 pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.valid" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <input type="text" + placeholder="Pick a date range..." + class="form-control" + formControlName="dateRange" + [bsValue]="form.controls.dateRange.value" + [bsConfig]="{ rangeInputFormat: 'YYYY-MM-DD', isAnimated: true }" + autocomplete="off" + bsDaterangepicker> </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts deleted file mode 100644 index a0dceaf5a411fd258544594c0bc211f3239d4b9d..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/between-date.component.spec.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { BetweenDateComponent } from './between-date.component'; -import { BetweenCriterion } from '../../../../store/models/criterion'; -import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; - -describe('[Instance][Search][Component][Criteria][SearchType] BetweenDateComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-between-date - [id]="id" - [operator]="operator" - [label]="label" - [criterion]="criterion"> - </app-between-date>` - }) - class TestHostComponent { - @ViewChild(BetweenDateComponent, { static: false }) - public testedComponent: BetweenDateComponent; - public id: number = undefined; - public operator: string = undefined; - public label: string = undefined; - public criterion: BetweenCriterion = undefined; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: BetweenDateComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - BetweenDateComponent, - TestHostComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - BsDatepickerModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'between', operator: 'eq', min: '2019-02-17', max: '2021-04-12' } as BetweenCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.dateRange.value[0].getDate()).toEqual(17); - expect(testedComponent.form.controls.dateRange.value[0].getMonth()).toEqual(1); - expect(testedComponent.form.controls.dateRange.value[0].getFullYear()).toEqual(2019); - expect(testedComponent.form.controls.dateRange.value[1].getDate()).toEqual(12); - expect(testedComponent.form.controls.dateRange.value[1].getMonth()).toEqual(3); - expect(testedComponent.form.controls.dateRange.value[1].getFullYear()).toEqual(2021); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.dateRange.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - const dateMin: Date = new Date('2019-02-17'); - const dateMax: Date = new Date('2021-04-12'); - testedComponent.form.controls.dateRange.setValue([dateMin, dateMax]); - const expectedCriterion = { id: testedComponent.id, type: 'between', operator, min: '2019-02-17', max: '2021-04-12' } as BetweenCriterion; - testedComponent.addCriterion.subscribe((event: BetweenCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#getDateString() should return a date as string', () => { - const dateString = '2019-02-17'; - const date = new Date(dateString); - expect(testedComponent.getDateString(date)).toEqual(dateString); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/between-date.component.ts b/client/src/app/instance/search/components/criteria/search-type/between-date.component.ts index cbd754df9e5cef5115f689476c76e78efc3ec906..f90a2cb2d658fda3141dc57cda5bc440bf4e8a9c 100644 --- a/client/src/app/instance/search/components/criteria/search-type/between-date.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/between-date.component.ts @@ -1,71 +1,47 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { - Component, - Input, - Output, - EventEmitter, - ChangeDetectionStrategy, - SimpleChanges, - OnChanges -} from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, BetweenCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -/** - * @class - * @classdesc Between date search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-between-date', - templateUrl: 'between-date.component.html', - styleUrls: [ 'operator.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'between-date.component.html' }) -export class BetweenDateComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<BetweenCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - dateRange: new FormControl('', [Validators.required]) - }); - - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const c = changes.criterion.currentValue as BetweenCriterion; - this.form.controls.dateRange.setValue([new Date(c.min), new Date(c.max)]); - this.form.disable(); - } +export class BetweenDateComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + dateRange: new FormControl('', [Validators.required]) + }); + } - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + const betweenCriterion = criterion as BetweenCriterion; + this.form.controls.dateRange.setValue([ + new Date(betweenCriterion.min), + new Date(betweenCriterion.max) + ]); } } - + /** - * Emits event to add criterion to the criteria list. + * Return new criterion * - * @fires EventEmitter<BetweenCriterion> + * @return Criterion */ - emitAdd(): void { + getCriterion(): Criterion { const dateMin = this.getDateString(this.form.controls.dateRange.value[0]); const dateMax = this.getDateString(this.form.controls.dateRange.value[1]); - const fd: BetweenCriterion = { id: this.attribute.id, type: 'between', min: dateMin, max: dateMax }; - this.addCriterion.emit(fd); + + return { + id: this.attribute.id, + type: 'between', + min: dateMin, + max: dateMax + } as BetweenCriterion; } /** diff --git a/client/src/app/instance/search/components/criteria/search-type/between.component.html b/client/src/app/instance/search/components/criteria/search-type/between.component.html index 526d2bfd2a742464e36847ccca4da1cb155d0b13..ae9e68b658b98c544906909461875c9c14e3e984 100644 --- a/client/src/app/instance/search/components/criteria/search-type/between.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/between.component.html @@ -1,34 +1,19 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-3 col-lg-auto pr-sm-1 mb-1 mb-lg-0"> - <div class="readonly">min</div> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1 mb-1 mb-sm-0"> - <input type="text" class="form-control" [placeholder]="getPlaceholderMin()" formControlName="min" autocomplete="off" /> - </div> - <div class="w-100 d-block d-lg-none"></div> - <div class="col col-sm-3 col-lg-auto pr-sm-1 mb-1"> - <div class="readonly">max</div> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <input type="text" class="form-control" [placeholder]="getPlaceholderMax()" formControlName="max" autocomplete="off" /> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-3 col-lg-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">min</div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.controls.min.value && !form.controls.max.value" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1 mb-1 mb-sm-0"> + <input type="text" class="form-control" [placeholder]="getPlaceholderMin()" formControlName="min" autocomplete="off" /> + </div> + <div class="w-100 d-block d-lg-none"></div> + <div class="col col-sm-3 col-lg-auto pr-sm-1 mb-1"> + <div class="operator_readonly">max</div> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <input type="text" class="form-control" [placeholder]="getPlaceholderMax()" formControlName="max" autocomplete="off" /> </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts deleted file mode 100644 index 2cf50ce7679f38dd0d5a6437011cbf222f8b8d99..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/between.component.spec.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { TooltipModule } from 'ngx-bootstrap/tooltip'; - -import { BetweenComponent } from './between.component'; -import { BetweenCriterion } from '../../../../store/models/criterion'; - -describe('[Instance][Search][Component][Criteria][SearchType] BetweenComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-between - [id]="id" - [label]="label" - [placeholderMin]="placeholderMin" - [placeholderMax]="placeholderMax" - [criterion]="criterion"> - </app-between>` - }) - class TestHostComponent { - @ViewChild(BetweenComponent, { static: false }) - public testedComponent: BetweenComponent; - public id: number = undefined; - public label: string = undefined; - public placeholderMin: string = undefined; - public placeholderMax: string = undefined; - public criterion: BetweenCriterion = undefined; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - @Component({ selector: 'app-help-like', template: '' }) - class HelpLikeStubComponent { } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: BetweenComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - BetweenComponent, - TestHostComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - TooltipModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'between', operator: 'eq', min: 'one', max: 'two' } as BetweenCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.min.value).toEqual('one'); - expect(testedComponent.form.controls.max.value).toEqual('two'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.min.value).toBeNull(); - expect(testedComponent.form.controls.max.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.form.controls.min.setValue('one'); - testedComponent.form.controls.max.setValue('two'); - const expectedCriterion = { id: testedComponent.id, type: 'between', operator, min: 'one', max: 'two' } as BetweenCriterion; - testedComponent.addCriterion.subscribe((event: BetweenCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#getPlaceholderMin() should fill the placeholder for the minimum value if defined', () => { - const placeholder = 'placeholder'; - testedComponent.placeholderMin = placeholder; - expect(testedComponent.getPlaceholderMin()).toEqual(placeholder); - }); - - it('#getPlaceholderMin() should not fill the placeholder for the minimum value if not defined', () => { - expect(testedComponent.getPlaceholderMin()).toEqual(''); - }); - - it('#getPlaceholderMax() should fill the placeholder for the maximum value if defined', () => { - const placeholder = 'placeholder'; - testedComponent.placeholderMax = placeholder; - expect(testedComponent.getPlaceholderMax()).toEqual(placeholder); - }); - - it('#getPlaceholderMax() should not fill the placeholder for the maximum value if not defined', () => { - expect(testedComponent.getPlaceholderMax()).toEqual(''); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/between.component.ts b/client/src/app/instance/search/components/criteria/search-type/between.component.ts index 244d30b489e5b315b70f556fc8e1f2f26bd58110..c111105c04db07aedbf6ccda7faacfbb4e386a3c 100644 --- a/client/src/app/instance/search/components/criteria/search-type/between.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/between.component.ts @@ -1,60 +1,33 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, BetweenCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -/** - * @class - * @classdesc Between search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-between', - templateUrl: 'between.component.html', - styleUrls: ['operator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'between.component.html' }) -export class BetweenComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<BetweenCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - min: new FormControl('', [Validators.required]), - max: new FormControl('', [Validators.required]) - }); - - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.patchValue(this.criterion); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); - } +export class BetweenComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + min: new FormControl('', [Validators.required]), + max: new FormControl('', [Validators.required]) + }); } /** - * Emits event to add criterion to the criteria list. + * Return new criterion * - * @fires EventEmitter<BetweenCriterion> + * @return Criterion */ - emitAdd(): void { - this.addCriterion.emit({id: this.attribute.id, type: 'between', ...this.form.value}); + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'between', + ...this.form.value + } as BetweenCriterion; } /** @@ -66,7 +39,7 @@ export class BetweenComponent implements OnChanges { if (!this.attribute.placeholder_min) { return ''; } else { - return this.attribute.placeholder_max; + return this.attribute.placeholder_min; } } @@ -82,4 +55,8 @@ export class BetweenComponent implements OnChanges { return this.attribute.placeholder_max; } } + + isValid() { + return this.form.controls.min.value || this.form.controls.max.value; + } } diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.html b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.html index 46a29878a32bdc7424b54732233df9880df3c7da..9cae6f77eda95db993c576f0934e4610adc07ed1 100644 --- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.html @@ -1,29 +1,14 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-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" formArrayName="checkboxes"> - <div *ngFor="let _ of getCheckboxes().controls; index as i" class="custom-control custom-checkbox form-check form-check-inline form-control-lg"> - <input class="custom-control-input" type="checkbox" id="cb_{{attribute.options[i].value}}" [formControlName]="i"> - <label class="custom-control-label" for="cb_{{attribute.options[i].value}}">{{ attribute.options[i].label }}</label> - </div> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">in</div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button *ngIf="!form.disabled" class="btn btn-outline-success" [hidden]="!isChecked()" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button *ngIf="form.disabled" class="btn btn-outline-danger" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1" formArrayName="checkboxes"> + <div *ngFor="let _ of getCheckboxes().controls; index as i" class="custom-control custom-checkbox form-check form-check-inline form-control-lg"> + <input class="custom-control-input" type="checkbox" id="cb_{{attribute.options[i].value}}" [formControlName]="i"> + <label class="custom-control-label" for="cb_{{attribute.options[i].value}}">{{ attribute.options[i].label }}</label> + </div> </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.scss b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.scss index 6b7fac9718ce69ef27a51d714dc78570e669c383..850526ebd59e8bae4f44b684f4e69c8e8cb1cc2f 100644 --- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.scss +++ b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.scss @@ -24,4 +24,4 @@ top: .8rem; width: 1.25rem; height: 1.25rem; -} \ No newline at end of file +} diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts deleted file mode 100644 index 98d1cc28f5126b03d46103efc14e043415d380cc..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormArray, FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { CheckboxComponent } from './checkbox.component'; -import { SelectMultipleCriterion } from '../../../../store/models/criterion'; -import { Option } from '../../../../../metamodel/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] CheckboxComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-checkbox - [id]="id" - [label]="label" - [options]="options" - [criterion]="criterion"> - </app-checkbox>` - }) - class TestHostComponent { - @ViewChild(CheckboxComponent, { static: false }) - public testedComponent: CheckboxComponent; - public id: number = undefined; - public label: string = undefined; - public options: Option[] = []; - public criterion: SelectMultipleCriterion = undefined; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: CheckboxComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - CheckboxComponent, - 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('should execute ngOnInit lifecycle', () => { - // testedComponent.options = [ - // { label: 'One', value: 'one', display: 1 }, - // { label: 'Two', value: 'two', display: 2 }, - // { label: 'Three', value: 'three', display: 3 } - // ]; - // testedComponent.ngOnInit(); - // console.log(testedComponent.options.length); - // const formArray = testedComponent.form.controls.checkboxes as FormArray; - // // console.log(formArray.controls[0]); - // expect(testedComponent).toBeTruthy(); - // }); - - // it('should call ngOnChanges and apply changes', () => { - // const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - // const options: Option[] = [ - // { label: 'One', value: 'one', display: 1 }, - // { label: 'Two', value: 'two', display: 2 }, - // { label: 'Three', value: 'three', display: 3 } - // ]; - // testHostComponent.criterion = { id: 1, type: 'multiple', options } as SelectMultipleCriterion; - // testHostFixture.detectChanges(); - // expect(testedComponent.form.controls.select.value).toEqual(['one', 'two', 'three']); - // expect(testedComponent.form.disabled).toBeTruthy(); - // testHostComponent.criterion = undefined; - // testHostFixture.detectChanges(); - // expect(testedComponent.form.controls.select.value).toBeNull(); - // expect(testedComponent.form.enabled).toBeTruthy(); - // expect(spy).toHaveBeenCalledTimes(2); - // }); - - // it('#getCheckboxes() should return an array of checkbox', () => { - // const formArray: FormArray = null; - // expect(testedComponent.getCheckboxes()).toBeTruthy(); - // }); - - // it('raises the add criterion event when clicked', () => { - // testedComponent.id = 1; - // testedComponent.options = [ - // { label: 'One', value: 'one', display: 1 }, - // { label: 'Two', value: 'two', display: 2 }, - // { label: 'Three', value: 'three', display: 3 } - // ]; - // testedComponent.form.controls.select.setValue(['three']); - // const expectedValue = [{ label: 'Three', value: 'three', display: 3 }]; - // const expectedCriterion = { id: testedComponent.id, type: 'multiple', options: expectedValue } as SelectMultipleCriterion; - // testedComponent.addCriterion.subscribe((event: SelectMultipleCriterion) => expect(event).toEqual(expectedCriterion)); - // testedComponent.emitAdd(); - // }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.ts b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.ts index a12f43287745025bf1a29824eb377799b2b49e69..8d9b1b070290fb878fc4a34812982b8464711981 100644 --- a/client/src/app/instance/search/components/criteria/search-type/checkbox.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/checkbox.component.ts @@ -1,64 +1,62 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; -import { FormGroup, FormArray, FormControl } from '@angular/forms'; +import { Component } from '@angular/core'; +import { FormGroup, FormControl, FormArray, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, SelectMultipleCriterion } from 'src/app/instance/store/models'; import { Attribute } from 'src/app/metamodel/models'; -/** - * @class - * @classdesc Checkbox search type component. - * - * @implements OnInit - * @implements OnChanges - */ @Component({ selector: 'app-checkbox', templateUrl: 'checkbox.component.html', - styleUrls: ['checkbox.component.scss', 'operator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + styleUrls: ['checkbox.component.scss' ], }) -export class CheckboxComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<SelectMultipleCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ }); +export class CheckboxComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({}); + } - ngOnInit(): void { + setAttribute(attribute: Attribute): void { + super.setAttribute(attribute); // Initialization of checkboxes (1 per option) const formControls: FormControl[] = []; - for (let i = 0; i < this.attribute.options.length; i++) { + for (let i = 0; i < attribute.options.length; i++) { formControls.push(new FormControl()); } this.form.addControl('checkboxes', new FormArray(formControls)); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const multipleCriterion = this.criterion as SelectMultipleCriterion; - + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { for (let i = 0; i < this.attribute.options.length; i++) { - if (multipleCriterion.options.find(o => o.label === this.attribute.options[i].label)) { + if ((criterion as SelectMultipleCriterion).options.find(o => o.label === this.attribute.options[i].label)) { this.getCheckboxes().controls[i].setValue(true); } } - this.form.disable(); } + } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + const selected = this.getCheckboxes().value; + const values = [...this.attribute.options.filter((option, index) => selected[index])]; - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); - } + return { + id: this.attribute.id, + type: 'multiple', + options: values + } as SelectMultipleCriterion; + } + + isValid(): boolean { + const selected = this.getCheckboxes().value; + const values = [...this.attribute.options.filter((option, index) => selected[index])]; + return values.length > 0; } /** @@ -70,17 +68,6 @@ export class CheckboxComponent implements OnInit, OnChanges { return this.form.controls.checkboxes as FormArray; } - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<SelectMultipleCriterion> - */ - emitAdd(): void { - const selected = this.getCheckboxes().value; - const values = [...this.attribute.options.filter((option, index) => selected[index])]; - this.addCriterion.emit({ id: this.attribute.id, type: 'multiple', options: values }); - } - /** * Checks if one of the checkboxes is checked. * diff --git a/client/src/app/instance/search/components/criteria/search-type/datalist.component.html b/client/src/app/instance/search/components/criteria/search-type/datalist.component.html index dedc999a702689cbca447febcc721fe6e008b94d..4af8b445c943b4532fabf34eb41de654924ed231 100644 --- a/client/src/app/instance/search/components/criteria/search-type/datalist.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/datalist.component.html @@ -1,43 +1,21 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <span *ngIf="attribute.operator === 'lk'" class="pl-1" [tooltip]="helpLike" placement="right" containerClass="custom-tooltip right-tooltip"> - <span class="far fa-question-circle fa-sm"></span> - </span> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <input [attr.list]="getDatalistId()" - type="text" - class="form-control" - [placeholder]="getPlaceholder()" - formControlName="value" - autocomplete="off" /> - <datalist [id]="getDatalistId()"> - <option *ngFor="let option of attribute.options" [value]="option.value"> - </datalist> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <input [attr.list]="getDatalistId()" + type="text" + class="form-control" + [placeholder]="getPlaceholder()" + formControlName="value" + autocomplete="off" /> + <datalist [id]="getDatalistId()"> + <option *ngFor="let option of attribute.options" [value]="option.value"> + </datalist> </div> </div> </form> - -<ng-template #helpLike> - <app-help-like></app-help-like> -</ng-template> \ No newline at end of file diff --git a/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts deleted file mode 100644 index 4167d6071d4045c88b9cab73c7b3f9eedfc594af..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/datalist.component.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { TooltipModule } from 'ngx-bootstrap/tooltip'; - -import { DatalistComponent } from './datalist.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; -import { Option } from '../../../../../metamodel/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] DatalistComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-datalist - [id]="id" - [operator]="operator" - [label]="label" - [placeholder]="placeholder" - [options]="options" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-datalist>` - }) - class TestHostComponent { - @ViewChild(DatalistComponent, { static: false }) - public testedComponent: DatalistComponent; - public id: number = undefined; - public operator: string = undefined; - public label: string = undefined; - public placeholder: string = undefined; - public options: Option[] = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - @Component({ selector: 'app-help-like', template: '' }) - class HelpLikeStubComponent { } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: DatalistComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - DatalistComponent, - TestHostComponent, - OperatorStubComponent, - HelpLikeStubComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - TooltipModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: 'myValue' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.value.value).toEqual('myValue'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.value.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - testedComponent.form.controls.value.setValue('myValue'); - const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: 'myValue' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#getPlaceholder() should fill the placeholder if defined', () => { - const placeholder = 'placeholder'; - testedComponent.placeholder = placeholder; - expect(testedComponent.getPlaceholder()).toEqual(placeholder); - }); - - it('#getPlaceholder() should not fill the placeholder if not defined', () => { - expect(testedComponent.getPlaceholder()).toEqual(''); - }); - - it('#getDatalistId() should return an id', () => { - testedComponent.id = 1; - expect(testedComponent.getDatalistId()).toEqual('datalist_' + 1); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/datalist.component.ts b/client/src/app/instance/search/components/criteria/search-type/datalist.component.ts index 2e693faaa9d552555d8164025b8e753aa646e0c9..f826281fbe9a762e2bc2af6db1ea9b39b88ffaba 100644 --- a/client/src/app/instance/search/components/criteria/search-type/datalist.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/datalist.component.ts @@ -1,58 +1,25 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, SimpleChanges, OnInit, OnChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Datalist search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-datalist', - templateUrl: 'datalist.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'datalist.component.html' }) -export class DatalistComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - value: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } +export class DatalistComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + value: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.patchValue(this.criterion); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (!criterion) { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -60,6 +27,23 @@ export class DatalistComponent implements OnInit, OnChanges { this.operatorOnChange(); } } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'field', + ...this.form.value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } /** * Modifies operator with the given one. @@ -73,16 +57,7 @@ export class DatalistComponent implements OnInit, OnChanges { } /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - this.addCriterion.emit({ id: this.attribute.id, type: 'field', ...this.form.value }); - } - - /** - * Returns placeholder. + * Return placeholder. * * @return string */ diff --git a/client/src/app/instance/search/components/criteria/search-type/date.component.html b/client/src/app/instance/search/components/criteria/search-type/date.component.html index 2c5b4ef2c400d3cc145ad9d39c2e3516ee8025a2..8c2b7950eb1be667647c3adcf67a680d9fd85f44 100644 --- a/client/src/app/instance/search/components/criteria/search-type/date.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/date.component.html @@ -1,34 +1,19 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <input type="text" - placeholder="Pick a date..." - class="form-control" - formControlName="date" - [bsValue]="form.controls.date.value" - [bsConfig]="{ dateInputFormat: 'YYYY-MM-DD', isAnimated: true }" - bsDatepicker> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <input type="text" + placeholder="Pick a date..." + class="form-control" + formControlName="date" + [bsValue]="form.controls.date.value" + [bsConfig]="{ dateInputFormat: 'YYYY-MM-DD', isAnimated: true }" + bsDatepicker> </div> </div> -</form> \ No newline at end of file +</form> diff --git a/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts deleted file mode 100644 index ee97c9214ca33d14151d8c4893b61cfc6a97be1c..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/date.component.spec.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { DateComponent } from './date.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; -import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; - -describe('[Instance][Search][Component][Criteria][SearchType] DateComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-date - [id]="id" - [operator]="operator" - [label]="label" - [placeholder]="placeholder" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-date>` - }) - class TestHostComponent { - @ViewChild(DateComponent, { static: false }) - public testedComponent: DateComponent; - public id: number = undefined; - public operator: string = undefined; - public label: string = undefined; - public placeholder: string = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: DateComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - DateComponent, - TestHostComponent, - OperatorStubComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - BsDatepickerModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: '2019-02-17' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.date.value.getDate()).toEqual(17); - expect(testedComponent.form.controls.date.value.getMonth()).toEqual(1); - expect(testedComponent.form.controls.date.value.getFullYear()).toEqual(2019); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.date.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - const date: Date = new Date('2019-02-17'); - testedComponent.form.controls.date.setValue(date); - const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: '2019-02-17' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#getPlaceholder() should fill the placeholder if defined', () => { - const placeholder = 'placeholder'; - testedComponent.placeholder = placeholder; - expect(testedComponent.getPlaceholder()).toEqual(placeholder); - }); - - it('#getPlaceholder() should not fill the placeholder if not defined', () => { - expect(testedComponent.getPlaceholder()).toEqual(''); - }); - - it('#getDateString() should return a date as string', () => { - const dateString = '2019-02-17'; - const date = new Date(dateString); - expect(testedComponent.getDateString(date)).toEqual(dateString); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/date.component.ts b/client/src/app/instance/search/components/criteria/search-type/date.component.ts index b80eb2158ae1ce130dafcf132a0f603da6e3ad62..903c0f5f20a3f37c8e138b449329368d77b22b14 100644 --- a/client/src/app/instance/search/components/criteria/search-type/date.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/date.component.ts @@ -1,60 +1,27 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Date search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-date', - templateUrl: 'date.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'date.component.html' }) -export class DateComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - date: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } +export class DateComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + date: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const criterion = changes.criterion.currentValue as FieldCriterion; - this.form.controls.operator.setValue(criterion.operator); - this.form.controls.date.setValue(new Date(criterion.value)); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + this.form.controls.date.setValue(new Date((criterion as FieldCriterion).value)); + } else { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -62,6 +29,29 @@ export class DateComponent implements OnInit, OnChanges { this.operatorOnChange(); } } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + let value = null; + if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { + value = this.getDateString(this.form.value.date) + } + + return { + id: this.attribute.id, + type: 'field', + operator: this.form.controls.operator.value, + value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } /** * Modifies operator with the given one. @@ -75,21 +65,7 @@ export class DateComponent implements OnInit, OnChanges { } /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - let value = null; - if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { - value = this.getDateString(this.form.value.date) - } - const fd = { id: this.attribute.id, type: 'field', operator: this.form.controls.operator.value, value }; - this.addCriterion.emit(fd); - } - - /** - * Returns placeholder. + * Return placeholder. * * @return string */ diff --git a/client/src/app/instance/search/components/criteria/search-type/datetime.component.html b/client/src/app/instance/search/components/criteria/search-type/datetime.component.html index 97b1912d60ea6b7a9ad4c98ecb17f2521212fe95..c199416c1c6805a4e6bd9820b685e8459f34f2a6 100644 --- a/client/src/app/instance/search/components/criteria/search-type/datetime.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/datetime.component.html @@ -1,50 +1,35 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1 mb-1 mb-lg-0 pr-lg-1"> + <input type="text" + placeholder="Pick a date..." + class="form-control" + formControlName="date" + [bsValue]="form.controls.date.value" + [bsConfig]="{ dateInputFormat: 'YYYY-MM-DD', isAnimated: true }" + bsDatepicker> + </div> + <div class="w-100 d-block d-lg-none"></div> + <div class="col col-lg-auto"> <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> + <div class="col-auto pl-lg-1 pr-1"> + <ng-select formControlName="hh" [multiple]="false" placeholder="HH..." class="ng-select-custom ng-select-time"> + <ng-option *ngFor="let hour of hours" [value]="hour">{{ hour }}</ng-option> + </ng-select> </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1 mb-1 mb-lg-0 pr-lg-1"> - <input type="text" - placeholder="Pick a date..." - class="form-control" - formControlName="date" - [bsValue]="form.controls.date.value" - [bsConfig]="{ dateInputFormat: 'YYYY-MM-DD', isAnimated: true }" - bsDatepicker> - </div> - <div class="w-100 d-block d-lg-none"></div> - <div class="col col-lg-auto"> - <div class="row"> - <div class="col-auto pl-lg-1 pr-1"> - <ng-select formControlName="hh" [multiple]="false" placeholder="HH..." class="ng-select-custom ng-select-time"> - <ng-option *ngFor="let hour of hours" [value]="hour">{{ hour }}</ng-option> - </ng-select> - </div> - <div class="col col-lg-auto p-0 text-center">:</div> - <div class="col-auto pl-1"> - <ng-select formControlName="mm" [multiple]="false" placeholder="MM..." class="ng-select-custom ng-select-time"> - <ng-option *ngFor="let min of minutes" [value]="min">{{ min }}</ng-option> - </ng-select> - </div> - </div> + <div class="col col-lg-auto p-0 text-center">:</div> + <div class="col-auto pl-1"> + <ng-select formControlName="mm" [multiple]="false" placeholder="MM..." class="ng-select-custom ng-select-time"> + <ng-option *ngFor="let min of minutes" [value]="min">{{ min }}</ng-option> + </ng-select> </div> </div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> - </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts deleted file mode 100644 index fa8f17a23e61118a784119974f2f5cf16a265b3e..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/datetime.component.spec.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { DatetimeComponent } from './datetime.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; -import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; -import { NgSelectModule } from '@ng-select/ng-select'; - -describe('[Instance][Search][Component][Criteria][SearchType] DatetimeComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-datetime - [id]="id" - [operator]="operator" - [label]="label" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-datetime>` - }) - class TestHostComponent { - @ViewChild(DatetimeComponent, { static: false }) - public testedComponent: DatetimeComponent; - public id: number = undefined; - public operator: string = undefined; - public label: string = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: DatetimeComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - DatetimeComponent, - TestHostComponent, - OperatorStubComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - NgSelectModule, - BsDatepickerModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: '2019-02-17 15:47' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.date.value.getDate()).toEqual(17); - expect(testedComponent.form.controls.date.value.getMonth()).toEqual(1); - expect(testedComponent.form.controls.date.value.getFullYear()).toEqual(2019); - expect(testedComponent.form.controls.hh.value).toEqual('15'); - expect(testedComponent.form.controls.mm.value).toEqual('47'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.date.value).toBeNull(); - expect(testedComponent.form.controls.hh.value).toBeNull(); - expect(testedComponent.form.controls.mm.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - const date: Date = new Date('2019-02-17'); - testedComponent.form.controls.date.setValue(date); - testedComponent.form.controls.hh.setValue('15'); - testedComponent.form.controls.mm.setValue('47'); - const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: '2019-02-17 15:47' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#initTime(t) should return an array of string with 2 digits from 0 to t', () => { - const n = 10; - expect(testedComponent.initTime(n).length).toEqual(n); - expect(testedComponent.initTime(n)[5]).toEqual('05'); - }); - - it('#getDateString() should return a date as string', () => { - const dateString = '2019-02-17'; - const date = new Date(dateString); - expect(testedComponent.getDateString(date)).toEqual(dateString); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/datetime.component.ts b/client/src/app/instance/search/components/criteria/search-type/datetime.component.ts index 6ee1b2f8de75c2ce60851532f3a2496611ac8968..f293941115a2f35ea8ee68423cabaf4d971f068f 100644 --- a/client/src/app/instance/search/components/criteria/search-type/datetime.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/datetime.component.ts @@ -1,70 +1,39 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Datetime search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-datetime', - templateUrl: 'datetime.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'datetime.component.html' }) -export class DatetimeComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - +export class DateTimeComponent extends AbstractSearchTypeComponent { hours: string[] = this.initTime(24); minutes: string[] = this.initTime(60); - public form = new FormGroup({ - operator: new FormControl(''), - date: new FormControl('', [Validators.required]), - hh: new FormControl('', [Validators.required]), - mm: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + date: new FormControl('', [Validators.required]), + hh: new FormControl('', [Validators.required]), + mm: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const criterion = changes.criterion.currentValue as FieldCriterion; - this.form.controls.operator.setValue(criterion.operator); - if (criterion.operator != 'nl' && criterion.operator != 'nnl') { - const [date, time] = criterion.value.split(' '); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + const fieldCriterion = criterion as FieldCriterion; + this.form.controls.operator.setValue(fieldCriterion.operator); + if (fieldCriterion.operator != 'nl' && fieldCriterion.operator != 'nnl') { + const [date, time] = fieldCriterion.value.split(' '); this.form.controls.date.setValue(new Date(date)); this.form.controls.hh.setValue(time.slice(0, 2)); this.form.controls.mm.setValue(time.slice(3, 5)); } - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + } else { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -72,6 +41,31 @@ export class DatetimeComponent implements OnInit, OnChanges { this.operatorOnChange(); } } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + let value = null; + if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { + const date = this.getDateString(this.form.value.date); + const time = `${this.form.value.hh}:${this.form.value.mm}`; + value = `${date} ${time}`; + } + + return { + id: this.attribute.id, + type: 'field', + operator: this.form.controls.operator.value, + value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } /** * Modifies operator with the given one. @@ -88,23 +82,6 @@ export class DatetimeComponent implements OnInit, OnChanges { } } - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - let value = null; - if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { - const date = this.getDateString(this.form.value.date); - const time = `${this.form.value.hh}:${this.form.value.mm}`; - value = `${date} ${time}`; - } - - const fd = {id: this.attribute.id, type: 'field', operator: this.form.controls.operator.value, value }; - this.addCriterion.emit(fd); - } - /** * Returns string array to represent the given time. * diff --git a/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts deleted file mode 100644 index 04076f228dbd6c3a30500425910a63755e36da4b..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/field.component.spec.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { TooltipModule } from 'ngx-bootstrap/tooltip'; - -import { FieldComponent } from './field.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; - -describe('[Instance][Search][Component][Criteria][SearchType] FieldComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-field - [id]="id" - [operator]="operator" - [label]="label" - [placeholder]="placeholder" - [attributeType]="attributeType" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-field>` - }) - class TestHostComponent { - @ViewChild(FieldComponent, { static: false }) - public testedComponent: FieldComponent; - public id: number = undefined; - public operator: string = undefined; - public label: string = undefined; - public placeholder: string = undefined; - public attributeType: string = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - @Component({ selector: 'app-help-like', template: '' }) - class HelpLikeStubComponent { } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: FieldComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - FieldComponent, - TestHostComponent, - OperatorStubComponent, - HelpLikeStubComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - TooltipModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: 'myValue' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.value.value).toEqual('myValue'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.value.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - testedComponent.form.controls.value.setValue('myValue'); - const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: 'myValue' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#getType() should return `number` if criterion is a number type', () => { - testedComponent.attributeType = 'smallint'; - expect(testedComponent.getType()).toEqual('number'); - testedComponent.attributeType = 'integer'; - expect(testedComponent.getType()).toEqual('number'); - testedComponent.attributeType = 'decimal'; - expect(testedComponent.getType()).toEqual('number'); - testedComponent.attributeType = 'float'; - expect(testedComponent.getType()).toEqual('number'); - }); - - it('#getType() should return `text` if criterion is not a number type', () => { - testedComponent.attributeType = 'char'; - expect(testedComponent.getType()).toEqual('text'); - }); - - it('#getPlaceholder() should fill the placeholder if defined', () => { - const placeholder = 'placeholder'; - testedComponent.placeholder = placeholder; - expect(testedComponent.getPlaceholder()).toEqual(placeholder); - }); - - it('#getPlaceholder() should not fill the placeholder if not defined', () => { - expect(testedComponent.getPlaceholder()).toEqual(''); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/field.component.ts b/client/src/app/instance/search/components/criteria/search-type/field.component.ts index 8565fe22405348e1c6e721f37ad0c0e8e7f2466e..4dfe994d230fa2c8062f54bc572c8d7771b27fec 100644 --- a/client/src/app/instance/search/components/criteria/search-type/field.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/field.component.ts @@ -1,67 +1,25 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { - Component, - Input, - Output, - EventEmitter, - ChangeDetectionStrategy, - SimpleChanges, - OnInit, - OnChanges -} from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { FieldCriterion, Criterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; +import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -/** - * @class - * @classdesc Field search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-field', - templateUrl: 'field.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: 'field.component.html' }) -export class FieldComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - value: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } +export class FieldComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + value: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.patchValue(this.criterion); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (!criterion) { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -69,38 +27,32 @@ export class FieldComponent implements OnInit, OnChanges { this.operatorOnChange(); } } - + /** - * Modifies operator with the given one. + * Return new criterion + * + * @return Criterion */ - operatorOnChange(): void { - if (this.form.controls.operator.value == 'nl' || this.form.controls.operator.value == 'nnl') { - this.form.controls.value.disable(); - } else { - this.form.controls.value.enable(); - } + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'field', + ...this.form.value + } as FieldCriterion; } - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - this.addCriterion.emit({ id: this.attribute.id, type: 'field', ...this.form.value }); + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; } /** - * Return field type. - * - * @return string + * Modifies operator with the given one. */ - getType(): string { - const numberTypeList = ['smallint', 'integer', 'decimal', 'float']; - if (this.attribute.operator === 'in' || this.attribute.operator === 'nin' || !numberTypeList.includes(this.attribute.type)) { - return 'text'; + operatorOnChange(): void { + if (this.form.controls.operator.value == 'nl' || this.form.controls.operator.value == 'nnl') { + this.form.controls.value.disable(); } else { - return 'number'; + this.form.controls.value.enable(); } } diff --git a/client/src/app/instance/search/components/criteria/search-type/help-like.component.html b/client/src/app/instance/search/components/criteria/search-type/help-like.component.html deleted file mode 100644 index fae1f66543a312b706a260a1c52d0bb892afc7b0..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/help-like.component.html +++ /dev/null @@ -1,11 +0,0 @@ -<div class="text-left"> - <div class="lead">Operators <span class="font-weight-bold">like</span> and <span class="font-weight-bold">not like</span></div> - <hr class="my-1"> - With <strong>like</strong> operator, you will search anything that look like your criterion. <br> - Ex: like <code>mac</code>, you will find anything that contains mac inside, like <code>bigmac</code>, <code>macbook</code>, <code>smack</code>...<br> - <br> - With <strong>not like</strong>, it's the opposite, you will find anything that not look like your criterion. <br> - Ex: not like <code>toto</code>, you will find anything that not contains toto inside, like <code>beer</code>, <code>crisps</code>, <code>friend</code>...<br> - <br> - <strong>Be careful: it's case sensitive.</strong> -</div> diff --git a/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts deleted file mode 100644 index bb1eb2c0c12dc402d9ca65f7c6bc8223bd936f73..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/help-like.component.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { HelpLikeComponent } from './help-like.component'; - -describe('[Instance][Search][Component][Criteria][SearchType] HelpLikeComponent', () => { - let component: HelpLikeComponent; - let fixture: ComponentFixture<HelpLikeComponent>; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [HelpLikeComponent] - }); - fixture = TestBed.createComponent(HelpLikeComponent); - component = fixture.componentInstance; - }); - - it('should create the component', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/help-like.component.ts b/client/src/app/instance/search/components/criteria/search-type/help-like.component.ts deleted file mode 100644 index 57598de55d2dac348413b3c91b9957d49b3b782b..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/help-like.component.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component } from '@angular/core'; - -/** - * @class - * @classdesc Help like operator component. - */ -@Component({ - selector: 'app-help-like', - templateUrl: 'help-like.component.html' -}) -export class HelpLikeComponent { } diff --git a/client/src/app/instance/search/components/criteria/search-type/index.ts b/client/src/app/instance/search/components/criteria/search-type/index.ts index 5954cfafcf1b43d37a9812c99fb99bb1358751a6..7150b01bafb61f26522f6b1ed9d517e4c1dade8d 100644 --- a/client/src/app/instance/search/components/criteria/search-type/index.ts +++ b/client/src/app/instance/search/components/criteria/search-type/index.ts @@ -1,3 +1,4 @@ +import { SearchTypeLoaderDirective } from './search-type-loader.directive'; import { FieldComponent } from './field.component'; import { BetweenComponent } from './between.component'; import { SelectComponent } from './select.component'; @@ -6,15 +7,14 @@ 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'; import { BetweenDateComponent } from './between-date.component'; +import { DateComponent } from './date.component'; import { TimeComponent } from './time.component'; -import { DatetimeComponent } from './datetime.component'; -import { JsonComponent } from './json.component'; -import { SvomJsonKwComponent } from './svom-json-kw.component'; -import { HelpLikeComponent } from './help-like.component'; +import { DateTimeComponent } from './datetime.component'; +import { JsonComponent }Â from './json.component'; export const searchTypeComponents = [ + SearchTypeLoaderDirective, FieldComponent, BetweenComponent, SelectComponent, @@ -23,11 +23,9 @@ export const searchTypeComponents = [ ListComponent, RadioComponent, CheckboxComponent, - DateComponent, BetweenDateComponent, + DateComponent, TimeComponent, - DatetimeComponent, - JsonComponent, - SvomJsonKwComponent, - HelpLikeComponent + DateTimeComponent, + JsonComponent ]; diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.html b/client/src/app/instance/search/components/criteria/search-type/json.component.html index 930fc5dc53510ff8c6fde4aeb675a3c3c8f60268..4a3923a18207afe5282e7b0ea69bcecbe5810bf7 100644 --- a/client/src/app/instance/search/components/criteria/search-type/json.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/json.component.html @@ -1,44 +1,29 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">json</div> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> - <div class="readonly">json</div> + <div class="col mb-1 mb-sm-0"> + <input class="form-control" id="path" name="path" placeholder="Path" autocomplete="off" formControlName="path"> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> + <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> + <ng-option value="eq">=</ng-option > + <ng-option value="gt">></ng-option > + <ng-option value="gte">>=</ng-option > + <ng-option value="lt"><</ng-option > + <ng-option value="lte"><=</ng-option > + </ng-select> </div> <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <div class="row"> - <div class="col mb-1 mb-sm-0"> - <input class="form-control" id="path" name="path" placeholder="Path" autocomplete="off" formControlName="path"> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> - <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> - <ng-option value="eq">=</ng-option > - <ng-option value="gt">></ng-option > - <ng-option value="gte">>=</ng-option > - <ng-option value="lt"><</ng-option > - <ng-option value="lte"><=</ng-option > - </ng-select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col"> - <input class="form-control" id="value" name="value" placeholder="Value" autocomplete="off" formControlName="value"> - </div> - </div> + <div class="col"> + <input class="form-control" id="value" name="value" placeholder="Value" autocomplete="off" formControlName="value"> </div> </div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.valid" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> - </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts deleted file mode 100644 index 673739f3f74f1f5c918828186d1bee27709d0c07..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/json.component.spec.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { NgSelectModule } from '@ng-select/ng-select'; - -import { JsonComponent } from './json.component'; -import { JsonCriterion } from '../../../../store/models/criterion'; - -describe('[Instance][Search][Component][Criteria][SearchType] JsonComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-json-criteria - [id]="id" - [label]="label" - [criterion]="criterion"> - </app-json-criteria>` - }) - class TestHostComponent { - @ViewChild(JsonComponent, { static: false }) - public testedComponent: JsonComponent; - public id: number = undefined; - public label: string = undefined; - public criterion: JsonCriterion = undefined; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: JsonComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - JsonComponent, - TestHostComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - NgSelectModule - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.path.value).toEqual('myPath'); - expect(testedComponent.form.controls.operator.value).toEqual('myOperator'); - expect(testedComponent.form.controls.value.value).toEqual('myValue'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.path.value).toBeNull(); - expect(testedComponent.form.controls.operator.value).toBeNull(); - expect(testedComponent.form.controls.value.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.form.controls.path.setValue('myPath'); - testedComponent.form.controls.operator.setValue('myOperator'); - testedComponent.form.controls.value.setValue('myValue'); - const expectedCriterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; - testedComponent.addCriterion.subscribe((event: JsonCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/json.component.ts b/client/src/app/instance/search/components/criteria/search-type/json.component.ts index 7e28f1d4b6c479ce40fe7c2250feca59be589cd1..5e0cdc5b49e0644d33ab34c773d1e752dc4af651 100644 --- a/client/src/app/instance/search/components/criteria/search-type/json.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/json.component.ts @@ -1,61 +1,33 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { JsonCriterion, Criterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; +import { Criterion, JsonCriterion } from 'src/app/instance/store/models'; -/** - * @class - * @classdesc JSON search type component. - * - * @implements OnChanges - */ @Component({ - selector: 'app-json-criteria', - templateUrl: 'json.component.html', - styleUrls: [ 'operator.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-json', + templateUrl: 'json.component.html' }) -export class JsonComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<JsonCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - path: new FormControl('', [Validators.required]), - operator: new FormControl('', [Validators.required]), - value: new FormControl('', [Validators.required]) - }); - - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.patchValue(changes.criterion.currentValue); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); - } +export class JsonComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + path: new FormControl('', [Validators.required]), + operator: new FormControl('', [Validators.required]), + value: new FormControl('', [Validators.required]) + }); } - + /** - * Emits event to add criterion to the criteria list. + * Return new criterion * - * @fires EventEmitter<JsonCriterion> + * @return Criterion */ - emitAdd(): void { - const js = { id: this.attribute.id, type: 'json', ...this.form.value }; - this.addCriterion.emit(js); + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'json', + ...this.form.value + } as JsonCriterion; } } diff --git a/client/src/app/instance/search/components/criteria/search-type/list.component.html b/client/src/app/instance/search/components/criteria/search-type/list.component.html index 62351c9f13f25dde74d75e74a40779abf6b63a02..259c3bd6f026f43675dc19de9252f81abafc5bba 100644 --- a/client/src/app/instance/search/components/criteria/search-type/list.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/list.component.html @@ -1,26 +1,11 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> - <div class="readonly">=</div> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <textarea class="form-control" rows="3" [placeholder]="getPlaceholder()" formControlName="list" autocomplete="off"></textarea> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">=</div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.valid" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <textarea class="form-control" rows="3" [placeholder]="getPlaceholder()" formControlName="list" autocomplete="off"></textarea> </div> </div> -</form> \ No newline at end of file +</form> diff --git a/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts deleted file mode 100644 index ea009ff97fffc95a0f6daa9090212ff192d258f5..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/list.component.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { ListComponent } from './list.component'; -import { ListCriterion } from '../../../../store/models/criterion'; - -describe('[Instance][Search][Component][Criteria][SearchType] ListComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-list - [id]="id" - [label]="label" - [placeholder]="placeholder" - [criterion]="criterion"> - </app-list>` - }) - class TestHostComponent { - @ViewChild(ListComponent, { static: false }) - public testedComponent: ListComponent; - public id: number = undefined; - public label: string = undefined; - public placeholder: string = undefined; - public criterion: ListCriterion = undefined; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: ListComponent; - - beforeEach(() => { - 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('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: testedComponent.id, type: 'list', values: ['1', '2'] } as ListCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.list.value).toEqual('1\n2'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.list.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#getPlaceholder() should fill the placeholder if defined', () => { - testedComponent.placeholder = 'placeholder'; - expect(testedComponent.getPlaceholder()).toEqual('placeholder'); - }); - - it('#getPlaceholder() should not fill the placeholder if not defined', () => { - expect(testedComponent.getPlaceholder()).toEqual(''); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.form.controls.list.setValue('1\n2'); - 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/client/src/app/instance/search/components/criteria/search-type/list.component.ts b/client/src/app/instance/search/components/criteria/search-type/list.component.ts index 77c32159cc450d40318202066aaa47021b84fd79..d065ccdc32db9a0cc686e33644f6c916275ede09 100644 --- a/client/src/app/instance/search/components/criteria/search-type/list.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/list.component.ts @@ -1,56 +1,44 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, SimpleChanges, OnChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; -import { ListCriterion, Criterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; +import { Criterion, ListCriterion } from 'src/app/instance/store/models'; -/** - * @class - * @classdesc List search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-list', - templateUrl: 'list.component.html', - styleUrls: ['operator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: 'list.component.html' }) -export class ListComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<ListCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - list: new FormControl('', [Validators.required]) - }); - - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const criterion = changes.criterion.currentValue as ListCriterion; +export class ListComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + list: new FormControl('', [Validators.required]) + }); + } - this.form.controls.list.setValue(criterion.values.join('\n')); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + this.form.controls.list.setValue((criterion as ListCriterion).values.join('\n')); this.form.disable(); } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); - } + } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'list', + values: this.form.value.list.split('\n') + } as ListCriterion; } /** - * Returns placeholder. + * Return placeholder. * * @return string */ @@ -61,14 +49,4 @@ export class ListComponent implements OnChanges { return this.attribute.placeholder_min; } } - - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<ListCriterion> - */ - emitAdd(): void { - const ls = { id: this.attribute.id, type: 'list', values: this.form.value.list.split('\n') }; - this.addCriterion.emit(ls); - } } diff --git a/client/src/app/instance/search/components/criteria/search-type/operator.component.scss b/client/src/app/instance/search/components/criteria/search-type/operator.component.scss deleted file mode 100644 index a4a0ae49ffd3de25f534641694bc346d6723ac91..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/operator.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -.readonly { - background-color: #e9ecef; - border: 1px solid #ced4da; - border-radius: .25rem; - display: block; - width: 100%; - height: calc(1.5em + .75rem + 2px); - padding: .375rem .75rem; - font-size: 1rem; - font-weight: 400; - line-height: 1.5; - color:#495057; -} \ No newline at end of file diff --git a/client/src/app/instance/search/components/criteria/search-type/radio.component.html b/client/src/app/instance/search/components/criteria/search-type/radio.component.html index 934827eef3973a10e4f2bf6f207d3a0a46e59720..77994fdbb0e8f944b13c27837233136a47a3dfbc 100644 --- a/client/src/app/instance/search/components/criteria/search-type/radio.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/radio.component.html @@ -1,31 +1,16 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <div *ngFor="let option of attribute.options" class="form-check form-check-inline"> - <input class="form-check-input" type="radio" id="cb_{{option.value}}" formControlName="radio" [value]="option.value"> - <label class="form-check-label" for="cb_{{option.value}}">{{ option.label }}</label> - </div> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <div *ngFor="let option of attribute.options" class="form-check form-check-inline"> + <input class="form-check-input" type="radio" id="cb_{{option.value}}" formControlName="radio" [value]="option.value"> + <label class="form-check-label" for="cb_{{option.value}}">{{ option.label }}</label> + </div> </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts deleted file mode 100644 index c6abb7b3a56111ef3aa113622958b7fed939998c..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/radio.component.spec.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { RadioComponent } from './radio.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; -import { Option } from '../../../../../metamodel/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] RadioComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-radio - [id]="id" - [label]="label" - [operator]="operator" - [options]="options" - [criterion]="criterion"> - </app-radio>` - }) - class TestHostComponent { - @ViewChild(RadioComponent, { static: false }) - public testedComponent: RadioComponent; - public id: number = undefined; - public label: string = undefined; - public operator: string = undefined; - public options: Option[] = undefined; - public criterion: FieldCriterion = undefined; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: RadioComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - RadioComponent, - 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('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: 'three' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.radio.value).toEqual('three'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.radio.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.operator = 'eq'; - testedComponent.form.controls.radio.setValue('three'); - testedComponent.options = [ - { label: 'One', value: 'one', display: 1 }, - { label: 'Two', value: 'two', display: 2 }, - { label: 'Three', value: 'three', display: 3 } - ]; - const expectedCriterion = { id: testedComponent.id, type: 'field', operator: 'eq', value: 'three' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/radio.component.ts b/client/src/app/instance/search/components/criteria/search-type/radio.component.ts index cdd24f18740e49d806b4afd9886381255c6cd56d..195fd921235604ddae7b07414b0bd900af088676 100644 --- a/client/src/app/instance/search/components/criteria/search-type/radio.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/radio.component.ts @@ -1,59 +1,27 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Radio search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-radio', - templateUrl: 'radio.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'radio.component.html' }) -export class RadioComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - radio: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } +export class RadioComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + radio: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const criterion = this.criterion as FieldCriterion; - this.form.controls.radio.setValue(criterion.value); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + this.form.controls.radio.setValue((criterion as FieldCriterion).value); + } else { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -61,6 +29,30 @@ export class RadioComponent implements OnInit, OnChanges { this.operatorOnChange(); } } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + let value = null; + if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { + const option = this.attribute.options.find(o => o.value === this.form.value.radio); + value = option.value; + } + + return { + id: this.attribute.id, + type: 'field', + operator: this.form.controls.operator.value, + value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } /** * Modifies operator with the given one. @@ -72,19 +64,4 @@ export class RadioComponent implements OnInit, OnChanges { this.form.controls.radio.enable(); } } - - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - let value = null; - if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { - const option = this.attribute.options.find(o => o.value === this.form.value.radio); - value = option.value; - } - const cb = {id: this.attribute.id, type: 'field', operator: this.form.controls.operator.value, value }; - this.addCriterion.emit(cb); - } } diff --git a/client/src/app/instance/search/components/criteria/search-type-loader.directive.ts b/client/src/app/instance/search/components/criteria/search-type/search-type-loader.directive.ts similarity index 100% rename from client/src/app/instance/search/components/criteria/search-type-loader.directive.ts rename to client/src/app/instance/search/components/criteria/search-type/search-type-loader.directive.ts diff --git a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.html b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.html index 35ef36a978be38b2ea9d96115a73836dade6d1c2..48de3a3d3a17c5636c59a4ae49005727e2e7a989 100644 --- a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.html @@ -1,28 +1,13 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-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"> - <ng-select formControlName="select" [multiple]="true" [hideSelected]="true" class="ng-select-custom"> - <ng-option *ngFor="let option of attribute.options" [value]="option.value">{{option.label}}</ng-option> - </ng-select> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">in</div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.valid" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <ng-select formControlName="select" [multiple]="true" [hideSelected]="true" class="ng-select-custom"> + <ng-option *ngFor="let option of attribute.options" [value]="option.value">{{option.label}}</ng-option> + </ng-select> </div> </div> -</form> \ No newline at end of file +</form> diff --git a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts deleted file mode 100644 index 9ad8f715d2a455559043a0e28a6912a6853b874d..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.spec.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { NgSelectModule } from '@ng-select/ng-select'; - -import { SelectMultipleComponent } from './select-multiple.component'; -import { SelectMultipleCriterion } from '../../../../store/models/criterion'; -import { Option } from '../../../../../metamodel/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] SelectMultipleComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-select-multiple - [id]="id" - [label]="label" - [options]="options" - [criterion]="criterion"> - </app-select-multiple>` - }) - class TestHostComponent { - @ViewChild(SelectMultipleComponent, { static: false }) - public testedComponent: SelectMultipleComponent; - public id: number = undefined; - public label: string = undefined; - public options: Option[] = undefined; - public criterion: SelectMultipleCriterion = undefined; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: SelectMultipleComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - SelectMultipleComponent, - TestHostComponent - ], - imports: [ - NgSelectModule, - FormsModule, - ReactiveFormsModule - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - const options: Option[] = [ - { label: 'One', value: 'one', display: 1 }, - { label: 'Two', value: 'two', display: 2 }, - { label: 'Three', value: 'three', display: 3 } - ]; - testHostComponent.criterion = { id: 1, type: 'multiple', options } as SelectMultipleCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.select.value).toEqual(['one', 'two', 'three']); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.select.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.options = [ - { label: 'One', value: 'one', display: 1 }, - { label: 'Two', value: 'two', display: 2 }, - { label: 'Three', value: 'three', display: 3 } - ]; - testedComponent.form.controls.select.setValue(['three']); - const expectedValue = [{ label: 'Three', value: 'three', display: 3 }]; - const expectedCriterion = { id: testedComponent.id, type: 'multiple', options: expectedValue } as SelectMultipleCriterion; - testedComponent.addCriterion.subscribe((event: SelectMultipleCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.ts b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.ts index d66ee9a957a1384f834ecb88bfdaddb28386c421..17c7b8d7e330453490055ad4197b1d49af2b297a 100644 --- a/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/select-multiple.component.ts @@ -1,65 +1,44 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, SelectMultipleCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -/** - * @class - * @classdesc Select multiple search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-select-multiple', - templateUrl: 'select-multiple.component.html', - styleUrls: ['operator.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'select-multiple.component.html' }) -export class SelectMultipleComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<SelectMultipleCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - select: new FormControl('', [Validators.required]) - }); +export class SelectMultipleComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + select: new FormControl('', [Validators.required]) + }); + } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const multipleCriterion = this.criterion as SelectMultipleCriterion; + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + const multipleCriterion = criterion as SelectMultipleCriterion; const values = multipleCriterion.options.map(option => option.value); this.form.controls.select.setValue(values); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); } } - + /** - * Emits event to add criterion to the criteria list. + * Return new criterion * - * @fires EventEmitter<SelectMultipleCriterion> + * @return Criterion */ - emitAdd(): void { + getCriterion(): Criterion { const values = this.form.value.select as string[]; - const options = this.attribute.options.filter(option => values.includes(option.value)); - const ms = { id: this.attribute.id, type: 'multiple', options }; - this.addCriterion.emit(ms); + + return { + id: this.attribute.id, + type: 'multiple', + options + } as SelectMultipleCriterion; } } diff --git a/client/src/app/instance/search/components/criteria/search-type/select.component.html b/client/src/app/instance/search/components/criteria/search-type/select.component.html index da79d656047719a8615a8e5b0bed6f10f86d5822..3e74403cdc522839caccafde2258b2e5e645097c 100644 --- a/client/src/app/instance/search/components/criteria/search-type/select.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/select.component.html @@ -1,37 +1,15 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <span *ngIf="attribute.operator === 'lk'" class="pl-1" [tooltip]="helpLike" placement="right" containerClass="custom-tooltip right-tooltip"> - <span class="far fa-question-circle fa-sm"></span> - </span> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <ng-select formControlName="select" [multiple]="false" [hideSelected]="true" class="ng-select-custom"> - <ng-option *ngFor="let option of attribute.options" [value]="option.value">{{option.label}}</ng-option> - </ng-select> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-sm-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> + <ng-select formControlName="select" [multiple]="false" [hideSelected]="true" class="ng-select-custom"> + <ng-option *ngFor="let option of attribute.options" [value]="option.value">{{option.label}}</ng-option> + </ng-select> </div> </div> </form> - -<ng-template #helpLike> - <app-help-like></app-help-like> -</ng-template> \ No newline at end of file diff --git a/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts deleted file mode 100644 index b4496d00f25b3d1efe1e88eaa6ca0968fa782b08..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/select.component.spec.ts +++ /dev/null @@ -1,108 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { NgSelectModule } from '@ng-select/ng-select'; -import { TooltipModule } from 'ngx-bootstrap/tooltip'; - -import { SelectComponent } from './select.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; -import { Option } from '../../../../../metamodel/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] SelectComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-select - [id]="id" - [label]="label" - [operator]="operator" - [options]="options" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-select>` - }) - class TestHostComponent { - @ViewChild(SelectComponent, { static: false }) - public testedComponent: SelectComponent; - public id: number = undefined; - public label: string = undefined; - public operator: string = undefined; - public options: Option[] = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - @Component({ selector: 'app-help-like', template: '' }) - class HelpLikeStubComponent { } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: SelectComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - SelectComponent, - TestHostComponent, - OperatorStubComponent, - HelpLikeStubComponent - ], - imports: [ - NgSelectModule, - FormsModule, - ReactiveFormsModule, - TooltipModule.forRoot() - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: 'three' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.select.value).toEqual('three'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.select.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.operator = 'eq'; - testedComponent.form.controls.select.setValue('three'); - testedComponent.options = [ - { label: 'One', value: 'one', display: 1 }, - { label: 'Two', value: 'two', display: 2 }, - { label: 'Three', value: 'three', display: 3 } - ]; - const expectedCriterion = { id: testedComponent.id, type: 'field', operator: 'eq', value: 'three' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/select.component.ts b/client/src/app/instance/search/components/criteria/search-type/select.component.ts index 3fa487334565ce082f4b2ca755f131a26d03e198..9b49b4c5c1cb76118713203ef65aaaf0ef6c196c 100644 --- a/client/src/app/instance/search/components/criteria/search-type/select.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/select.component.ts @@ -1,58 +1,27 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Select search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-select', - templateUrl: 'select.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'select.component.html' }) -export class SelectComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - select: new FormControl('', [Validators.required]) - }); - - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } +export class SelectComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + select: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.controls.select.setValue(changes.criterion.currentValue.value); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + this.form.controls.select.setValue((criterion as FieldCriterion).value); + } else { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -61,6 +30,29 @@ export class SelectComponent implements OnInit, OnChanges { } } + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + let value = null; + if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { + const option = this.attribute.options.find(o => o.value === this.form.value.select); + value = option.value; + } + return { + id: this.attribute.id, + type: 'field', + operator: this.form.controls.operator.value, + value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } + /** * Modifies operator with the given one. */ @@ -71,19 +63,4 @@ export class SelectComponent implements OnInit, OnChanges { this.form.controls.select.enable(); } } - - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - let value = null; - if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { - const option = this.attribute.options.find(o => o.value === this.form.value.select); - value = option.value; - } - const se = {id: this.attribute.id, type: 'field', operator: this.form.controls.operator.value, value }; - this.addCriterion.emit(se); - } } diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html index 26edbf9879ec6317bca480c58178dafbe23df283..482a50291107fe2c42f6063751789e1e1f16f70a 100644 --- a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.html @@ -1,47 +1,32 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <div class="operator_readonly">json</div> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col pl-sm-1"> <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> - <div class="readonly">json</div> + <div class="col mb-1 mb-sm-0"> + <input *ngIf="svomKeywords.length < 1" class="form-control" id="path" name="path" placeholder="Path" autocomplete="off" formControlName="path"> + <ng-select *ngIf="svomKeywords.length > 0" [multiple]="false" [hideSelected]="true" placeholder="Select svom product keyword" class="ng-select-custom" formControlName="path"> + <ng-option *ngFor="let svomKeyword of svomKeywords" [value]="getKeywordValue(svomKeyword)">{{ svomKeyword.extension }}/{{ svomKeyword.name }}</ng-option> + </ng-select> + </div> + <div class="w-100 d-block d-sm-none"></div> + <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> + <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> + <ng-option value="eq">=</ng-option > + <ng-option value="gt">></ng-option > + <ng-option value="gte">>=</ng-option > + <ng-option value="lt"><</ng-option > + <ng-option value="lte"><=</ng-option > + </ng-select> </div> <div class="w-100 d-block d-sm-none"></div> - <div class="col pl-sm-1"> - <div class="row"> - <div class="col mb-1 mb-sm-0"> - <input *ngIf="svomKeywords.length < 1" class="form-control" id="path" name="path" placeholder="Path" autocomplete="off" formControlName="path"> - <ng-select *ngIf="svomKeywords.length > 0" [multiple]="false" [hideSelected]="true" placeholder="Select svom product keyword" class="ng-select-custom" formControlName="path"> - <ng-option *ngFor="let svomKeyword of svomKeywords" [value]="getKeywordValue(svomKeyword)">{{ svomKeyword.extension }}/{{ svomKeyword.name }}</ng-option> - </ng-select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col col-sm-auto mb-1 mb-sm-0 px-sm-0"> - <ng-select [clearable]="false" [multiple]="false" [hideSelected]="true" class="ng-select-custom" formControlName="operator"> - <ng-option value="eq">=</ng-option > - <ng-option value="gt">></ng-option > - <ng-option value="gte">>=</ng-option > - <ng-option value="lt"><</ng-option > - <ng-option value="lte"><=</ng-option > - </ng-select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col"> - <input class="form-control" id="value" name="value" placeholder="Value" autocomplete="off" formControlName="value"> - </div> - </div> + <div class="col"> + <input class="form-control" id="value" name="value" placeholder="Value" autocomplete="off" formControlName="value"> </div> </div> </div> - <div class="col-2 text-center align-self-end pb-3"> - <button class="btn btn-outline-success" *ngIf="!form.disabled" [hidden]="!form.valid" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="form.disabled" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> - </div> </div> </form> diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts deleted file mode 100644 index 05f55eec0e209ff39dc6559c31db1c4b60a81554..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { Component, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { NgSelectModule } from '@ng-select/ng-select'; - -import { SvomJsonKwComponent } from './svom-json-kw.component'; -import { Criterion, JsonCriterion, SvomKeyword } from '../../../../store/models'; - -describe('[Instance][Search][Component][Criteria][SearchType] SvomJsonKwComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-svom-json-kw-criteria - [id]="id" - [label]="label" - [criterion]="criterion" - [criteriaList]="criteriaList" - [svomKeywords]="svomKeywords"> - </app-svom-json-kw-criteria >` - }) - class TestHostComponent { - @ViewChild(SvomJsonKwComponent, { static: false }) - public testedComponent: SvomJsonKwComponent; - public id: number = undefined; - public label: string = undefined; - public criterion: JsonCriterion = undefined; - public criteriaList: Criterion[] = []; - public svomKeywords: SvomKeyword[] = []; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: SvomJsonKwComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - SvomJsonKwComponent, - TestHostComponent - ], - imports: [ - FormsModule, - ReactiveFormsModule, - NgSelectModule - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.path.value).toEqual('myPath'); - expect(testedComponent.form.controls.operator.value).toEqual('myOperator'); - expect(testedComponent.form.controls.value.value).toEqual('myValue'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.path.value).toBeNull(); - expect(testedComponent.form.controls.operator.value).toBeNull(); - expect(testedComponent.form.controls.value.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - testedComponent.form.controls.path.setValue('myPath'); - testedComponent.form.controls.operator.setValue('myOperator'); - testedComponent.form.controls.value.setValue('myValue'); - const expectedCriterion = { id: testedComponent.id, type: 'json', path: 'myPath', operator: 'myOperator', value: 'myValue' } as JsonCriterion; - testedComponent.addCriterion.subscribe((event: JsonCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('Calculates the value of ng-option', () => { - expect(testedComponent.getKeywordValue({ - extension: 'PrimaryHDU', - name: 'CARD', - data_type: 'string', - default: '' - })).toEqual('PrimaryHDU,CARD'); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts index 7f77d22b8b3d9916190ae39cd805dff80e000709..6196a5157fa1d0893f0e5019dd82cccb894990a3 100644 --- a/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/svom-json-kw.component.ts @@ -1,77 +1,33 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; - -import { JsonCriterion, Criterion, SvomKeyword } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; + +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; +import { Criterion, JsonCriterion } from 'src/app/instance/store/models'; @Component({ - selector: 'app-svom-json-kw-criteria', - templateUrl: 'svom-json-kw.component.html', - styleUrls: [ 'operator.component.scss' ], - changeDetection: ChangeDetectionStrategy.OnPush + selector: 'app-svom-json-kw', + templateUrl: 'svom-json-kw.component.html' }) -export class SvomJsonKwComponent implements OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Input() criteriaList: Criterion[]; - @Input() svomKeywords: SvomKeyword[]; - @Output() selectSvomAcronym: EventEmitter<string> = new EventEmitter(); - @Output() resetSvomKeywords: EventEmitter<{}> = new EventEmitter(); - @Output() addCriterion: EventEmitter<JsonCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - path: new FormControl('', [Validators.required]), - operator: new FormControl('', [Validators.required]), - value: new FormControl('', [Validators.required]) - }); - - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - this.form.patchValue(changes.criterion.currentValue); - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); - } - - if (changes.criteriaList && this.svomKeywords.length < 1 && changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3)) { - this.selectSvomAcronym.emit(changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3).value); - } - - if (changes.criteriaList && this.svomKeywords.length > 0 && !changes.criteriaList.currentValue.find((c: Criterion) => c.id === 3)) { - this.resetSvomKeywords.emit(); - } - } - - /** - * Transform a SVOM json Keyword to as path value (anis json search) - * - * @param svomKeyword Keyword selected by user - * @returns string path value - */ - getKeywordValue(svomKeyword: SvomKeyword): string { - return `${svomKeyword.extension},${svomKeyword.name}` +export class SvomJsonKwComponent extends AbstractSearchTypeComponent { + constructor() { + super(); + this.form = new FormGroup({ + path: new FormControl('', [Validators.required]), + operator: new FormControl('', [Validators.required]), + value: new FormControl('', [Validators.required]) + }); } - + /** - * Emits event to add criterion to the criteria list. + * Return new criterion * - * @fires EventEmitter<JsonCriterion> + * @return Criterion */ - emitAdd(): void { - const js = { id: this.attribute.id, type: 'json', ...this.form.value }; - this.addCriterion.emit(js); + getCriterion(): Criterion { + return { + id: this.attribute.id, + type: 'json', + ...this.form.value + } as JsonCriterion; } } diff --git a/client/src/app/instance/search/components/criteria/search-type/time.component.html b/client/src/app/instance/search/components/criteria/search-type/time.component.html index 4e8a8e91bddeb843353c9e295430709936da038d..4d940d450c614160079f24e028379a357d4b230a 100644 --- a/client/src/app/instance/search/components/criteria/search-type/time.component.html +++ b/client/src/app/instance/search/components/criteria/search-type/time.component.html @@ -1,36 +1,21 @@ <form [formGroup]="form" novalidate> - <div class="row"> - <div class="col form-group"> - <label> - <app-attribute-label [label]="attribute.label" [description]="attribute.description"></app-attribute-label> - </label> - <div class="row"> - <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> - <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> - <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> - </select> - </div> - <div class="w-100 d-block d-sm-none"></div> - <div class="col-auto pl-sm-1 pr-1"> - <ng-select formControlName="hh" [multiple]="false" placeholder="HH..." class="ng-select-custom ng-select-time"> - <ng-option *ngFor="let hour of hours" [value]="hour">{{ hour }}</ng-option> - </ng-select> - </div> - <div class="col col-sm-auto p-0 text-center">:</div> - <div class="col-auto pl-1"> - <ng-select formControlName="mm" [multiple]="false" placeholder="MM..." class="ng-select-custom ng-select-time"> - <ng-option *ngFor="let min of minutes" [value]="min">{{ min }}</ng-option> - </ng-select> - </div> - </div> + <div class="row form-group"> + <div class="col col-sm-auto pr-sm-1 mb-1 mb-lg-0"> + <select class="custom-select" formControlName="operator" (change)="operatorOnChange()"> + <option *ngFor="let o of operators" [ngValue]="o.value">{{ o.label }}</option> + </select> </div> - <div class="col-2 text-center align-self-end mb-0 mb-sm-1 pb-3"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!form.valid && form.controls.operator.value != 'nl' && form.controls.operator.value != 'nnl'" (click)="emitAdd()"> - <span class="fas fa-plus fa-fw"></span> - </button> - <button class="btn btn-outline-danger" *ngIf="criterion" (click)="deleteCriterion.emit(attribute.id)"> - <span class="fa fa-times fa-fw"></span> - </button> + <div class="w-100 d-block d-sm-none"></div> + <div class="col-auto pl-sm-1 pr-1"> + <ng-select formControlName="hh" [multiple]="false" placeholder="HH..." class="ng-select-custom ng-select-time"> + <ng-option *ngFor="let hour of hours" [value]="hour">{{ hour }}</ng-option> + </ng-select> + </div> + <div class="col col-sm-auto p-0 text-center">:</div> + <div class="col-auto pl-1"> + <ng-select formControlName="mm" [multiple]="false" placeholder="MM..." class="ng-select-custom ng-select-time"> + <ng-option *ngFor="let min of minutes" [value]="min">{{ min }}</ng-option> + </ng-select> </div> </div> -</form> \ No newline at end of file +</form> diff --git a/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts b/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts deleted file mode 100644 index 4b627ac6599797d1a5edddabef980fc023e66cd3..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/search-type/time.component.spec.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { Component, Input, ViewChild } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; - -import { NgSelectModule } from '@ng-select/ng-select'; - -import { TimeComponent } from './time.component'; -import { FieldCriterion } from '../../../../store/models/criterion'; - -describe('[Instance][Search][Component][Criteria][SearchType] TimeComponent', () => { - @Component({ - selector: `app-host`, - template: ` - <app-time - [id]="id" - [label]="label" - [operator]="operator" - [criterion]="criterion" - [advancedForm]="advancedForm"> - </app-time>` - }) - class TestHostComponent { - @ViewChild(TimeComponent, { static: false }) - public testedComponent: TimeComponent; - public id: number = undefined; - public label: string = undefined; - public operator: string = undefined; - public criterion: FieldCriterion = undefined; - public advancedForm: boolean = false; - } - - @Component({ selector: 'app-operator', template: '' }) - class OperatorStubComponent { - @Input() operator: string; - @Input() searchType: string; - @Input() advancedForm: boolean; - @Input() disabled: boolean; - } - - let testHostComponent: TestHostComponent; - let testHostFixture: ComponentFixture<TestHostComponent>; - let testedComponent: TimeComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - declarations: [ - TimeComponent, - TestHostComponent, - OperatorStubComponent - ], - imports: [ - NgSelectModule, - FormsModule, - ReactiveFormsModule - ] - }); - testHostFixture = TestBed.createComponent(TestHostComponent); - testHostComponent = testHostFixture.componentInstance; - testHostFixture.detectChanges(); - testedComponent = testHostComponent.testedComponent; - }); - - it('should create the component', () => { - expect(testedComponent).toBeTruthy(); - }); - - it('should call ngOnChanges and apply changes', () => { - const spy = jest.spyOn(testedComponent, 'ngOnChanges'); - testHostComponent.criterion = { id: 1, type: 'field', operator: 'eq', value: '15:47' } as FieldCriterion; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.hh.value).toEqual('15'); - expect(testedComponent.form.controls.mm.value).toEqual('47'); - expect(testedComponent.form.disabled).toBeTruthy(); - testHostComponent.criterion = undefined; - testHostFixture.detectChanges(); - expect(testedComponent.form.controls.hh.value).toBeNull(); - expect(testedComponent.form.controls.hh.value).toBeNull(); - expect(testedComponent.form.enabled).toBeTruthy(); - expect(spy).toHaveBeenCalledTimes(2); - }); - - it('#changeOperator() should change the operator', () => { - expect(testedComponent.operator).toBeUndefined(); - testedComponent.changeOperator('toto'); - expect(testedComponent.operator).toBe('toto'); - }); - - it('raises the add criterion event when clicked', () => { - testedComponent.id = 1; - const operator = 'eq'; - testedComponent.operator = operator; - testedComponent.form.controls.hh.setValue('15'); - testedComponent.form.controls.mm.setValue('47'); - const expectedCriterion = { id: testedComponent.id, type: 'field', operator, value: '15:47' } as FieldCriterion; - testedComponent.addCriterion.subscribe((event: FieldCriterion) => expect(event).toEqual(expectedCriterion)); - testedComponent.emitAdd(); - }); - - it('#initTime(t) should return an array of string with 2 digits from 0 to t', () => { - const n = 10; - expect(testedComponent.initTime(n).length).toEqual(n); - expect(testedComponent.initTime(n)[5]).toEqual('05'); - }); -}); diff --git a/client/src/app/instance/search/components/criteria/search-type/time.component.ts b/client/src/app/instance/search/components/criteria/search-type/time.component.ts index 9b3cc6585f5203c46a1cddac2dcb0943af21f8ca..6c74117c9421134763d72a70b56fd4eac5d906ff 100644 --- a/client/src/app/instance/search/components/criteria/search-type/time.component.ts +++ b/client/src/app/instance/search/components/criteria/search-type/time.component.ts @@ -1,68 +1,36 @@ -/** - * This file is part of Anis Client. - * - * @copyright Laboratoire d'Astrophysique de Marseille / CNRS - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, OnInit, OnChanges, SimpleChanges } from '@angular/core'; +import { Component } from '@angular/core'; import { FormGroup, FormControl, Validators } from '@angular/forms'; +import { AbstractSearchTypeComponent } from './abstract-search-type.component'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; -import { Attribute } from 'src/app/metamodel/models'; -import { searchTypeOperators } from 'src/app/shared/utils'; -/** - * @class - * @classdesc Time search type component. - * - * @implements OnChanges - */ @Component({ selector: 'app-time', - templateUrl: 'time.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + templateUrl: 'time.component.html' }) - -export class TimeComponent implements OnInit, OnChanges { - @Input() attribute: Attribute; - @Input() criterion: Criterion; - @Output() addCriterion: EventEmitter<FieldCriterion> = new EventEmitter(); - @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); - - public form = new FormGroup({ - operator: new FormControl(''), - hh: new FormControl('', [Validators.required]), - mm: new FormControl('', [Validators.required]) - }); - +export class TimeComponent extends AbstractSearchTypeComponent { hours: string[] = this.initTime(24); minutes: string[] = this.initTime(60); - operators = searchTypeOperators; - - ngOnInit() { - if (!this.attribute.dynamic_operator) { - this.form.controls.operator.disable(); - } + constructor() { + super(); + this.form = new FormGroup({ + operator: new FormControl(''), + hh: new FormControl('', [Validators.required]), + mm: new FormControl('', [Validators.required]) + }); } - ngOnChanges(changes: SimpleChanges): void { - if (changes.criterion && changes.criterion.currentValue) { - const criterion = changes.criterion.currentValue as FieldCriterion; - this.form.controls.operator.setValue(criterion.operator); - if (criterion.operator != 'nl' && criterion.operator != 'nnl') { - this.form.controls.hh.setValue(criterion.value.slice(0, 2)); - this.form.controls.mm.setValue(criterion.value.slice(3, 5)); + setCriterion(criterion: Criterion) { + super.setCriterion(criterion); + if (criterion) { + const fieldCriterion = criterion as FieldCriterion; + this.form.controls.operator.setValue(fieldCriterion.operator); + if (fieldCriterion.operator != 'nl' && fieldCriterion.operator != 'nnl') { + this.form.controls.hh.setValue(fieldCriterion.value.slice(0, 2)); + this.form.controls.mm.setValue(fieldCriterion.value.slice(3, 5)); } - this.form.disable(); - } - - if (changes.criterion && !changes.criterion.currentValue) { - this.form.enable(); - this.form.reset(); + } else { this.form.controls.operator.setValue(this.attribute.operator); if (!this.attribute.dynamic_operator) { this.form.controls.operator.disable(); @@ -70,6 +38,29 @@ export class TimeComponent implements OnInit, OnChanges { this.operatorOnChange(); } } + + /** + * Return new criterion + * + * @return Criterion + */ + getCriterion(): Criterion { + let value = null; + if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { + value = `${this.form.value.hh}:${this.form.value.mm}` + } + + return { + id: this.attribute.id, + type: 'field', + operator: this.form.controls.operator.value, + value + } as FieldCriterion; + } + + isValid(): boolean { + return this.form.valid || this.form.controls.operator.value === 'nl' || this.form.controls.operator.value === 'nnl'; + } /** * Modifies operator with the given one. @@ -84,20 +75,6 @@ export class TimeComponent implements OnInit, OnChanges { } } - /** - * Emits event to add criterion to the criteria list. - * - * @fires EventEmitter<FieldCriterion> - */ - emitAdd(): void { - let value = null; - if (this.form.controls.operator.value != 'nl' && this.form.controls.operator.value != 'nnl') { - value = `${this.form.value.hh}:${this.form.value.mm}` - } - const time = {id: this.attribute.id, type: 'field', operator: this.form.controls.operator.value, value }; - this.addCriterion.emit(time); - } - /** * Returns string array to represent the given time. * @@ -113,4 +90,5 @@ export class TimeComponent implements OnInit, OnChanges { } return array; } + } diff --git a/client/src/app/instance/search/components/criteria/test-search-type.component.ts b/client/src/app/instance/search/components/criteria/test-search-type.component.ts deleted file mode 100644 index 72d3aefa2813cdab0f9a0223c9adcede0245cb18..0000000000000000000000000000000000000000 --- a/client/src/app/instance/search/components/criteria/test-search-type.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component, Input } from '@angular/core'; -import { AbstractSearchTypeComponent } from './abstract-search-type.component'; - -@Component({ - selector: 'app-test-search-type', - template: '<p>{{ data.text }}</p>' -}) -export class TestSearchTypeComponent implements AbstractSearchTypeComponent { - @Input() data: any; -} diff --git a/client/src/styles.scss b/client/src/styles.scss index dfcc0815a1435d9e54d35ec964131da9877fb1c1..2f276ed780c804c7a33845a14cafaff9d99f1680 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -95,3 +95,17 @@ input.ng-invalid, select.ng-invalid, .ng-select.ng-invalid div.ng-select-contain .disabled { cursor: not-allowed !important; } + +.operator_readonly { + background-color: #e9ecef; + border: 1px solid #ced4da; + border-radius: .25rem; + display: block; + width: 100%; + height: calc(1.5em + .75rem + 2px); + padding: .375rem .75rem; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color:#495057; +}