diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d2210a9fa4f6653fd0b0989597c85758b7cedb0..d2b6603b5a0069fe959dc9bc7c134be92d4cd44b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - #32 => Criteria components with default value did not work - #79 => Repair and improve component checkbox + test - #80 => Search type field + IN or NOT IN didn't work +- #63 => If no outputs selectionned, request cannot work ### Changed - #76 => Adding label min and max for the between component diff --git a/src/app/search/components/output/output-by-category.component.spec.ts b/src/app/search/components/output/output-by-category.component.spec.ts index 1854fa3ea627e63a10763b8efc3cb89cf69e5c71..713096dac97179f176eabdccdee296b8b333b4d7 100644 --- a/src/app/search/components/output/output-by-category.component.spec.ts +++ b/src/app/search/components/output/output-by-category.component.spec.ts @@ -1,6 +1,8 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, ViewChild } from '@angular/core'; +import { ToastrModule } from 'ngx-toastr'; +import { ToastrService } from 'ngx-toastr'; import { OutputByCategoryComponent } from './output-by-category.component'; import { Attribute } from '../../../metamodel/model'; import { ATTRIBUTE_LIST } from '../../../../settings/test-data'; @@ -36,7 +38,9 @@ describe('[Search][Output] Component: OutputByCategoryComponent', () => { declarations: [ OutputByCategoryComponent, TestHostComponent - ] + ], + imports: [ToastrModule.forRoot()], + providers: [ToastrService] }); testHostFixture = TestBed.createComponent(TestHostComponent); testHostComponent = testHostFixture.componentInstance; diff --git a/src/app/search/components/output/output-by-category.component.ts b/src/app/search/components/output/output-by-category.component.ts index 54ce630c5dd49728794f24df543801203dd4e7a1..f12fde2838141a7aa54d30a5bb4240e5e81db464 100644 --- a/src/app/search/components/output/output-by-category.component.ts +++ b/src/app/search/components/output/output-by-category.component.ts @@ -1,5 +1,6 @@ import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy } from '@angular/core'; +import { ToastrService } from 'ngx-toastr'; import { Attribute } from '../../../metamodel/model'; @Component({ @@ -11,10 +12,23 @@ import { Attribute } from '../../../metamodel/model'; export class OutputByCategoryComponent { @Input() categoryLabel: string; @Input() attributeList: Attribute[]; - @Input() outputList: number[]; + @Input() set outputList(outputList: number[]) { + this._outputList = outputList; + if (outputList.length === 0) { + this.toastr.warning('At least 1 output is required!'); + this.outputListEmpty.emit(true); + } else { + this.outputListEmpty.emit(false); + } + } @Input() isAllSelected: boolean; @Input() isAllUnselected: boolean; + @Output() outputListEmpty: EventEmitter<boolean> = new EventEmitter(); @Output() change: EventEmitter<number[]> = new EventEmitter(); + public _outputList: number[]; + + + constructor(private toastr: ToastrService) { } getAttributeListSortedByDisplay() { return this.attributeList @@ -22,11 +36,11 @@ export class OutputByCategoryComponent { } isSelected(id: number) { - return this.outputList.filter(i => i === id).length > 0; + return this._outputList.filter(i => i === id).length > 0; } toggleSelection(attributeId: number): void { - const clonedOutputList = [...this.outputList]; + const clonedOutputList = [...this._outputList]; const index = clonedOutputList.indexOf(attributeId); if (index > -1) { clonedOutputList.splice(index, 1); @@ -37,7 +51,7 @@ export class OutputByCategoryComponent { } selectAll(): void { - const clonedOutputList = [...this.outputList]; + const clonedOutputList = [...this._outputList]; const attributeListId = this.attributeList.map(a => a.id); attributeListId.filter(id => clonedOutputList.indexOf(id) === -1).forEach(id => { clonedOutputList.push(id); @@ -46,7 +60,7 @@ export class OutputByCategoryComponent { } unselectAll(): void { - const clonedOutputList = [...this.outputList]; + const clonedOutputList = [...this._outputList]; const attributeListId = this.attributeList.map(a => a.id); attributeListId.filter(id => clonedOutputList.indexOf(id) > -1).forEach(id => { const index = clonedOutputList.indexOf(id); diff --git a/src/app/search/components/output/output-by-family.component.html b/src/app/search/components/output/output-by-family.component.html index 61fb7281e239e584e9fbf7efa62ca9cce08c9db0..16fbe74fbdab6c489c2f190829c76c8a0d838e9b 100644 --- a/src/app/search/components/output/output-by-family.component.html +++ b/src/app/search/components/output/output-by-family.component.html @@ -7,6 +7,7 @@ [outputList]="outputList" [isAllSelected]="getIsAllSelected(category.id)" [isAllUnselected]="getIsAllUnselected(category.id)" + (outputListEmpty)="outputListEmpty.emit($event)" (change)="emitChange($event)"> </app-output-by-category> </div> diff --git a/src/app/search/components/output/output-by-family.component.ts b/src/app/search/components/output/output-by-family.component.ts index 3da47155e2d4aba66a6b3386cb7804c6d030e74e..e23b29ab7df92d886e64d733c1e4218c762ea28d 100644 --- a/src/app/search/components/output/output-by-family.component.ts +++ b/src/app/search/components/output/output-by-family.component.ts @@ -12,6 +12,7 @@ export class OutputByFamilyComponent { @Input() categoryList: Category[]; @Input() datasetAttributeList: Attribute[]; @Input() outputList: number[]; + @Output() outputListEmpty: EventEmitter<boolean> = new EventEmitter(); @Output() change: EventEmitter<number[]> = new EventEmitter(); getCategoryByFamilySortedByDisplay(idFamily: number): Category[] { diff --git a/src/app/search/components/output/output-tabs.component.html b/src/app/search/components/output/output-tabs.component.html index 93ba0ecb6ef19172bb7660b46134680215df2a21..49a90056b11253f0a77b7f638d4f54c2f151c0cb 100644 --- a/src/app/search/components/output/output-tabs.component.html +++ b/src/app/search/components/output/output-tabs.component.html @@ -7,7 +7,8 @@ [datasetAttributeList]="datasetAttributeList" [categoryList]="categoryList" [outputList]="outputList" - (change)="emitChange($event)"> + (outputListEmpty)="outputListEmpty.emit($event)" + (change)="change.emit($event)"> </app-output-by-family> </div> </div> @@ -28,8 +29,9 @@ [outputFamily]="family" [datasetAttributeList]="datasetAttributeList" [categoryList]="categoryList" - [outputList]="outputList" - (change)="emitChange($event)"> + [outputList]="outputList" + (outputListEmpty)="outputListEmpty.emit($event)" + (change)="change.emit($event)"> </app-output-by-family> </accordion-group> </accordion> \ No newline at end of file diff --git a/src/app/search/components/output/output-tabs.component.spec.ts b/src/app/search/components/output/output-tabs.component.spec.ts index bdbc344bd277ba85c5d01d0a736f626c9ecfcc34..f7de6c9fbcd8083d4371e0331fb879072b299e3b 100644 --- a/src/app/search/components/output/output-tabs.component.spec.ts +++ b/src/app/search/components/output/output-tabs.component.spec.ts @@ -65,11 +65,5 @@ describe('[Search][Output] Component: OutputTabsComponent', () => { expect(sortedOutputFamilyList[0].id).toBe(2); expect(sortedOutputFamilyList[1].id).toBe(1); }); - - it('#emitChange(outputList) should raise change event', () => { - const expectedOutputList = [1]; - testedComponent.change.subscribe((event: number[]) => expect(event).toEqual(expectedOutputList)); - testedComponent.emitChange([1]); - }); }); diff --git a/src/app/search/components/output/output-tabs.component.ts b/src/app/search/components/output/output-tabs.component.ts index fedded17ffcbc26d252c946ea060c09f6d762f27..c6bee89452710392b6ae1e7fe0e5130c4ed277b7 100644 --- a/src/app/search/components/output/output-tabs.component.ts +++ b/src/app/search/components/output/output-tabs.component.ts @@ -12,14 +12,11 @@ export class OutputTabsComponent { @Input() categoryList: Category[]; @Input() datasetAttributeList: Attribute[]; @Input() outputList: number[]; + @Output() outputListEmpty: EventEmitter<boolean> = new EventEmitter(); @Output() change: EventEmitter<number[]> = new EventEmitter(); getOutputFamilyListSortedByDisplay(): Family[] { return this.outputFamilyList .sort((a, b) => a.display - b.display); } - - emitChange(clonedOutpuList: number[]): void { - this.change.emit(clonedOutpuList); - } } diff --git a/src/app/search/components/progress-bar.component.html b/src/app/search/components/progress-bar.component.html index 4ccdbe2489cc42a6b19749fdb6cc7af1e0e2ad0b..9ba611d6409b1f7fc62bad2dfdba2cef6b27af36 100644 --- a/src/app/search/components/progress-bar.component.html +++ b/src/app/search/components/progress-bar.component.html @@ -49,7 +49,7 @@ </a> </li> <li class="nav-item" [ngClass]="{'active': currentStep === 'result', 'checked': resultStepChecked}"> - <a *ngIf="datasetName" class="nav-link" [ngClass]="{'disabled': !datasetName}" + <a *ngIf="datasetName" class="nav-link" [ngClass]="{'disabled': outputListEmpty}" routerLink="/search/result/{{datasetName}}" [queryParams]="queryParams" data-toggle="tab"> <div class="icon-circle"> <span class="fas fa-table"></span> diff --git a/src/app/search/components/progress-bar.component.ts b/src/app/search/components/progress-bar.component.ts index 6e21d2b37b57345391a2e5f90d8ab89b7ddc91a8..16e4039300ce652b0aeb8a098c951b088042206a 100644 --- a/src/app/search/components/progress-bar.component.ts +++ b/src/app/search/components/progress-bar.component.ts @@ -1,5 +1,7 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; +import { SearchQueryParams } from '../store/model'; + @Component({ selector: 'app-progress-bar', templateUrl: 'progress-bar.component.html', @@ -12,7 +14,8 @@ export class ProgressBarComponent { @Input() criteriaStepChecked: boolean; @Input() outputStepChecked: boolean; @Input() resultStepChecked: boolean; - @Input() queryParams: any; + @Input() queryParams: SearchQueryParams; + @Input() outputListEmpty: boolean; getStepClass() { switch (this.currentStep) { diff --git a/src/app/search/components/summary.component.html b/src/app/search/components/summary.component.html index 86f7f7577194347a63edcc9dcf07b0777dd10c07..d5cc32639f5cfa91f0e980cdcc335b5638b9271d 100644 --- a/src/app/search/components/summary.component.html +++ b/src/app/search/components/summary.component.html @@ -20,7 +20,10 @@ <p class="text-center font-italic" [tooltip]="summaryOutputs"> Output <span class="far fa-question-circle fa-xs"></span> </p> - <ul class="pl-5 list-unstyled"> + <p *ngIf="outputList.length < 1" class="pl-5 text-danger font-weight-bold"> + At least 1 output required! + </p> + <ul *ngIf="outputList.length > 0" class="pl-5 list-unstyled"> <li *ngFor="let output of outputList"> {{ getAttribute(output).form_label }} </li> diff --git a/src/app/search/components/summary.component.ts b/src/app/search/components/summary.component.ts index be26bef6cd693e08c34073af37882067756a89f6..6da9b7071fea792dc45748a4009f30dacab03560 100644 --- a/src/app/search/components/summary.component.ts +++ b/src/app/search/components/summary.component.ts @@ -1,6 +1,6 @@ import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; -import { Criterion } from '../store/model'; +import { Criterion, SearchQueryParams } from '../store/model'; import { Dataset, Attribute, Family, Category } from '../../metamodel/model'; import { printCriterion as print } from '../../shared/utils' @@ -19,7 +19,8 @@ export class SummaryComponent { @Input() outputFamilyList: Family[]; @Input() categoryList: Category[]; @Input() outputList: number[]; - @Input() queryParams: any; + @Input() queryParams: SearchQueryParams; + @Input() outputListEmpty: boolean; getDataset(): Dataset { return this.datasetList.find(dataset => dataset.name === this.datasetName); diff --git a/src/app/search/containers/output.component.css b/src/app/search/containers/output.component.css new file mode 100644 index 0000000000000000000000000000000000000000..252761f5ab73ebc704197c3a19694df2f803509e --- /dev/null +++ b/src/app/search/containers/output.component.css @@ -0,0 +1,3 @@ +.disabled { + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/app/search/containers/output.component.html b/src/app/search/containers/output.component.html index 1a37564079aa15ba2adb77b28fcd9eca713f95e9..ab9a65946ccfed88e1089b93b516864c8a6c7a91 100644 --- a/src/app/search/containers/output.component.html +++ b/src/app/search/containers/output.component.html @@ -11,6 +11,7 @@ [categoryList]="categoryList | async" [datasetAttributeList]="datasetAttributeList | async" [outputList]="outputList | async" + (outputListEmpty)="emitOutputListEmpty($event)" (change)="updateOutputList($event)"> </app-output-tabs> </div> @@ -24,6 +25,7 @@ [outputFamilyList]="outputFamilyList | async" [categoryList]="categoryList | async" [outputList]="outputList | async" + [outputListEmpty]="outputListEmpty | async" [queryParams]="queryParams | async"> </app-summary> </div> @@ -36,9 +38,14 @@ </a> </div> <div class="col col-auto"> - <a routerLink="/search/result/{{datasetName | async}}" [queryParams]="queryParams | async" - class="btn btn-outline-primary"> + <button *ngIf="outputListEmpty | async;else notEmpty" class="btn btn-outline-primary disabled not-allowed" title="At least 1 output required!"> Next <span class="fas fa-arrow-right"></span> - </a> + </button> + <ng-template #notEmpty> + <a routerLink="/search/result/{{datasetName | async}}" [queryParams]="queryParams | async" + class="btn btn-outline-primary"> + Next <span class="fas fa-arrow-right"></span> + </a> + </ng-template> </div> </div> \ No newline at end of file diff --git a/src/app/search/containers/output.component.ts b/src/app/search/containers/output.component.ts index 02d3fc4beaad9ac0020bd04a0a16e7ca9af578e8..249f09917df6ad6f6b3e9ed2587569b162f43b5e 100644 --- a/src/app/search/containers/output.component.ts +++ b/src/app/search/containers/output.component.ts @@ -20,7 +20,8 @@ interface StoreState { @Component({ selector: 'app-output', - templateUrl: 'output.component.html' + templateUrl: 'output.component.html', + styleUrls: ['output.component.css'] }) export class OutputComponent implements OnInit { public outputSearchMetaIsLoading: Observable<boolean>; @@ -39,6 +40,7 @@ export class OutputComponent implements OnInit { public criteriaList: Observable<Criterion[]>; public outputList: Observable<number[]>; public queryParams: Observable<SearchQueryParams>; + public outputListEmpty: Observable<boolean>; constructor(private store: Store<StoreState>, private scrollTopService: ScrollTopService) { this.outputSearchMetaIsLoading = store.select(metamodelSelector.getOutputSearchMetaIsLoading); @@ -57,6 +59,7 @@ export class OutputComponent implements OnInit { this.criteriaList = this.store.select(searchSelector.getCriteriaList); this.outputList = this.store.select(searchSelector.getOutputList); this.queryParams = this.store.select(searchSelector.getQueryParams); + this.outputListEmpty = this.store.select(searchSelector.getOutputListEmpty); } ngOnInit() { @@ -72,4 +75,10 @@ export class OutputComponent implements OnInit { updateOutputList(outputList: number[]): void { this.store.dispatch(new searchActions.UpdateOutputListAction(outputList)); } + + emitOutputListEmpty(outputListEmpty: boolean): void { + // Create a micro task that is processed after the current synchronous code + // This micro task prevent the expression changed after is has been checked error + Promise.resolve(null).then(() => this.store.dispatch(new searchActions.OutputListEmptyAction(outputListEmpty))); + } } diff --git a/src/app/search/containers/search.component.html b/src/app/search/containers/search.component.html index 49b935fcfc36d38f85d74845e50b2d8efc340b11..2c70f5b45b48129afc59d58c32798295834eb200 100644 --- a/src/app/search/containers/search.component.html +++ b/src/app/search/containers/search.component.html @@ -5,7 +5,8 @@ [criteriaStepChecked]="criteriaStepChecked | async" [outputStepChecked]="outputStepChecked | async" [resultStepChecked]="resultStepChecked | async" - [queryParams]="queryParams | async"> + [queryParams]="queryParams | async" + [outputListEmpty]="outputListEmpty | async"> </app-progress-bar> <router-outlet></router-outlet> </div> \ No newline at end of file diff --git a/src/app/search/containers/search.component.ts b/src/app/search/containers/search.component.ts index 8c1221d52a3c548ed05c83231aa834a42fdfaa6e..0fd226b4c01de545ab9e16324afb2f075e9e5d73 100644 --- a/src/app/search/containers/search.component.ts +++ b/src/app/search/containers/search.component.ts @@ -2,7 +2,7 @@ import { Component, } from '@angular/core'; import { Observable } from 'rxjs'; import { Store } from '@ngrx/store'; - +import { SearchQueryParams } from '../store/model'; import * as searchReducer from '../store/search.reducer'; import * as searchSelector from '../store/search.selector'; @@ -16,7 +16,8 @@ export class SearchComponent { public criteriaStepChecked: Observable<boolean>; public outputStepChecked: Observable<boolean>; public resultStepChecked: Observable<boolean>; - public queryParams: Observable<any>; + public queryParams: Observable<SearchQueryParams>; + public outputListEmpty: Observable<boolean>; constructor(private store: Store<searchReducer.State>) { this.currentStep = store.select(searchSelector.getCurrentStep); @@ -25,5 +26,6 @@ export class SearchComponent { this.outputStepChecked = store.select(searchSelector.getOutputStepChecked); this.resultStepChecked = store.select(searchSelector.getResultStepChecked); this.queryParams = this.store.select(searchSelector.getQueryParams); + this.outputListEmpty = this.store.select(searchSelector.getOutputListEmpty); } } diff --git a/src/app/search/store/search.action.ts b/src/app/search/store/search.action.ts index 2a1082dab495d72614f7b0c970a3604b83a408fa..90eac783c805ad630d1d3b927059e3377c076f39 100644 --- a/src/app/search/store/search.action.ts +++ b/src/app/search/store/search.action.ts @@ -25,8 +25,10 @@ export const EXECUTE_PROCESS = '[Search] Execute Process'; export const EXECUTE_PROCESS_WIP = '[Search] Execute Process WIP'; export const EXECUTE_PROCESS_SUCCESS = '[Search] Execute Process Success'; export const EXECUTE_PROCESS_FAIL = '[Search] Execute Process Fail'; +export const OUTPUT_LIST_EMPTY = '[Search] Output List Empty'; export const DESTROY_RESULTS = '[Search] Destroy Results'; + export class InitSearchByUrl implements Action { type = INIT_SEARCH_BY_URL; @@ -166,6 +168,12 @@ export class ExecuteProcessFailAction implements Action { constructor(public payload: {} = null) { } } +export class OutputListEmptyAction implements Action { + type = OUTPUT_LIST_EMPTY; + + constructor(public payload: boolean) { } +} + export class DestroyResultsAction implements Action { type = DESTROY_RESULTS; @@ -196,4 +204,5 @@ export type Actions | ExecuteProcessWipAction | ExecuteProcessSuccessAction | ExecuteProcessFailAction + | OutputListEmptyAction | DestroyResultsAction; diff --git a/src/app/search/store/search.reducer.ts b/src/app/search/store/search.reducer.ts index 0ecb651833a6a5838f1e79f0ae255c361852b366..1b588d5271454a0dee0fc9eacc2b01e439dc950d 100644 --- a/src/app/search/store/search.reducer.ts +++ b/src/app/search/store/search.reducer.ts @@ -17,6 +17,7 @@ export interface State { processWip: boolean; processDone: boolean; processId: string; + outputListEmpty: boolean; } const initialState: State = { @@ -33,14 +34,14 @@ const initialState: State = { selectedData: [], processWip: false, processDone: false, - processId: null + processId: null, + outputListEmpty: null }; export function reducer(state: State = initialState, action: actions.Actions): State { switch (action.type) { case actions.CHANGE_STEP: const currentStep = action.payload as string; - return { ...state, currentStep @@ -48,7 +49,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.SELECT_DATASET: const datasetName = action.payload as string; - return { ...state, pristine: false, @@ -81,7 +81,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.UPDATE_CRITERIA_LIST: const criteriaList = action.payload as Criterion[]; - return { ...state, criteriaList @@ -89,7 +88,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.ADD_CRITERION: const criterion = action.payload as Criterion; - return { ...state, criteriaList: [...state.criteriaList, criterion] @@ -97,7 +95,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.DELETE_CRITERION: const id = action.payload as number; - return { ...state, criteriaList: [...state.criteriaList.filter(c => c.id !== id)] @@ -105,7 +102,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.UPDATE_OUTPUT_LIST: const outputList = action.payload as number[]; - return { ...state, outputList @@ -113,7 +109,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.RETRIEVE_DATA_SUCCESS: const searchData = action.payload as any[]; - return { ...state, searchData @@ -121,7 +116,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.GET_DATA_LENGTH_SUCCESS: const dataLength = action.payload as number; - return { ...state, dataLength @@ -129,7 +123,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.ADD_SELECTED_DATA: const addData = action.payload; - return { ...state, selectedData: [...state.selectedData, addData] @@ -137,7 +130,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.DELETE_SELECTED_DATA: const deleteData = action.payload; - return { ...state, selectedData: [...state.selectedData.filter(d => d !== deleteData)] @@ -152,7 +144,6 @@ export function reducer(state: State = initialState, action: actions.Actions): S case actions.EXECUTE_PROCESS_WIP: const processId = action.payload as string; - return { ...state, processId @@ -172,6 +163,13 @@ export function reducer(state: State = initialState, action: actions.Actions): S processDone: false, processId: null }; + + case actions.OUTPUT_LIST_EMPTY: + const outputListEmpty = action.payload as boolean; + return { + ...state, + outputListEmpty + }; case actions.DESTROY_RESULTS: return { @@ -199,3 +197,4 @@ export const getSelectedData = (state: State) => state.selectedData; export const getProcessWip = (state: State) => state.processWip; export const getProcessDone = (state: State) => state.processDone; export const getProcessId = (state: State) => state.processId; +export const getOutputListEmpty = (state: State) => state.outputListEmpty; diff --git a/src/app/search/store/search.selector.ts b/src/app/search/store/search.selector.ts index 94ffe0be1848843c55bdf9316d99065cf830b23a..5da37d114993bd744f41b2432588640b112058f1 100644 --- a/src/app/search/store/search.selector.ts +++ b/src/app/search/store/search.selector.ts @@ -106,3 +106,8 @@ export const getProcessId = createSelector( getSearchState, search.getProcessId ); + +export const getOutputListEmpty = createSelector( + getSearchState, + search.getOutputListEmpty +); diff --git a/src/styles.css b/src/styles.css index b3596e114feba7f7b342404c25a8abb3b4c53c79..e8a520362632e26739adb4582bb315586e439790 100644 --- a/src/styles.css +++ b/src/styles.css @@ -4,7 +4,6 @@ color: #7AC29A; } - /* Custom styles for external library components */ /* Needs to be in global stylesheet due to ViewEncapsulation */ .custom-accordion-output .panel-body {