diff --git a/src/app/metamodel/action/attribute.action.ts b/src/app/metamodel/action/attribute.action.ts index 4b1d08a0739faaa321933c4985ec069784138bff..199d3326144b3e44a4dcac872b3ecf729aac9dfe 100644 --- a/src/app/metamodel/action/attribute.action.ts +++ b/src/app/metamodel/action/attribute.action.ts @@ -4,6 +4,7 @@ import { Attribute } from '../model'; export const LOAD_ATTRIBUTE_SEARCH_META = '[Attribute] Load Attribute Search Meta'; export const LOAD_ATTRIBUTE_SEARCH_META_SUCCESS = '[Attribute] Load Attribute Search Meta Success'; +export const LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS = '[Attribute] Load Attribute Search Multiple Meta Success'; export const LOAD_ATTRIBUTE_SEARCH_META_FAIL = '[Attribute] Load Attribute Search Meta Fail'; export class LoadAttributeSearchMetaAction implements Action { @@ -18,6 +19,12 @@ export class LoadAttributeSearchMetaSuccessAction implements Action { constructor(public payload: Attribute[]) { } } +export class LoadAttributeSearchMultipleMetaSuccessAction implements Action { + readonly type = LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS; + + constructor(public payload: Attribute[]) { } +} + export class LoadAttributeSearchMetaFailAction implements Action { readonly type = LOAD_ATTRIBUTE_SEARCH_META_FAIL; @@ -27,4 +34,5 @@ export class LoadAttributeSearchMetaFailAction implements Action { export type Actions = LoadAttributeSearchMetaAction | LoadAttributeSearchMetaSuccessAction + | LoadAttributeSearchMultipleMetaSuccessAction | LoadAttributeSearchMetaFailAction; diff --git a/src/app/metamodel/effects/attribute.effects.ts b/src/app/metamodel/effects/attribute.effects.ts index fd22724f57bc88ddb66ada5938dcdccee357b92b..8b9d239dd47536680a4aeaaa44d3d6e50648da6a 100644 --- a/src/app/metamodel/effects/attribute.effects.ts +++ b/src/app/metamodel/effects/attribute.effects.ts @@ -3,30 +3,50 @@ import { Injectable } from '@angular/core'; import { ToastrService } from 'ngx-toastr'; import { Effect, Actions, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; -import { switchMap, map, catchError, tap } from 'rxjs/operators'; +import { switchMap, map, catchError, tap, withLatestFrom } from 'rxjs/operators'; import { Attribute } from '../model'; import * as attributeActions from '../action/attribute.action'; import { AttributeService } from '../services/attribute.service'; +import { Store } from "@ngrx/store"; +import * as fromRouter from "@ngrx/router-store"; +import * as utils from "../../shared/utils"; +import * as fromSearch from "../../search/store/search.reducer"; +import * as fromMetamodel from "../reducers"; +import * as fromConeSearch from "../../shared/cone-search/store/cone-search.reducer"; @Injectable() export class AttributeEffects { constructor( private actions$: Actions, private attributeService: AttributeService, - private toastr: ToastrService + private toastr: ToastrService, + private store$: Store<{ + router: fromRouter.RouterReducerState<utils.RouterStateUrl>, + search: fromSearch.State, + metamodel: fromMetamodel.State, + coneSearch: fromConeSearch.State + }> ) { } @Effect() loadAttributeSearchMetaAction$ = this.actions$.pipe( ofType(attributeActions.LOAD_ATTRIBUTE_SEARCH_META), - switchMap((action: attributeActions.LoadAttributeSearchMetaAction) => - this.attributeService.retrieveAttributeSearchMeta(action.payload).pipe( - map((attributeList: Attribute[]) => - new attributeActions.LoadAttributeSearchMetaSuccessAction(attributeList)), + withLatestFrom(this.store$), + switchMap(([action, state]) => { + const loadAttributeSearchMetaAction = action as attributeActions.LoadAttributeSearchMetaAction; + return this.attributeService.retrieveAttributeSearchMeta(loadAttributeSearchMetaAction.payload).pipe( + map((attributeList: Attribute[]) => { + const module: string = state.router.state.url.split('/')[1]; + if (module === 'search') { + new attributeActions.LoadAttributeSearchMetaSuccessAction(attributeList) + } else { + new attributeActions.LoadAttributeSearchMultipleMetaSuccessAction(attributeList) + } + }), catchError(() => of(new attributeActions.LoadAttributeSearchMetaFailAction())) ) - ) + }) ); @Effect({ dispatch: false }) diff --git a/src/app/search-multiple/components/index.ts b/src/app/search-multiple/components/index.ts index e9759b2e82a6247e29246651c3eea281e8d71cef..3ae2d9e2a671005cd1036bbd00537e54d4449778 100644 --- a/src/app/search-multiple/components/index.ts +++ b/src/app/search-multiple/components/index.ts @@ -4,6 +4,8 @@ import { DatasetListComponent } from './datasets/dataset-list.component'; import { DatasetsByProjetComponent } from './datasets/datasets-by-projet.component'; import { OverviewComponent } from './result/overview.component'; import { DatasetsResultComponent } from './result/datasets-result.component'; +import { DatatableComponent } from "../../shared/datatable/datatable.component"; +import { RendererComponents } from "../../shared/datatable/renderer"; export const dummiesComponents = [ ProgressBarMultipleComponent, @@ -11,5 +13,7 @@ export const dummiesComponents = [ DatasetListComponent, DatasetsByProjetComponent, OverviewComponent, - DatasetsResultComponent + DatasetsResultComponent, + DatatableComponent, + RendererComponents ]; \ No newline at end of file diff --git a/src/app/search-multiple/components/result/datasets-result.component.html b/src/app/search-multiple/components/result/datasets-result.component.html index f39830447585f11da10b4a58b5f72b69815c8649..95c9678d71345352d68c7aa7f4f7fc2454836e47 100644 --- a/src/app/search-multiple/components/result/datasets-result.component.html +++ b/src/app/search-multiple/components/result/datasets-result.component.html @@ -1,7 +1,7 @@ <div *ngIf="datasetsCountIsLoaded"> <accordion [isAnimated]="true"> <ng-container *ngFor="let dataset of getOrderedDatasetWithResults()"> - <accordion-group (isOpenChange)="retrieveData.emit(dataset.name)" #ag [panelClass]="'custom-accordion'" [isOpen]="false" class="my-2"> + <accordion-group (isOpenChange)="retrieveMeta.emit(dataset.name)" #ag [panelClass]="'custom-accordion'" [isOpen]="false" class="my-2"> <button class="btn btn-link btn-block clearfix" accordion-heading> <div class="pull-left float-left"> {{ dataset.label }} <span class="badge badge-pill badge-primary">{{ getCount(dataset.name) }}</span> @@ -14,27 +14,18 @@ </span> </div> </button> - toto +<!-- <app-datatable--> +<!-- [dataset]="dataset"--> +<!-- [datasetAttributeList]="getDatasetAttributeList(dataset.name)"--> +<!-- [outputList]="outputList"--> +<!-- [data]="data"--> +<!-- [dataLength]="getCount(dataset.name)"--> +<!-- (getData)="getData(dataset.name, $event)"--> +<!-- (addSelectedData)="addSelectedData($event)"--> +<!-- (deleteSelectedData)="deleteSelectedData($event)"--> +<!-- (executeProcess)="executeProcess($event)">--> +<!-- </app-datatable>--> </accordion-group> </ng-container> </accordion> -</div> - - -<!-- <app-result-datatable--> -<!-- [datasetName]="datasetName | async"--> -<!-- [datasetList]="datasetList | async"--> -<!-- [queryParams]="queryParams | async"--> -<!-- [datasetAttributeList]="datasetAttributeList | async"--> -<!-- [outputList]="outputList | async"--> -<!-- [searchData]="searchData | async"--> -<!-- [dataLength]="dataLength | async"--> -<!-- [selectedData]="selectedData | async"--> -<!-- (getSearchData)="getSearchData($event)"--> -<!-- (addSelectedData)="addSearchData($event)"--> -<!-- (deleteSelectedData)="deleteSearchData($event)"--> -<!-- [processWip]="processWip | async"--> -<!-- [processDone]="processDone | async"--> -<!-- [processId]="processId | async"--> -<!-- (executeProcess)="executeProcess($event)">--> -<!-- </app-result-datatable>--> \ No newline at end of file +</div> \ No newline at end of file diff --git a/src/app/search-multiple/components/result/datasets-result.component.ts b/src/app/search-multiple/components/result/datasets-result.component.ts index 386ff6148a481bae9a9260866f1dd10bf066e404..c892de98b81b339d3e130d1f1b8b1265187fe6c6 100644 --- a/src/app/search-multiple/components/result/datasets-result.component.ts +++ b/src/app/search-multiple/components/result/datasets-result.component.ts @@ -1,8 +1,8 @@ -import {Component, Input, Output, ChangeDetectionStrategy, EventEmitter} from '@angular/core'; +import { Component, Input, Output, ChangeDetectionStrategy, EventEmitter } from '@angular/core'; -import { Dataset, Project } from "../../../metamodel/model"; +import { Attribute, Dataset, Project } from "../../../metamodel/model"; import { DatasetCount } from "../../store/model"; -import {sortByDisplay} from "../../../shared/utils"; +import { sortByDisplay } from "../../../shared/utils"; @Component({ selector: 'app-datasets-result', @@ -14,7 +14,10 @@ export class DatasetsResultComponent { @Input() projectList: Project[]; @Input() datasetList: Dataset[]; @Input() selectedDatasets: string[]; + // TODO: change any type + @Input() datasetsAttributeList: any; @Input() datasetsCount: DatasetCount[]; + @Output() retrieveMeta: EventEmitter<string> = new EventEmitter(); @Output() retrieveData: EventEmitter<string> = new EventEmitter(); getOrderedDatasetWithResults(): Dataset[] { @@ -37,4 +40,24 @@ export class DatasetsResultComponent { getCount(dname: string): number { return this.datasetsCount.find(c => c.dname === dname).count; } + + // getDatasetAttributeList(dname: string): Attribute[] { + // + // } + + getData(dname: string, pagination: any): void { + console.log(dname, pagination); + } + + addSelectedData(d: number | string): void { + console.log('addSelectedData: ' + d); + } + + deleteSelectedData(d: number | string): void { + console.log('deleteSelectedData: ' + d); + } + + executeProcess(typeProcess: string): void { + console.log('executeProcess: ' + typeProcess); + } } \ No newline at end of file diff --git a/src/app/search-multiple/containers/result-multiple.component.html b/src/app/search-multiple/containers/result-multiple.component.html index d066d5d2ccdc24938bb4b046c60acf83f9f16705..f39ce987348fb8aee84bcfa8b161a36a21eca0c6 100644 --- a/src/app/search-multiple/containers/result-multiple.component.html +++ b/src/app/search-multiple/containers/result-multiple.component.html @@ -17,7 +17,7 @@ [datasetList]="datasetList | async" [selectedDatasets]="selectedDatasets | async" [datasetsCount]="datasetsCount | async" - (retrieveData)="retrieveData($event)"> + (retrieveMeta)="retrieveMeta($event)"> </app-datasets-result> </div> </div> diff --git a/src/app/search-multiple/containers/result-multiple.component.ts b/src/app/search-multiple/containers/result-multiple.component.ts index 43f43a134344e1c5eba39b0412c4099535c44b24..b6a2cd186e80213ee0a96e4dd0cfb6f8209b2896 100644 --- a/src/app/search-multiple/containers/result-multiple.component.ts +++ b/src/app/search-multiple/containers/result-multiple.component.ts @@ -8,6 +8,7 @@ import { Attribute, Dataset, Project } from '../../metamodel/model'; import { ConeSearch } from "../../shared/cone-search/store/model"; import * as searchMultipleActions from '../store/search-multiple.action'; import * as datasetActions from '../../metamodel/action/dataset.action'; +import * as attributeActions from '../../metamodel/action/attribute.action'; import * as fromSearchMultiple from '../store/search-multiple.reducer'; import * as fromMetamodel from '../../metamodel/reducers'; import * as searchMultipleSelector from '../store/search-multiple.selector'; @@ -77,8 +78,8 @@ export class ResultMultipleComponent implements OnInit, OnDestroy { this.store.dispatch(new searchMultipleActions.RetrieveDatasetsCountAction()); } - retrieveData(dname: string): void { - console.log(dname); + retrieveMeta(dname: string): void { + this.store.dispatch(new attributeActions.LoadAttributeSearchMetaAction(dname)); } // getSearchData(pagination: [number, number, number, string]): void { diff --git a/src/app/search-multiple/store/search-multiple.effects.ts b/src/app/search-multiple/store/search-multiple.effects.ts index 06b14bef8ff4b46eef86c08bccb9df393cc75ba2..21a48f039b3f183f203bcb7abd2f52cd2f0b7a05 100644 --- a/src/app/search-multiple/store/search-multiple.effects.ts +++ b/src/app/search-multiple/store/search-multiple.effects.ts @@ -20,6 +20,7 @@ import * as datasetActions from "../../metamodel/action/dataset.action"; import {Attribute, Dataset, Family, Project} from "../../metamodel/model"; import * as searchActions from "../../search/store/search.action"; import * as documentationActions from "../../documentation/store/documentation.action"; +import * as attributeActions from "../../metamodel/action/attribute.action"; @Injectable() export class SearchMultipleEffects { @@ -122,6 +123,117 @@ export class SearchMultipleEffects { tap(_ => this.toastr.error('Loading Failed!', 'The data count of datasets loading failed')) ); + @Effect() + loadDatasetAttributeListSuccessAction$ = this.actions$.pipe( + ofType(attributeActions.LOAD_ATTRIBUTE_SEARCH_MULTIPLE_META_SUCCESS), + withLatestFrom(this.store$), + switchMap(([action, state]) => { + const loadAttributeSearchMultipleMetaSuccessAction = action as attributeActions.LoadAttributeSearchMultipleMetaSuccessAction; + console.log('MODULE MULTIPLE'); + + return of(); + + // const actions: Action[] = []; + // + // let defaultOutputList = loadAttributeSearchMetaSuccessAction.payload + // .filter(attribute => attribute.selected && attribute.id_output_category) + // .sort((a, b) => a.output_display - b.output_display) + // .map(attribute => attribute.id); + // + // if (state.router.state.queryParams.a) { + // defaultOutputList = state.router.state.queryParams.a.split(';').map((o: string) => parseInt(o, 10)); + // } + // + // actions.push(new searchActions.UpdateOutputListAction(defaultOutputList)); + // + // let defaultCriteriaList = loadAttributeSearchMetaSuccessAction.payload + // .filter(attribute => attribute.id_criteria_family && attribute.search_type && attribute.min) + // .map(attribute => { + // switch (attribute.search_type) { + // case 'field': + // case 'select': + // case 'datalist': + // case 'radio': + // case 'date': + // case 'date-time': + // case 'time': + // return { id: attribute.id, type: 'field', value: attribute.min.toString(), operator: attribute.operator }; + // case 'list': + // return { id: attribute.id, type: 'list', values: attribute.min.toString().split('|') }; + // case 'between': + // case 'between-date': + // return { id: attribute.id, type: 'between', min: attribute.min.toString(), max: attribute.max.toString() }; + // case 'select-multiple': + // case 'checkbox': + // const msValues = attribute.min.toString().split('|'); + // const options = attribute.options.filter(option => msValues.includes(option.value)); + // return { id: attribute.id, type: 'multiple', options }; + // case 'json': + // const [path, operator, value] = attribute.min.toString().split('|'); + // return { id: attribute.id, type: 'json', path, operator, value }; + // + // default: + // return null; + // } + // }); + // + // if (state.router.state.queryParams.c) { + // defaultCriteriaList = state.router.state.queryParams.c.split(';').map((c: string) => { + // const params = c.split('::'); + // const attribute = loadAttributeSearchMetaSuccessAction.payload.find(a => a.id === parseInt(params[0], 10)); + // switch (attribute.search_type) { + // case 'field': + // case 'select': + // case 'datalist': + // case 'radio': + // case 'date': + // case 'date-time': + // case 'time': + // return { id: parseInt(params[0], 10), type: 'field', operator: params[1], value: params[2] }; + // case 'list': + // return { id: parseInt(params[0], 10), type: 'list', values: params[2].split('|') }; + // case 'between': + // case 'between-date': + // if (params[1] === 'bw') { + // const bwValues = params[2].split('|'); + // return { id: parseInt(params[0], 10), type: 'between', min: bwValues[0], max: bwValues[1] }; + // } else if (params[1] === 'gte') { + // return { id: parseInt(params[0], 10), type: 'between', min: params[2], max: null }; + // } else { + // return { id: parseInt(params[0], 10), type: 'between', min: null, max: params[2] }; + // } + // case 'select-multiple': + // case 'checkbox': + // const msValues = params[2].split('|'); + // const options = attribute.options.filter(option => msValues.includes(option.value)); + // return { id: parseInt(params[0], 10), type: 'multiple', options }; + // case 'json': + // const [path, operator, value] = params[2].split('|'); + // return { id: parseInt(params[0], 10), type: 'json', path, operator, value }; + // + // default: + // return null; + // } + // }); + // } + // + // actions.push(new searchActions.UpdateCriteriaListAction(defaultCriteriaList)); + // + // if (state.router.state.queryParams.cs) { + // const params = state.router.state.queryParams.cs.split(':'); + // const coneSearch: ConeSearch = { + // ra: parseFloat(params[0]), + // dec: parseFloat(params[1]), + // radius: parseInt(params[2], 10) + // }; + // actions.push(new searchActions.IsConeSearchAddedAction(true)); + // actions.push(new coneSearchActions.AddConeSearchAction(coneSearch)); + // } + // + // return actions; + }) + ); + // @Effect() // retrieveDataAction$ = this.actions$.pipe( // ofType(searchMultipleActions.RETRIEVE_DATA), diff --git a/src/app/shared/datatable/datatable.component.css b/src/app/shared/datatable/datatable.component.css new file mode 100644 index 0000000000000000000000000000000000000000..19bd4e74589df063181a8bf5fd71302d12bca16f --- /dev/null +++ b/src/app/shared/datatable/datatable.component.css @@ -0,0 +1,32 @@ +.data-selected { + cursor: pointer; +} + +.data-selected button:focus { + box-shadow: none; +} + +ul { + margin-bottom: 0; +} + +.custom-select { + width: fit-content; +} + +.clickable:hover { + cursor: pointer; + background-color: #F7F7F7; +} + +.unsorted { + color: #c5c5c5; +} + +.clickable:hover .unsorted, .on-hover, .inactive, .clickable:hover .active { + display: none; +} + +.clickable:hover .on-hover, .clickable:hover .inactive { + display: inline; +} diff --git a/src/app/shared/datatable/datatable.component.html b/src/app/shared/datatable/datatable.component.html new file mode 100644 index 0000000000000000000000000000000000000000..24e9cc554c037b381ce9e2ea586ccd2dca6da59e --- /dev/null +++ b/src/app/shared/datatable/datatable.component.html @@ -0,0 +1,127 @@ +<div *ngIf="!dataLength || !outputList || !datasetAttributeList || !data" class="text-center"> + <span class="fas fa-circle-notch fa-spin fa-3x"></span> + <span class="sr-only">Loading...</span> +</div> +<div *ngIf="dataLength && outputList && datasetAttributeList && data"> + <div *ngIf="dataset.selectable_row" class="mb-2"> + <button [disabled]="noSelectedData() || processWip" (click)="emitProcess('csv')" + class="btn btn-sm btn-outline-primary"> + To CSV + </button> + <span *ngIf="processWip" class="float-right mr-2"> + <span class="fas fa-circle-notch fa-spin fa-2x"></span> + </span> + <a *ngIf="processDone" href="http://0.0.0.0:8085/{{ processId }}.csv" + class="btn btn-sm btn-outline-secondary float-right"> + Download your CSV + </a> + </div> + <div class="table-responsive"> + <table class="table table-bordered table-hover"> + <thead> + <tr> + <th *ngIf="dataset.selectable_row"></th> + <th *ngFor="let attribute of getOutputList()" scope="col" class="clickable" (click)="sort(attribute.id)"> + {{ attribute.label }} + <span *ngIf="attribute.id === sortedCol" class="pl-2"> + <span [ngClass]="{'active': sortedOrder === 'a', 'inactive': sortedOrder === 'd'}"> + <span class="fas fa-sort-amount-down-alt"></span> + </span> + <span [ngClass]="{'active': sortedOrder === 'd', 'inactive': sortedOrder === 'a'}"> + <span class="fas fa-sort-amount-up"></span> + </span> + </span> + <span *ngIf="attribute.id !== sortedCol" class="pl-2"> + <span class="unsorted"> + <span class="fas fa-arrows-alt-v"></span> + </span> + <span class="on-hover"> + <span class="fas fa-sort-amount-down-alt"></span> + </span> + </span> + </th> + </tr> + </thead> + <tbody> + <tr *ngFor="let datum of data"> + <td *ngIf="dataset.selectable_row" class="data-selected" + (click)="toggleSelection(datum)"> + <button class="btn btn-block text-left p-0 m-0"> + <div *ngIf="!isSelected(datum)"> + <span class="far fa-square fa-lg text-secondary"></span> + </div> + <div *ngIf="isSelected(datum)"> + <span class="fas fa-check-square fa-lg theme-color"></span> + </div> + </button> + </td> + <td *ngFor="let attribute of getOutputList()" class="align-middle"> + <div *ngIf="datum[attribute.label]" [ngSwitch]="attribute.renderer"> + <div *ngSwitchCase="'detail'"> + <app-detail + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-detail> + </div> + <div *ngSwitchCase="'link'"> + <app-link + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-link> + </div> + <div *ngSwitchCase="'download'"> + <app-download + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-download> + </div> + <div *ngSwitchCase="'image'"> + <app-image + [value]="datum[attribute.label]" + [datasetName]="dataset.name" + [config]="attribute.renderer_config"> + </app-image> + </div> + <div *ngSwitchCase="'json'"> + <app-json + [value]="datum[attribute.label]" + [attributeLabel]="attribute.label" + [config]="attribute.renderer_config"> + </app-json> + </div> + <div *ngSwitchDefault> + {{ datum[attribute.label] }} + </div> + </div> + </td> + </tr> + </tbody> + </table> + </div> + <div class="row mt-3"> + <div class="col"> + Showing + <select class="custom-select" (change)="changeNbItems($event.target.value)"> + <option value="10" selected="true">10</option> + <option value="20">20</option> + <option value="50">50</option> + <option value="100">100</option> + </select> + of {{ dataLength }} items + </div> + <div class="col-auto"> + <pagination + [totalItems]="dataLength" + [boundaryLinks]="true" + [rotate]="true" + [maxSize]="5" + [itemsPerPage]="nbItems" + previousText="‹" nextText="›" firstText="«" lastText="»" + (pageChanged)="changePage($event.page)"> + </pagination> + </div> + </div> +</div> \ No newline at end of file diff --git a/src/app/shared/datatable/datatable.component.spec.ts b/src/app/shared/datatable/datatable.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e7cdcadd7c4268a47a65eda14a6d2fa7db108856 --- /dev/null +++ b/src/app/shared/datatable/datatable.component.spec.ts @@ -0,0 +1,193 @@ +// import { ComponentFixture, TestBed } from '@angular/core/testing'; +// import { Component, Input } from '@angular/core'; +// import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +// +// import { AccordionModule } from 'ngx-bootstrap/accordion'; +// import { DatatableSectionComponent } from './datatable.component'; +// import { Dataset } from '../../../metamodel/model'; +// import { ATTRIBUTE_LIST, DATASET_LIST } from '../../../../settings/test-data'; +// +// describe('[Search][Result] Component: DatatableComponent', () => { +// @Component({ selector: 'app-img', template: '' }) +// class ImgStubComponent { +// @Input() src: string; +// } +// +// @Component({ selector: 'app-thumbnail', template: '' }) +// class ThumbnailStubComponent { +// @Input() src: string; +// @Input() attributeName: string; +// } +// +// @Component({ selector: 'app-link', template: '' }) +// class LinkStubComponent { +// @Input() href: string; +// } +// +// @Component({ selector: 'app-btn', template: '' }) +// class BtnStubComponent { +// @Input() href: string; +// } +// +// @Component({ selector: 'app-detail', template: '' }) +// class DetailStubComponent { +// @Input() style: string; +// @Input() datasetName: string; +// @Input() data: string | number; +// } +// +// @Component({ selector: 'app-download', template: '' }) +// class DownloadStubComponent { +// @Input() href: string; +// } +// +// @Component({ selector: 'app-json-renderer', template: '' }) +// class JsonStubComponent { +// @Input() attributeName: string; +// @Input() json: string; +// } +// +// @Component({ selector: 'pagination', template: '' }) +// class PaginationStubComponent { +// @Input() totalItems: number; +// @Input() boundaryLinks: boolean; +// @Input() rotate: boolean; +// @Input() maxSize: number; +// } +// +// let component: DatatableSectionComponent; +// let fixture: ComponentFixture<DatatableSectionComponent>; +// +// beforeEach(() => { +// TestBed.configureTestingModule({ +// declarations: [ +// DatatableSectionComponent, +// ImgStubComponent, +// ThumbnailStubComponent, +// LinkStubComponent, +// BtnStubComponent, +// DetailStubComponent, +// DownloadStubComponent, +// JsonStubComponent, +// PaginationStubComponent +// ], +// imports: [AccordionModule.forRoot(), BrowserAnimationsModule] +// }); +// fixture = TestBed.createComponent(DatatableSectionComponent); +// component = fixture.componentInstance; +// }); +// +// it('should create the component', () => { +// expect(component).toBeTruthy(); +// }); +// +// it('#isDatatableOpened() should return if datatable has to be opened or not', () => { +// component.datasetList = DATASET_LIST; +// component.datasetName = 'cat_1'; +// expect(component.isDatatableOpened()).toBeTruthy(); +// component.datasetName = 'cat_2'; +// expect(component.isDatatableOpened()).toBeFalsy(); +// }); +// +// it('#getOutputList() should return filtered output list', () => { +// component.outputList = [2] +// component.datasetAttributeList = ATTRIBUTE_LIST; +// expect(component.getOutputList().length).toBe(1); +// }); +// +// it('#getDataset() should return dataset object', () => { +// component.datasetList = DATASET_LIST; +// component.datasetName = 'cat_1'; +// const dataset: Dataset = component.getDataset(); +// expect(dataset.name).toBe('cat_1'); +// expect(dataset.label).toBe('Cat 1'); +// }); +// +// it('#getAttributeId(attributeName) should return id of attributeName', () => { +// component.datasetAttributeList = ATTRIBUTE_LIST; +// expect(component.getAttributeId('name_one')).toBe(1); +// }); +// +// it('#toggleSelection(datum) should return add datum to selectedData', () => { +// const datum = { label_one: 123456 }; +// component.datasetAttributeList = ATTRIBUTE_LIST; +// component.selectedData = []; +// component.addSelectedData.subscribe((event: any) => expect(event).toBe(123456)); +// component.toggleSelection(datum); +// }); +// +// it('#toggleSelection(datum) should return remove datum to selectedData', () => { +// const datum = { label_one: 123456 }; +// component.selectedData = [123456]; +// component.datasetAttributeList = ATTRIBUTE_LIST; +// component.deleteSelectedData.subscribe((event: any) => expect(event).toBe(123456)); +// component.toggleSelection(datum); +// }); +// +// it('#isSelected(datum) should return true datum is selected', () => { +// const datum = { label_one: 123456 }; +// component.datasetAttributeList = ATTRIBUTE_LIST; +// component.selectedData = [123456]; +// expect(component.isSelected(datum)).toBeTruthy(); +// }); +// +// it('#isSelected(datum) should return false datum is not selected', () => { +// const datum = { label_one: 123456 }; +// component.datasetAttributeList = ATTRIBUTE_LIST; +// component.selectedData = []; +// expect(component.isSelected(datum)).toBeFalsy(); +// }); +// +// it('#noSelectedData() should return true if no selectedData', () => { +// component.selectedData = []; +// expect(component.noSelectedData()).toBeTruthy(); +// }); +// +// it('#noSelectedData() should return false if there are selectedData', () => { +// component.selectedData = [123456]; +// expect(component.noSelectedData()).toBeFalsy(); +// }); +// +// it('#emitProcess() should raise executeProcess event and transmit process type', () => { +// component.executeProcess.subscribe((event: string) => expect(event).toBe('test')); +// component.emitProcess('test'); +// }); +// +// it('#changePage() should change page value and raise getSearchData event', () => { +// component.sortedCol = 1; +// component.sortedOrder = 'a'; +// component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([2, 10, 1, 'a'])); +// component.changePage(2); +// }); +// +// it('#changeNbItems() should change nbItems value and raise getSearchData event', () => { +// component.sortedCol = 1; +// component.sortedOrder = 'a'; +// component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 20, 1, 'a'])); +// component.changeNbItems(20); +// }); +// +// it('#ngOnInit() should init sortedCol and sortedOrder values', () => { +// component.datasetAttributeList = ATTRIBUTE_LIST; +// component.ngOnInit(); +// expect(component.sortedCol).toEqual(1); +// expect(component.sortedOrder).toBe('a'); +// }); +// +// it('#sort() should raise getSearchData event with correct parameters', () => { +// component.sortedOrder = 'a'; +// let subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'a'])); +// component.sort(1); +// subscribtion.unsubscribe(); +// component.sortedCol = 1; +// component.sortedOrder = 'a'; +// subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'd'])); +// component.sort(1); +// subscribtion.unsubscribe(); +// component.sortedCol = 1; +// component.sortedOrder = 'd'; +// subscribtion = component.getSearchData.subscribe((event: [number, number, number, string]) => expect(event).toEqual([1, 10, 1, 'a'])); +// component.sort(1); +// }); +// }); +// diff --git a/src/app/shared/datatable/datatable.component.ts b/src/app/shared/datatable/datatable.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..02d0823af89086358cfcc36b501d5a5981a29bc0 --- /dev/null +++ b/src/app/shared/datatable/datatable.component.ts @@ -0,0 +1,97 @@ +import { Component, Input, ChangeDetectionStrategy, Output, EventEmitter, ViewEncapsulation, OnInit } from '@angular/core'; + +// import { SearchQueryParams } from '../../store/model'; +import { Attribute, Dataset } from 'src/app/metamodel/model'; + + +@Component({ + selector: 'app-datatable', + templateUrl: 'datatable.component.html', + styleUrls: ['datatable.component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, + encapsulation: ViewEncapsulation.None +}) +export class DatatableComponent implements OnInit { + @Input() dataset: Dataset; + // @Input() queryParams: SearchQueryParams; + @Input() datasetAttributeList: Attribute[]; + @Input() outputList: number[]; + @Input() data: any[]; + @Input() dataLength: number; + @Input() selectedData: any[] = []; + @Input() processWip: boolean = false; + @Input() processDone: boolean = false; + @Input() processId: string = null; + @Output() getData: EventEmitter<[number, number, number, string]> = new EventEmitter(); + @Output() addSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() deleteSelectedData: EventEmitter<number | string> = new EventEmitter(); + @Output() executeProcess: EventEmitter<string> = new EventEmitter(); + nbItems = 10; + page = 1; + sortedCol: number = null; + sortedOrder: string = null; + + ngOnInit() { + this.sortedCol = this.datasetAttributeList.find(a => a.search_flag === 'ID').id; + this.sortedOrder = 'a'; + this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]); + } + + getOutputList(): Attribute[] { + return this.datasetAttributeList + .filter(a => this.outputList.includes(a.id)) + .sort((a, b) => a.output_display - b.output_display); + } + + getAttributeId(attributeName: string): number { + const attribute = this.datasetAttributeList.find(a => a.name === attributeName); + return attribute.id; + } + + toggleSelection(datum: any): void { + const attribute = this.datasetAttributeList.find(a => a.search_flag === 'ID'); + + const index = this.selectedData.indexOf(datum[attribute.label]); + if (index > -1) { + this.deleteSelectedData.emit(datum[attribute.label]); + } else { + this.addSelectedData.emit(datum[attribute.label]); + } + } + + isSelected(datum: any): boolean { + const attribute = this.datasetAttributeList.find(a => a.search_flag === 'ID'); + + if (this.selectedData.indexOf(datum[attribute.label]) > -1) { + return true; + } + return false; + } + + noSelectedData(): boolean { + return this.selectedData.length < 1; + } + + emitProcess(typeProcess: string): void { + this.executeProcess.emit(typeProcess); + } + + changePage(nb: number): void { + this.page = nb; + this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]); + } + changeNbItems(nb: number): void { + this.nbItems = nb; + this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]); + } + + sort(id: number): void { + if (id === this.sortedCol) { + this.sortedOrder = this.sortedOrder === 'a' ? 'd' : 'a'; + } else { + this.sortedCol = id; + this.sortedOrder = 'a'; + } + this.getData.emit([this.page, this.nbItems, this.sortedCol, this.sortedOrder]); + } +} diff --git a/src/app/shared/datatable/renderer/detail.component.html b/src/app/shared/datatable/renderer/detail.component.html new file mode 100644 index 0000000000000000000000000000000000000000..0b6336b1e1acb9cf348dc857b43bd2fedc62ddc2 --- /dev/null +++ b/src/app/shared/datatable/renderer/detail.component.html @@ -0,0 +1,4 @@ +<a routerLink="/detail/{{ datasetName }}/{{ value }}" [target]="config.blank == true ? '_blank' : '_self'" + [ngClass]="{'btn btn-outline-primary btn-sm' : config.display == 'text-button'}"> + {{ value }} +</a> diff --git a/src/app/shared/datatable/renderer/detail.component.spec.ts b/src/app/shared/datatable/renderer/detail.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..eb79818742ce02ac9ed386dbdad2dc9c6f98008a --- /dev/null +++ b/src/app/shared/datatable/renderer/detail.component.spec.ts @@ -0,0 +1,22 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { DetailComponent } from './detail.component'; + +describe('[Search][Result][Renderer] Component: DetailComponent', () => { + let component: DetailComponent; + let fixture: ComponentFixture<DetailComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DetailComponent], + imports: [RouterTestingModule] + }); + fixture = TestBed.createComponent(DetailComponent); + component = fixture.componentInstance; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/datatable/renderer/detail.component.ts b/src/app/shared/datatable/renderer/detail.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..900ed6c9cc0ed894cdb9c89e4ed00e75d140aa41 --- /dev/null +++ b/src/app/shared/datatable/renderer/detail.component.ts @@ -0,0 +1,19 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { RendererConfig } from "../../../metamodel/model"; + +interface DetailConfig extends RendererConfig { + display: string; + blank: boolean; +} + +@Component({ + selector: 'app-detail', + templateUrl: 'detail.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DetailComponent { + @Input() value: string | number; + @Input() datasetName: string; + @Input() config: DetailConfig; +} diff --git a/src/app/shared/datatable/renderer/download.component.html b/src/app/shared/datatable/renderer/download.component.html new file mode 100644 index 0000000000000000000000000000000000000000..430f6d477c5e259505a4abf00839fd14a9df8856 --- /dev/null +++ b/src/app/shared/datatable/renderer/download.component.html @@ -0,0 +1,4 @@ +<a [href]="getHref()" [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button')}"> + <span *ngIf="config.display !== 'icon-button'">{{ config.text }}</span> + <span *ngIf="config.display === 'icon-button'" class="{{config.icon}}"></span> +</a> \ No newline at end of file diff --git a/src/app/shared/datatable/renderer/download.component.spec.ts b/src/app/shared/datatable/renderer/download.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..47a8674d2aefe3cba0dfa41187ada36822a2661c --- /dev/null +++ b/src/app/shared/datatable/renderer/download.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { DownloadComponent } from './download.component'; +import { environment } from "../../../../../environments/environment"; + +describe('[Search][Result][Renderer] Component: DownloadComponent', () => { + let component: DownloadComponent; + let fixture: ComponentFixture<DownloadComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [DownloadComponent] + }); + fixture = TestBed.createComponent(DownloadComponent); + component = fixture.componentInstance; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('#getHref() should return file url', () => { + component.datasetName = 'dname'; + component.value = 'val'; + expect(component.getHref()).toBe(environment.apiUrl + '/download-file/dname/val'); + }); +}); diff --git a/src/app/shared/datatable/renderer/download.component.ts b/src/app/shared/datatable/renderer/download.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..bbd64eeff295c5bb5ecdba5ee4f32358709fc3fe --- /dev/null +++ b/src/app/shared/datatable/renderer/download.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { RendererConfig } from '../../../metamodel/model'; +import { getHost } from "../../utils"; + +interface LinkConfig extends RendererConfig { + display: string; + text: string; + icon: string; +} + +@Component({ + selector: 'app-download', + templateUrl: 'download.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DownloadComponent { + @Input() value: string; + @Input() datasetName: string; + @Input() config: LinkConfig; + + getHref(): string { + return getHost() + '/download-file/' + this.datasetName + '/' + this.value; + } +} diff --git a/src/app/shared/datatable/renderer/image.component.html b/src/app/shared/datatable/renderer/image.component.html new file mode 100644 index 0000000000000000000000000000000000000000..471be7f6a950997b0bf1519feebc7affe4c3b94d --- /dev/null +++ b/src/app/shared/datatable/renderer/image.component.html @@ -0,0 +1 @@ +<img [src]="getValue()" [alt]="getValue()"> \ No newline at end of file diff --git a/src/app/shared/datatable/renderer/image.component.spec.ts b/src/app/shared/datatable/renderer/image.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..c89f860dbe86aff3f9bc65f2b174d7f4e72e4ca7 --- /dev/null +++ b/src/app/shared/datatable/renderer/image.component.spec.ts @@ -0,0 +1,27 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ImageComponent } from './image.component'; +import { environment } from "../../../../../environments/environment"; + +describe('[Search][Result][Renderer] Component: ImageComponent', () => { + let component: ImageComponent; + let fixture: ComponentFixture<ImageComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ImageComponent] + }); + fixture = TestBed.createComponent(ImageComponent); + component = fixture.componentInstance; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('#getValue() should return image url', () => { + component.datasetName = 'dname'; + component.value = 'val'; + expect(component.getValue()).toEqual(environment.apiUrl + '/download-file/dname/val'); + }); +}); diff --git a/src/app/shared/datatable/renderer/image.component.ts b/src/app/shared/datatable/renderer/image.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..29477adb3ea4c6ec60d84debdebbacc782916963 --- /dev/null +++ b/src/app/shared/datatable/renderer/image.component.ts @@ -0,0 +1,25 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { RendererConfig } from '../../../metamodel/model'; +import { getHost } from "../../utils"; + +interface ImageConfig extends RendererConfig { + display: string; + dataset_file: boolean; + blank: boolean; +} + +@Component({ + selector: 'app-image', + templateUrl: 'image.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ImageComponent { + @Input() value: string | number; + @Input() datasetName: string; + @Input() config: ImageConfig; + + getValue(): string { + return getHost() + '/download-file/' + this.datasetName + '/' + this.value; + } +} diff --git a/src/app/shared/datatable/renderer/index.ts b/src/app/shared/datatable/renderer/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..ce40ca2d3d5111d1125744c23658002281bd0451 --- /dev/null +++ b/src/app/shared/datatable/renderer/index.ts @@ -0,0 +1,13 @@ +import { DetailComponent } from './detail.component'; +import { ImageComponent } from './image.component'; +import { JsonComponent } from './json.component'; +import { LinkComponent } from './link.component'; +import { DownloadComponent } from './download.component'; + +export const RendererComponents = [ + DetailComponent, + ImageComponent, + JsonComponent, + LinkComponent, + DownloadComponent +]; diff --git a/src/app/shared/datatable/renderer/json.component.html b/src/app/shared/datatable/renderer/json.component.html new file mode 100644 index 0000000000000000000000000000000000000000..95f6bf5cbac116ba299049a5cbbf7c9be31549c9 --- /dev/null +++ b/src/app/shared/datatable/renderer/json.component.html @@ -0,0 +1,15 @@ +<button class="btn btn-outline-primary btn-sm" (click)="openModal(modal)"> + JSON +</button> + +<ng-template #modal> + <div class="modal-header"> + <h4 class="modal-title pull-left">{{ attributeLabel }}</h4> + <button type="button" class="close pull-right" aria-label="Close" (click)="modalRef.hide()"> + <span aria-hidden="true">×</span> + </button> + </div> + <div class="modal-body"> + <ngx-json-viewer [json]="value"></ngx-json-viewer> + </div> +</ng-template> \ No newline at end of file diff --git a/src/app/shared/datatable/renderer/json.component.spec.ts b/src/app/shared/datatable/renderer/json.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..e8b0896a8b169139c0df831be80ef22df7832e47 --- /dev/null +++ b/src/app/shared/datatable/renderer/json.component.spec.ts @@ -0,0 +1,25 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NgxJsonViewerModule } from 'ngx-json-viewer'; +import { ModalModule } from 'ngx-bootstrap/modal'; +import { BsModalService } from 'ngx-bootstrap/modal'; + +import { JsonComponent } from './json.component'; + +describe('[Search][Result][Renderer] Component: JsonComponent', () => { + let component: JsonComponent; + let fixture: ComponentFixture<JsonComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [JsonComponent], + imports: [NgxJsonViewerModule, ModalModule.forRoot()], + providers: [BsModalService] + }); + fixture = TestBed.createComponent(JsonComponent); + component = fixture.componentInstance; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/shared/datatable/renderer/json.component.ts b/src/app/shared/datatable/renderer/json.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..9963350fde91d1ea691faa3e1a0986eb08e5abab --- /dev/null +++ b/src/app/shared/datatable/renderer/json.component.ts @@ -0,0 +1,29 @@ +import { Component, ChangeDetectionStrategy, Input, TemplateRef } from '@angular/core'; +import { BsModalService, BsModalRef } from 'ngx-bootstrap/modal'; + +import { RendererConfig } from '../../../metamodel/model'; + +interface JsonConfig extends RendererConfig { +} + +@Component({ + selector: 'app-json', + templateUrl: 'json.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class JsonComponent { + @Input() value: string | number; + @Input() attributeLabel: string; + @Input() config: JsonConfig; + + modalRef: BsModalRef; + + constructor(private modalService: BsModalService) { } + + openModal(template: TemplateRef<any>) { + this.modalRef = this.modalService.show( + template, + Object.assign({}, { class: 'modal-fit-content' }) + ); + } +} diff --git a/src/app/shared/datatable/renderer/link.component.html b/src/app/shared/datatable/renderer/link.component.html new file mode 100644 index 0000000000000000000000000000000000000000..2ec236b52990cc59eb07abbfdb083c94ff56f00d --- /dev/null +++ b/src/app/shared/datatable/renderer/link.component.html @@ -0,0 +1,5 @@ +<a [href]="getValue()" target="{{(config.blank) ? '_blank' : '_self'}}" + [ngClass]="{'btn btn-outline-primary btn-sm': (config.display=='text-button' || config.display=='icon-button')}"> + <span *ngIf="config.display !== 'icon-button'">{{ getText() }}</span> + <span *ngIf="config.display === 'icon-button'" class="{{config.icon}}"></span> +</a> \ No newline at end of file diff --git a/src/app/shared/datatable/renderer/link.component.spec.ts b/src/app/shared/datatable/renderer/link.component.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..74887d573b2ab805bc59e21c398df712c4717672 --- /dev/null +++ b/src/app/shared/datatable/renderer/link.component.spec.ts @@ -0,0 +1,44 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LinkComponent } from './link.component'; + +describe('[Search][Result][Renderer] Component: LinkComponent', () => { + let component: LinkComponent; + let fixture: ComponentFixture<LinkComponent>; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [LinkComponent] + }); + fixture = TestBed.createComponent(LinkComponent); + component = fixture.componentInstance; + }); + + it('should create the component', () => { + expect(component).toBeTruthy(); + }); + + it('#getValue() should return link url', () => { + component.config = { + href: 'url', + display: 'display', + text: 'text', + icon: 'icon', + blank: true + }; + component.value = 'val'; + expect(component.getValue()).toEqual('url'); + }); + + it('#getText() should return link text', () => { + component.config = { + href: 'url', + display: 'display', + text: 'text', + icon: 'icon', + blank: true + }; + component.value = 'val'; + expect(component.getText()).toEqual('text'); + }); +}); diff --git a/src/app/shared/datatable/renderer/link.component.ts b/src/app/shared/datatable/renderer/link.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..5f22acc8ea1457600005e2d983db43b56b07d19b --- /dev/null +++ b/src/app/shared/datatable/renderer/link.component.ts @@ -0,0 +1,30 @@ +import { Component, Input, ChangeDetectionStrategy } from '@angular/core'; + +import { RendererConfig } from '../../../metamodel/model'; + +interface LinkConfig extends RendererConfig { + href: string; + display: string; + text: string; + icon: string; + blank: boolean; +} + +@Component({ + selector: 'app-link', + templateUrl: 'link.component.html', + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class LinkComponent { + @Input() value: string | number; + @Input() datasetName: string; + @Input() config: LinkConfig; + + getValue() { + return this.config.href.replace('$value', this.value.toString()); + } + + getText() { + return this.config.text.replace('$value', this.value.toString()); + } +}