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 fa4bc0823c2528089f3fa980ec930dacec8997eb..59dc44c48450c09d258524a2c901aa72bfa2ecd3 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 @@ -4,6 +4,7 @@ [criterion]="getCriterion(attribute.id)" [criteriaList]="criteriaList" (addCriterion)="emitAdd($event)" + (updateCriterion)="emitUpdate($event)" (deleteCriterion)="emitDelete($event)"> </app-criterion> </div> diff --git a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts index edc3ae5cc773c6c0afe38b7bc2b14f4217283bad..fa8c7617c6a89bb65eb4eff253797b3fb0b44fac 100644 --- a/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts +++ b/client/src/app/instance/search/components/criteria/criteria-by-family.component.ts @@ -25,6 +25,7 @@ export class CriteriaByFamilyComponent { @Input() attributeList: Attribute[]; @Input() criteriaList: Criterion[]; @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); + @Output() updateCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); /** @@ -60,6 +61,17 @@ export class CriteriaByFamilyComponent { this.addCriterion.emit(criterion); } + /** + * Emits event to update the given criterion to the criteria list. + * + * @param {Criterion} updatedCriterion - The updated criterion. + * + * @fires EventEmitter<Criterion> + */ + emitUpdate(updatedCriterion: Criterion): void { + this.updateCriterion.emit(updatedCriterion); + } + /** * Emits event to remove the given criterion ID from the criteria list. * diff --git a/client/src/app/instance/search/components/criteria/criteria-tabs.component.html b/client/src/app/instance/search/components/criteria/criteria-tabs.component.html index fa9d3aef3286c6ac5d3fb9cdd32279b4c00f2d3b..da39e6820fb4dcd3947b8bd2830b0a3603cf1729 100644 --- a/client/src/app/instance/search/components/criteria/criteria-tabs.component.html +++ b/client/src/app/instance/search/components/criteria/criteria-tabs.component.html @@ -17,6 +17,7 @@ [attributeList]="attributeList | attributeListByFamily:family.id" [criteriaList]="criteriaList" (addCriterion)="emitAdd($event)" + (updateCriterion)="emitUpdate($event)" (deleteCriterion)="emitDelete($event)"> </app-criteria-by-family> </accordion-group> diff --git a/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts b/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts index f781372a5cf74bd91196d2375ff945b629262937..a11d3b6be64f7e0a70c6253c64bee4d8c53593f2 100644 --- a/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts +++ b/client/src/app/instance/search/components/criteria/criteria-tabs.component.ts @@ -26,6 +26,7 @@ export class CriteriaTabsComponent { @Input() criteriaFamilyList: CriteriaFamily[]; @Input() criteriaList: Criterion[]; @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); + @Output() updateCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); /** @@ -39,6 +40,17 @@ export class CriteriaTabsComponent { this.addCriterion.emit(criterion); } + /** + * Emits event to update the given criterion to the criteria list. + * + * @param {Criterion} updatedCriterion - The updated criterion. + * + * @fires EventEmitter<Criterion> + */ + emitUpdate(updatedCriterion: Criterion): void { + this.updateCriterion.emit(updatedCriterion); + } + /** * Emits event to remove the given criterion ID to the criteria list. * 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 f9249c619b86339377989b806c7bef02cf295fea..a20b44c936b8d7ed0be717e29cc66fc63ca3c512 100644 --- a/client/src/app/instance/search/components/criteria/criterion.component.html +++ b/client/src/app/instance/search/components/criteria/criterion.component.html @@ -6,9 +6,6 @@ <ng-template searchType></ng-template> </div> <div class="col-2 text-center align-self-center"> - <button class="btn btn-outline-success" *ngIf="!criterion" [hidden]="!searchTypeComponent.isValid() && !searchTypeComponent.nullOrNotNull" (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> 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 245f6fba7466aaba222547c067c764facfa8ad8c..26d4e8175cb8e64d6f944250fe225ba7ede726a0 100644 --- a/client/src/app/instance/search/components/criteria/criterion.component.ts +++ b/client/src/app/instance/search/components/criteria/criterion.component.ts @@ -12,6 +12,7 @@ import { Component, Input, Output, EventEmitter, ViewChild, SimpleChanges, OnIni import { Attribute } from 'src/app/metamodel/models'; import { Criterion, FieldCriterion } from 'src/app/instance/store/models'; import { SearchTypeLoaderDirective, AbstractSearchTypeComponent, getSearchTypeComponent } from './search-type'; +import { criterionToString } from 'src/app/instance/store/models'; @Component({ selector: 'app-criterion', @@ -22,6 +23,7 @@ export class CriterionComponent implements OnInit, OnChanges { @Input() criterion: Criterion; @Input() criteriaList: Criterion[]; @Output() addCriterion: EventEmitter<Criterion> = new EventEmitter(); + @Output() updateCriterion: EventEmitter<Criterion> = new EventEmitter(); @Output() deleteCriterion: EventEmitter<number> = new EventEmitter(); @ViewChild(SearchTypeLoaderDirective, {static: true}) SearchTypeLoaderDirective!: SearchTypeLoaderDirective; @@ -30,12 +32,19 @@ export class CriterionComponent implements OnInit, OnChanges { ngOnInit() { const viewContainerRef = this.SearchTypeLoaderDirective.viewContainerRef; + viewContainerRef.clear(); const componentRef = viewContainerRef.createComponent<AbstractSearchTypeComponent>( getSearchTypeComponent(this.attribute.search_type) ); componentRef.instance.setAttribute(this.attribute); componentRef.instance.setCriterion(this.criterion); componentRef.instance.setCriteriaList(this.criteriaList); + componentRef.instance.emitAdd.subscribe(() => this.emitAdd()); + componentRef.instance.emitDelete.subscribe(() => { + if (this.criterion) { + this.deleteCriterion.emit(this.criterion.id) + } + }); this.searchTypeComponent = componentRef.instance; } @@ -65,6 +74,14 @@ export class CriterionComponent implements OnInit, OnChanges { } else { criterion = this.searchTypeComponent.getCriterion(); } - this.addCriterion.emit(criterion); + + const existingCriterion = this.criteriaList.find(c => c.id === criterion.id) + if (existingCriterion) { + if (criterionToString(existingCriterion) !== criterionToString(criterion)) { + this.updateCriterion.emit(criterion); + } + } else { + this.addCriterion.emit(criterion); + } } } diff --git a/client/src/app/instance/search/components/criteria/search-criteria-list.component.html b/client/src/app/instance/search/components/criteria/search-criteria-list.component.html index a5525f7c7e0fa4165e98f9e38f5989b982b07ea4..3017a41dbdad6a5beb5fd0b2982590a5a6a2b048 100644 --- a/client/src/app/instance/search/components/criteria/search-criteria-list.component.html +++ b/client/src/app/instance/search/components/criteria/search-criteria-list.component.html @@ -11,13 +11,6 @@ </span></h4> </div> </button> - - <p *ngIf="!coneSearch && criteriaList.length < 1"> - To add search criteria, fill fields in the form bellow and click on the - <strong><span class="fas fa-plus fa-fw"></span></strong> button for each - search constraint. To remove a constraint click on the - <strong><span class="fas fa-times fa-fw"></span></strong>. - </p> <app-cone-search-parameters *ngIf="coneSearch" [coneSearch]="coneSearch" [backgroundColor]="instance.design_color"> 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 index 2ef2b50d64685e780185810cff8751ab25acd572..8bbf855f58576b8616bb295bf70c8872db6e13d1 100644 --- 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 @@ -7,24 +7,41 @@ * file that was distributed with this source code. */ -import { Directive } from '@angular/core'; +import { Directive, EventEmitter, OnInit, OnDestroy } from '@angular/core'; import { UntypedFormGroup } 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'; +import { debounceTime, Subscription } from 'rxjs'; @Directive() -export abstract class AbstractSearchTypeComponent { +export abstract class AbstractSearchTypeComponent implements OnInit, OnDestroy { attribute: Attribute; criteriaList: Criterion[]; + emitAdd: EventEmitter<{}> = new EventEmitter<{}>(); + emitDelete: EventEmitter<{}> = new EventEmitter<{}>(); form: UntypedFormGroup; + formValueChangesSubscription: Subscription; operators = searchTypeOperators; nullOrNotNull: string = ''; constructor() { } + ngOnInit() { + this.formValueChangesSubscription = this.form.valueChanges.pipe( + debounceTime(300), + ) + .subscribe(() => { + if (this.isValid()) { + this.emitAdd.emit(); + } else { + this.emitDelete.emit(); + } + }); + } + setAttribute(attribute: Attribute) { this.attribute = attribute; } @@ -32,10 +49,9 @@ export abstract class AbstractSearchTypeComponent { setCriterion(criterion: Criterion) { if (criterion) { this.form.patchValue(criterion); - this.form.disable(); } else { - this.form.enable(); this.form.reset(); + this.enable(); this.nullOrNotNull = ''; } } @@ -71,4 +87,8 @@ export abstract class AbstractSearchTypeComponent { return 'number'; } } + + ngOnDestroy(): void { + this.formValueChangesSubscription.unsubscribe(); + } } 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 9df636547f7c037a189a8ec9ce6488942eaa5727..4095d96be9acbb05970b7fabaeab3cad0200f27a 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 @@ -7,7 +7,7 @@ * file that was distributed with this source code. */ -import { Component } from '@angular/core'; +import { Component} from '@angular/core'; import { UntypedFormGroup, UntypedFormControl, Validators } from '@angular/forms'; import { AbstractSearchTypeComponent } from './abstract-search-type.component'; @@ -56,7 +56,8 @@ export class BetweenComponent extends AbstractSearchTypeComponent { return { id: this.attribute.id, type: 'between', - ...this.form.value + min: (this.form.controls.min.value) ? this.form.controls.min.value : null, + max: (this.form.controls.max.value) ? this.form.controls.max.value : null, } as BetweenCriterion; } @@ -87,7 +88,10 @@ export class BetweenComponent extends AbstractSearchTypeComponent { } isValid() { - return this.form.controls.min.value || this.form.controls.max.value; + return this.form.controls.min.value + || this.form.controls.max.value + || this.form.controls.label.value === 'nl' + || this.form.controls.label.value === 'nnl'; } labelOnChange() { 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 69aa61b31fc26ea3405f7de2f544c76aaf912a61..e969245e8fb27e6f02f4e48e7d2c8bd3c1b0233a 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 @@ -82,7 +82,9 @@ export class CheckboxComponent extends AbstractSearchTypeComponent { isValid(): boolean { const selected = this.getCheckboxes().value; const values = [...this.attribute.options.filter((option, index) => selected[index])]; - return values.length > 0; + return values.length > 0 + || this.form.controls.label.value === 'nl' + || this.form.controls.label.value === 'nnl'; } /** diff --git a/client/src/app/instance/search/containers/criteria.component.html b/client/src/app/instance/search/containers/criteria.component.html index 41898accec3adaef3dea3bd317a9ebf33ac50876..6b3e0fe32e5613798a48fd9f50d082331edae77f 100644 --- a/client/src/app/instance/search/containers/criteria.component.html +++ b/client/src/app/instance/search/containers/criteria.component.html @@ -34,6 +34,7 @@ [criteriaFamilyList]="criteriaFamilyList | async" [criteriaList]="criteriaList | async" (addCriterion)="addCriterion($event)" + (updateCriterion)="updateCriterion($event)" (deleteCriterion)="deleteCriterion($event)"> </app-criteria-tabs> </div> diff --git a/client/src/app/instance/search/containers/criteria.component.ts b/client/src/app/instance/search/containers/criteria.component.ts index b850069b983d64d58700eff797aa1e0224e890c9..1320c1df1ca2ca883c1b1fdb1aacec45c0f0c0d1 100644 --- a/client/src/app/instance/search/containers/criteria.component.ts +++ b/client/src/app/instance/search/containers/criteria.component.ts @@ -71,6 +71,15 @@ export class CriteriaComponent extends AbstractSearchComponent { this.store.dispatch(searchActions.addCriterion({ criterion })); } + /** + * Dispatches action to update the given criterion to the search. + * + * @param {Criterion} updatedCriterion - The updated criterion. + */ + updateCriterion(updatedCriterion: Criterion): void { + this.store.dispatch(searchActions.updateCriterion({ updatedCriterion })) + } + /** * Dispatches action to remove the given criterion ID to the search. * diff --git a/client/src/app/instance/store/actions/search.actions.ts b/client/src/app/instance/store/actions/search.actions.ts index 86333f653f87a2d19ab33f8ed3e092e20654105c..51cd7a0384f7dbb2dc7b64cccc145bad8865bb8d 100644 --- a/client/src/app/instance/store/actions/search.actions.ts +++ b/client/src/app/instance/store/actions/search.actions.ts @@ -24,6 +24,7 @@ export const checkOutput = createAction('[Search] Check Output'); export const checkResult = createAction('[Search] Check Result'); export const updateCriteriaList = createAction('[Search] Update Criteria List', props<{ criteriaList: Criterion[] }>()); export const addCriterion = createAction('[Search] Add Criterion', props<{ criterion: Criterion }>()); +export const updateCriterion = createAction('[Search] Update Criterion', props<{ updatedCriterion: Criterion }>()); export const deleteCriterion = createAction('[Search] Delete Criterion', props<{ idCriterion: number }>()); export const updateOutputList = createAction('[Search] Update Output List', props<{ outputList: number[] }>()); export const retrieveDataLength = createAction('[Search] Retrieve Data Length'); diff --git a/client/src/app/instance/store/reducers/search.reducer.ts b/client/src/app/instance/store/reducers/search.reducer.ts index e81da50750d1d573e65d2d3e307ef8408b147a5d..acceaacc730dd72a1724f8c698d6c21969c167ea 100644 --- a/client/src/app/instance/store/reducers/search.reducer.ts +++ b/client/src/app/instance/store/reducers/search.reducer.ts @@ -93,6 +93,12 @@ export const searchReducer = createReducer( ...state, criteriaList: [...state.criteriaList, criterion] })), + on(searchActions.updateCriterion, (state, { updatedCriterion }) => ({ + ...state, + criteriaList: state.criteriaList.map( + criterion => (criterion.id === updatedCriterion.id) ? updatedCriterion : criterion + ) + })), on(searchActions.deleteCriterion, (state, { idCriterion }) => ({ ...state, criteriaList: [...state.criteriaList.filter(c => c.id !== idCriterion)]