diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fd60918d046a178b951afd91aed075d9066e957..7e4f5fb605bf195de844d3c2ef21af7d6caddec7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [3.6.0] - 2021-xx +### Added +- #135 => Save search result page state when navigate to detail page + ### Changed - #155 => Display dataset label instead of dataset name in result page 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 f9d68bf2cf67b7b18e222524c65a7315a2ae55ae..cfa0e0fe80b886448e403f4de8e71d6d6de591bd 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,11 @@
- +
diff --git a/src/app/search-multiple/containers/result-multiple.component.spec.ts b/src/app/search-multiple/containers/result-multiple.component.spec.ts index 34263ec5b34c2ca75f5b3428ffa7d28108f6884c..872a73df3e750744eab58b774fa4bc501860ed59 100644 --- a/src/app/search-multiple/containers/result-multiple.component.spec.ts +++ b/src/app/search-multiple/containers/result-multiple.component.spec.ts @@ -15,7 +15,7 @@ import { AttributesByDataset, Dataset, Family } from '../../metamodel/model'; import * as fromConeSearch from '../../shared/cone-search/store/cone-search.reducer'; import { ConeSearch } from '../../shared/cone-search/store/model'; import { ScrollTopService } from '../../shared/service/sroll-top.service'; -import { Pagination, PaginationOrder } from '../../shared/datatable/model'; +import { Pagination, PaginationOrder } from '../../shared/datatable/store/model'; import { RouterLinkDirectiveStub } from '../../../settings/test-data/router-link-directive-stub'; describe('[SearchMultiple] Container: ResultMultipleComponent', () => { diff --git a/src/app/search-multiple/containers/result-multiple.component.ts b/src/app/search-multiple/containers/result-multiple.component.ts index 64b5ed34853a6e95a67995bcd8ff36b808ccc136..f72ddd42cc745927e5718cdb00ccfeaa0df2aafd 100644 --- a/src/app/search-multiple/containers/result-multiple.component.ts +++ b/src/app/search-multiple/containers/result-multiple.component.ts @@ -24,7 +24,7 @@ import * as metamodelSelector from '../../metamodel/selectors'; import { AttributesByDataset, Dataset, Family } from '../../metamodel/model'; import * as coneSearchSelector from '../../shared/cone-search/store/cone-search.selector'; import { ConeSearch } from '../../shared/cone-search/store/model'; -import { Pagination } from '../../shared/datatable/model'; +import { Pagination } from '../../shared/datatable/store/model'; import { ScrollTopService } from '../../shared/service/sroll-top.service'; /** @@ -65,6 +65,7 @@ export class ResultMultipleComponent implements OnInit, OnDestroy { public data: Observable<{ dname: string, data: any[]}[]>; public datasetsWithData: Observable; public allData: Observable>; + public datatableState: Observable<{ dname: string, accordionIsOpen: boolean }[]>; public selectedData: Observable<{ dname: string, data: (number | string)[] }[]>; constructor(private store: Store, private scrollTopService: ScrollTopService) { @@ -83,6 +84,7 @@ export class ResultMultipleComponent implements OnInit, OnDestroy { this.allAttributeList = this.store.select(metamodelSelector.getAllAttributeList); this.datasetsWithData = this.store.select(searchMultipleSelector.getDatasetsWithData); this.allData = this.store.select(searchMultipleSelector.getAllData); + this.datatableState = store.select(searchMultipleSelector.getDatatableState); this.selectedData = this.store.select(searchMultipleSelector.getSelectedData); } @@ -120,6 +122,15 @@ export class ResultMultipleComponent implements OnInit, OnDestroy { this.store.dispatch(new searchMultipleActions.RetrieveDataAction(params)); } + /** + * Dispatches action to update datatables state with the given updated datatables state. + * + * @param {{ dname: string, accordionIsOpen: boolean }[]} updatedDatatableState - The updated datatables state. + */ + updateDatatableState(updatedDatatableState: { dname: string, accordionIsOpen: boolean }[]): void { + this.store.dispatch(new searchMultipleActions.UpdateDatatableStateAction(updatedDatatableState)); + } + /** * Dispatches action to update data selection with the given updated selected data. * diff --git a/src/app/search-multiple/containers/search-multiple.component.ts b/src/app/search-multiple/containers/search-multiple.component.ts index 99605bc61a9f75ea72fb70b5ec77d5cc0917d0fa..5ce7ea6a4c89b3311ddb50ab1cb631ba17394e23 100644 --- a/src/app/search-multiple/containers/search-multiple.component.ts +++ b/src/app/search-multiple/containers/search-multiple.component.ts @@ -18,6 +18,7 @@ import { SearchMultipleQueryParams } from '../store/model'; import * as searchActions from '../../search/store/search.action'; import * as coneSearchSelector from '../../shared/cone-search/store/cone-search.selector'; import * as coneSearchActions from '../../shared/cone-search/store/cone-search.action'; +import * as datatableActions from '../../shared/datatable/store/datatable.action'; @Component({ selector: 'app-search-multiple', @@ -46,5 +47,6 @@ export class SearchMultipleComponent { this.queryParams = store.select(searchMultipleSelector.getQueryParams); this.store.dispatch(new searchActions.ResetSearchAction()); this.store.dispatch(new coneSearchActions.DeleteConeSearchAction()); + this.store.dispatch(new datatableActions.ResetDatatableAction()); } } diff --git a/src/app/search-multiple/store/search-multiple.action.spec.ts b/src/app/search-multiple/store/search-multiple.action.spec.ts index 9a89055158e2178634e6081bbf95710c3a27ab4b..c5400dbaae8c9bb9b547f4236dc6f76794b6573c 100644 --- a/src/app/search-multiple/store/search-multiple.action.spec.ts +++ b/src/app/search-multiple/store/search-multiple.action.spec.ts @@ -1,6 +1,6 @@ import * as searchMultipleActions from '../store/search-multiple.action'; import { DatasetCount } from './model'; -import { Pagination, PaginationOrder } from '../../shared/datatable/model'; +import { Pagination, PaginationOrder } from '../../shared/datatable/store/model'; describe('[SearchMultiple] Action', () => { it('should create InitSearchByUrlAction', () => { @@ -77,6 +77,12 @@ describe('[SearchMultiple] Action', () => { expect(action.payload).toEqual('toto'); }); + it('should create UpdateDatatableStateAction', () => { + const action = new searchMultipleActions.UpdateDatatableStateAction([{ dname: 'toto', accordionIsOpen: false }]); + expect(action.type).toEqual(searchMultipleActions.UPDATE_DATATABLE_STATE); + expect(action.payload).toEqual([{ dname: 'toto', accordionIsOpen: false }]); + }); + it('should create UpdateSelectedDataAction', () => { const action = new searchMultipleActions.UpdateSelectedDataAction([{ dname: 'toto', data: [1] }]); expect(action.type).toEqual(searchMultipleActions.UPDATE_SELECTED_DATA); diff --git a/src/app/search-multiple/store/search-multiple.action.ts b/src/app/search-multiple/store/search-multiple.action.ts index c97e8045dc7fada826dab8d5c76642a4fb2d0b3a..b4d1734519737b5452bbf7cc1e34ff08ac572177 100644 --- a/src/app/search-multiple/store/search-multiple.action.ts +++ b/src/app/search-multiple/store/search-multiple.action.ts @@ -10,7 +10,7 @@ import { Action } from '@ngrx/store'; import { DatasetCount } from './model'; -import { Pagination } from '../../shared/datatable/model'; +import { Pagination } from '../../shared/datatable/store/model'; export const INIT_SEARCH_BY_URL = '[SearchMultiple] Init By Url'; @@ -26,6 +26,7 @@ export const RETRIEVE_DATASETS_COUNT_FAIL = '[SearchMultiple] Retrieve Datasets export const RETRIEVE_DATA = '[SearchMultiple] Retrieve Data'; export const RETRIEVE_DATA_SUCCESS = '[SearchMultiple] Retrieve Data Success'; export const RETRIEVE_DATA_FAIL = '[SearchMultiple] Retrieve Data Fail'; +export const UPDATE_DATATABLE_STATE = '[SearchMultiple] Update Datatable State'; export const UPDATE_SELECTED_DATA = '[SearchMultiple] Update Selected Data'; export const DESTROY_RESULTS = '[SearchMultiple] Destroy Results'; export const RESET_SEARCH = '[SearchMultiple] Reset Search'; @@ -173,6 +174,17 @@ export class RetrieveDataFailAction implements Action { constructor(public payload: string) { } } +/** + * @class + * @classdesc UpdateDatatableStateAction action. + * @readonly + */ +export class UpdateDatatableStateAction implements Action { + readonly type = UPDATE_DATATABLE_STATE; + + constructor(public payload: { dname: string, accordionIsOpen: boolean }[]) { } +} + /** * @class * @classdesc UpdateSelectedDataAction action. @@ -220,6 +232,7 @@ export type Actions | RetrieveDataAction | RetrieveDataSuccessAction | RetrieveDataFailAction + | UpdateDatatableStateAction | UpdateSelectedDataAction | DestroyResultsAction | ResetSearchAction; diff --git a/src/app/search-multiple/store/search-multiple.effects.ts b/src/app/search-multiple/store/search-multiple.effects.ts index 6ff23ceffacade44bd860a9d497a68d601bb1dfe..89f9198f2bbaeac9ae071513d0955ba2cb18cf64 100644 --- a/src/app/search-multiple/store/search-multiple.effects.ts +++ b/src/app/search-multiple/store/search-multiple.effects.ts @@ -26,7 +26,7 @@ import { Dataset } from '../../metamodel/model'; import * as fromConeSearch from '../../shared/cone-search/store/cone-search.reducer'; import * as coneSearchActions from '../../shared/cone-search/store/cone-search.action'; import { ConeSearch } from '../../shared/cone-search/store/model'; -import { Pagination, PaginationOrder } from '../../shared/datatable/model'; +import { Pagination, PaginationOrder } from '../../shared/datatable/store/model'; import * as utils from '../../shared/utils'; @Injectable() diff --git a/src/app/search-multiple/store/search-multiple.reducer.spec.ts b/src/app/search-multiple/store/search-multiple.reducer.spec.ts index b06696224f56e611580a22da090fda6a4d2c4c76..d17dfc9b742fb42b96aa3c2969aeb757daef12a3 100644 --- a/src/app/search-multiple/store/search-multiple.reducer.spec.ts +++ b/src/app/search-multiple/store/search-multiple.reducer.spec.ts @@ -1,7 +1,7 @@ import * as fromSearchMultiple from './search-multiple.reducer'; import * as searchMultipleActions from './search-multiple.action'; import { DataByDataset, DatasetCount } from './model'; -import { Pagination, PaginationOrder } from '../../shared/datatable/model'; +import { Pagination, PaginationOrder } from '../../shared/datatable/store/model'; describe('[SearchMultiple] Reducer', () => { it('should return init state', () => { @@ -30,6 +30,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -50,6 +51,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -70,6 +72,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -90,6 +93,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -110,6 +114,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -132,6 +137,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -152,6 +158,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeTruthy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -173,6 +180,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeTruthy(); expect(state.datasetsCount).toEqual(datasetsCount); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -193,6 +201,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -216,6 +225,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -242,6 +252,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -268,6 +279,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -288,6 +300,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(1); expect(state.selectedData).toContain({ dname: 'toto', data:[1, 2] }); expect(state).not.toEqual(initialState); @@ -300,6 +313,7 @@ describe('[SearchMultiple] Reducer', () => { entities: { 'toto': { datasetName: 'toto', isLoading: false, isLoaded: true, data: [1, 2] }}, datasetsCountIsLoaded: true, datasetsCount: [{ dname: 'toto', count: 2}], + datatableState: [{ dname: 'toto', accordionIsOpen: true }], selectedData: [{ dname: 'toto', data:[1, 2] }] }; const action = new searchMultipleActions.DestroyResultsAction(); @@ -316,6 +330,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -334,6 +349,7 @@ describe('[SearchMultiple] Reducer', () => { datasetsCountIsLoading: true, datasetsCountIsLoaded: true, datasetsCount: [{ dname: 'toto', count: 2}], + datatableState: [{ dname: 'toto', accordionIsOpen: true }], selectedData: [{ dname: 'toto', data:[1, 2] }] }; const action = new searchMultipleActions.ResetSearchAction(); @@ -350,6 +366,7 @@ describe('[SearchMultiple] Reducer', () => { expect(state.datasetsCountIsLoading).toBeFalsy(); expect(state.datasetsCountIsLoaded).toBeFalsy(); expect(state.datasetsCount.length).toEqual(0); + expect(state.datatableState.length).toEqual(0); expect(state.selectedData.length).toEqual(0); expect(state).not.toEqual(initialState); }); @@ -438,6 +455,13 @@ describe('[SearchMultiple] Reducer', () => { expect(fromSearchMultiple.getDatasetsCount(state).length).toEqual(0); }); + it('should get datatableState', () => { + const action = {} as searchMultipleActions.Actions; + const state = fromSearchMultiple.reducer(undefined, action); + + expect(fromSearchMultiple.getDatatableState(state).length).toEqual(0); + }); + it('should get selectedData', () => { const action = {} as searchMultipleActions.Actions; const state = fromSearchMultiple.reducer(undefined, action); diff --git a/src/app/search-multiple/store/search-multiple.reducer.ts b/src/app/search-multiple/store/search-multiple.reducer.ts index ebfc9c20358e6e594dfdd145c73cdc4cdf84eb84..318a89ed6fadf3da3f9358ffa71d1b069a8ee02d 100644 --- a/src/app/search-multiple/store/search-multiple.reducer.ts +++ b/src/app/search-multiple/store/search-multiple.reducer.ts @@ -28,6 +28,7 @@ export interface State extends EntityState { datasetsCountIsLoading: boolean; datasetsCountIsLoaded: boolean; datasetsCount: DatasetCount[]; + datatableState: { dname: string, accordionIsOpen: boolean }[]; selectedData: { dname: string, data: (string | number)[] }[]; } @@ -56,6 +57,7 @@ export const initialState: State = adapter.getInitialState({ datasetsCountIsLoading: false, datasetsCountIsLoaded: false, datasetsCount: [], + datatableState: [], selectedData: [] }); @@ -74,6 +76,7 @@ export function reducer(state: State = initialState, action: actions.Actions): S return { ...state, selectedDatasets: action.payload + }; case actions.CHANGE_STEP: @@ -147,6 +150,12 @@ export function reducer(state: State = initialState, action: actions.Actions): S } } as Update, state); + case actions.UPDATE_DATATABLE_STATE: + return { + ...state, + datatableState: action.payload + }; + case actions.UPDATE_SELECTED_DATA: return { ...state, @@ -158,6 +167,7 @@ export function reducer(state: State = initialState, action: actions.Actions): S ...state, datasetsCountIsLoaded: false, datasetsCount: [], + datatableState: [], selectedData: [] }); @@ -183,4 +193,5 @@ export const getSelectedDatasets = (state: State) => state.selectedDatasets; export const getDatasetsCountIsLoading = (state: State) => state.datasetsCountIsLoading; export const getDatasetsCountIsLoaded = (state: State) => state.datasetsCountIsLoaded; export const getDatasetsCount = (state: State) => state.datasetsCount; +export const getDatatableState = (state: State) => state.datatableState; export const getSelectedData = (state: State) => state.selectedData; diff --git a/src/app/search-multiple/store/search-multiple.selector.spec.ts b/src/app/search-multiple/store/search-multiple.selector.spec.ts index e72120ea74f4996e1442275b3d16cf7aca629bec..296fc8f62816c451bfeb2f5aa5cf1d32169215c1 100644 --- a/src/app/search-multiple/store/search-multiple.selector.spec.ts +++ b/src/app/search-multiple/store/search-multiple.selector.spec.ts @@ -87,6 +87,11 @@ describe('[SearchMultiple] Selector', () => { expect(searchMultipleSelector.getDatasetsCount(state).length).toEqual(0); }); + it('should get datatableState', () => { + const state = { searchMultiple: { ...fromSearchMultiple.initialState }}; + expect(searchMultipleSelector.getDatatableState(state).length).toEqual(0); + }); + it('should get datasetsWithData', () => { const state = { searchMultiple: { ...fromSearchMultiple.initialState }}; expect(searchMultipleSelector.getDatasetsWithData(state).length).toEqual(0); @@ -97,7 +102,7 @@ describe('[SearchMultiple] Selector', () => { expect(searchMultipleSelector.getAllData(state)).toEqual({}); }); - it('should get sSelectedData', () => { + it('should get selectedData', () => { const state = { searchMultiple: { ...fromSearchMultiple.initialState }}; expect(searchMultipleSelector.getSelectedData(state).length).toEqual(0); }); diff --git a/src/app/search-multiple/store/search-multiple.selector.ts b/src/app/search-multiple/store/search-multiple.selector.ts index bc8e43f70b01ee8f03c8fde7a1b11be98e4be346..cfaa7ad0fb9e08bcbd8c346163b06d3f0941bb5f 100644 --- a/src/app/search-multiple/store/search-multiple.selector.ts +++ b/src/app/search-multiple/store/search-multiple.selector.ts @@ -91,6 +91,11 @@ export const getDatasetsCount = createSelector( searchMultiple.getDatasetsCount ); +export const getDatatableState = createSelector( + getSearchMultipleState, + searchMultiple.getDatatableState +); + export const getDatasetsWithData = createSelector( getSearchMultipleState, searchMultiple.selectIds diff --git a/src/app/search/components/result/datatable-tab.component.html b/src/app/search/components/result/datatable-tab.component.html index e3cfd49430569371fd0f6ca15d85be65ab007af3..13370db92e6a5a276a70c0019b6d9aa77750abbd 100644 --- a/src/app/search/components/result/datatable-tab.component.html +++ b/src/app/search/components/result/datatable-tab.component.html @@ -1,5 +1,5 @@ - - + + +
@@ -104,24 +104,25 @@
Showing - + + + + of {{ dataLength }} items
- +
\ No newline at end of file diff --git a/src/app/shared/datatable/datatable.component.spec.ts b/src/app/shared/datatable/containers/datatable.component.spec.ts similarity index 50% rename from src/app/shared/datatable/datatable.component.spec.ts rename to src/app/shared/datatable/containers/datatable.component.spec.ts index c29f358e5648c45e53a3fe945d667853c575459b..1b29d9d0809eb554ac63968da5d39a6dc02bef4c 100644 --- a/src/app/shared/datatable/datatable.component.spec.ts +++ b/src/app/shared/datatable/containers/datatable.component.spec.ts @@ -2,13 +2,17 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { Component, Input } from '@angular/core'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { MockStore, provideMockStore } from '@ngrx/store/testing'; +import { of } from 'rxjs'; import { AccordionModule } from 'ngx-bootstrap/accordion'; import { DatatableComponent } from './datatable.component'; -import { ATTRIBUTE_LIST, DATASET } from '../../../settings/test-data'; -import { Pagination, PaginationOrder } from './model'; +import * as fromDatatable from '../store/datatable.reducer'; +import * as datatableActions from '../store/datatable.action'; +import { Pagination, PaginationOrder } from '../store/model'; +import { ATTRIBUTE_LIST, DATASET } from '../../../../settings/test-data'; -describe('[Search][Result] Component: DatatableComponent', () => { +describe('[Shared][Datatable] Container: DatatableComponent', () => { @Component({ selector: 'app-img', template: '' }) class ImgStubComponent { @Input() src: string; @@ -58,6 +62,10 @@ describe('[Search][Result] Component: DatatableComponent', () => { let component: DatatableComponent; let fixture: ComponentFixture; + let store: MockStore; + const initialState = { + datatable: { ...fromDatatable.initialState } + }; beforeEach(() => { TestBed.configureTestingModule({ @@ -72,20 +80,54 @@ describe('[Search][Result] Component: DatatableComponent', () => { JsonStubComponent, PaginationStubComponent ], - imports: [AccordionModule.forRoot(), BrowserAnimationsModule] + imports: [AccordionModule.forRoot(), BrowserAnimationsModule], + providers: [ provideMockStore({ initialState }) ] }); fixture = TestBed.createComponent(DatatableComponent); component = fixture.componentInstance; + store = TestBed.inject(MockStore); }); it('should create the component', () => { expect(component).toBeTruthy(); }); - it('#requiredParams() should return if required params are loaded', () => { + it('#ngOnInit() should init sortedCol value, call #updateDatatable() and set currentPage', () => { + const changeSortedColAction = new datatableActions.ChangeSortedColAction(1); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); + component.attributeList = ATTRIBUTE_LIST; + component.page = of(9); + component.ngOnInit(); + expect(spyDispatch).toHaveBeenCalledTimes(1); + expect(spyDispatch).toHaveBeenCalledWith(changeSortedColAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); + expect(component.currentPage).toEqual(9); + }); + + it('should get page', () => { + expect(component.getPage()).toEqual(1); + }); + + it('should get nbItems', () => { + expect(component.getNbItems()).toEqual(10); + }); + + it('should get sortedCol', () => { + expect(component.getSortedCol()).toBeNull(); + component.sortedCol = of(1); + expect(component.getSortedCol()).toEqual(1); + }); + + it('should get sortedOrder', () => { + expect(component.getSortedOrder()).toEqual(PaginationOrder.a); + }); + + it('#requiredParams() should return if required params are defined', () => { component.attributeList = ATTRIBUTE_LIST; component.outputList = [1]; component.dataLength = 1; + component.sortedCol = of(1); expect(component.requiredParams()).toBeTruthy(); component.dataLength = undefined; expect(component.requiredParams()).toBeFalsy(); @@ -93,7 +135,10 @@ describe('[Search][Result] Component: DatatableComponent', () => { component.outputList = []; expect(component.requiredParams()).toBeFalsy(); component.outputList = [1]; - component.attributeList = [] + component.attributeList = []; + expect(component.requiredParams()).toBeFalsy(); + component.attributeList = ATTRIBUTE_LIST; + component.sortedCol = of(null); expect(component.requiredParams()).toBeFalsy(); }); @@ -143,78 +188,84 @@ describe('[Search][Result] Component: DatatableComponent', () => { expect(component.isSelected(datum)).toBeFalsy(); }); - it('#changePage() should change page value and raise getData event', () => { + it('#changePage() should dispatch action to change page and call #updateDatatable()', () => { component.dataset = DATASET; - component.sortedCol = 1; - component.sortedOrder = PaginationOrder.a; - const expectedPagination: Pagination = { - dname: 'cat_1', - page: 2, - nbItems: 10, - sortedCol: 1, - order: PaginationOrder.a - } - component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); + const changePageAction = new datatableActions.ChangePageAction(2); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); component.changePage(2); + expect(spyDispatch).toHaveBeenCalledTimes(1); + expect(spyDispatch).toHaveBeenCalledWith(changePageAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); }); - it('#changeNbItems() should change nbItems value and raise getData event', () => { + it('#changeNbItems() should dispatch action to change nbItems and call #updateDatatable()', () => { component.dataset = DATASET; - component.sortedCol = 1; - component.sortedOrder = PaginationOrder.a; - const expectedPagination: Pagination = { - dname: 'cat_1', - page: 1, - nbItems: 20, - sortedCol: 1, - order: PaginationOrder.a - } - component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); + const changeNbItemsAction = new datatableActions.ChangeNbItemsAction(20); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); component.changeNbItems(20); + expect(spyDispatch).toHaveBeenCalledTimes(1); + expect(spyDispatch).toHaveBeenCalledWith(changeNbItemsAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); }); - it('#sort() should raise getData event with correct parameters', () => { - component.dataset = DATASET; - component.sortedOrder = PaginationOrder.a; - let expectedPagination: Pagination = { - dname: 'cat_1', - page: 1, - nbItems: 10, - sortedCol: 1, - order: PaginationOrder.a - } - let subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); + it('#sort() should dispatch action to change sortedOrder to `d` and call #updateDatatable()', () => { + component.sortedCol = of(1); + const changeSortedOrderAction = new datatableActions.ChangeSortedOrderAction(PaginationOrder.d); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); component.sort(1); - subscribtion.unsubscribe(); - component.sortedCol = 1; - component.sortedOrder = PaginationOrder.a; - expectedPagination = { - dname: 'cat_1', - page: 1, - nbItems: 10, - sortedCol: 1, - order: PaginationOrder.d - } - subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); + expect(spyDispatch).toHaveBeenCalledTimes(1); + expect(spyDispatch).toHaveBeenCalledWith(changeSortedOrderAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); + }); + + it('#sort() should dispatch action to change sortedOrder to `a` and call #updateDatatable()', () => { + component.sortedCol = of(1); + component.sortedOrder = of(PaginationOrder.d); + const changeSortedOrderAction = new datatableActions.ChangeSortedOrderAction(PaginationOrder.a); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); component.sort(1); - subscribtion.unsubscribe(); - component.sortedCol = 1; - component.sortedOrder = PaginationOrder.d; - expectedPagination = { + expect(spyDispatch).toHaveBeenCalledTimes(1); + expect(spyDispatch).toHaveBeenCalledWith(changeSortedOrderAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); + }); + + it('#sort() should dispatch action to change sortedOrder and sortedCol and call #updateDatatable()', () => { + component.sortedCol = of(1); + const changeSortedColAction = new datatableActions.ChangeSortedColAction(2); + const changeSortedOrderAction = new datatableActions.ChangeSortedOrderAction(PaginationOrder.a); + const spyDispatch = spyOn(store, 'dispatch'); + const spyUpdate = spyOn(component, 'updateDatatable'); + component.sort(2); + expect(spyDispatch).toHaveBeenCalledTimes(2); + expect(spyDispatch).toHaveBeenCalledWith(changeSortedColAction); + expect(spyDispatch).toHaveBeenCalledWith(changeSortedOrderAction); + expect(spyUpdate).toHaveBeenCalledTimes(1); + }); + + it('#updateDatatable() should raise getData event', () => { + component.dataset = DATASET; + component.sortedCol = of(1); + const expectedPagination: Pagination = { dname: 'cat_1', page: 1, nbItems: 10, sortedCol: 1, order: PaginationOrder.a } - subscribtion = component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); - component.sort(1); + component.getData.subscribe((event: Pagination) => expect(event).toEqual(expectedPagination)); + component.updateDatatable(); }); - it('#ngOnInit() should init sortedCol value', () => { - component.attributeList = ATTRIBUTE_LIST; - component.ngOnInit(); - expect(component.sortedCol).toEqual(1); + it('#addVisited() should dispatch action to add object ID to the visited object list', () => { + const addVisitedAction = new datatableActions.AddVisitedAction(1); + const spy = spyOn(store, 'dispatch'); + component.addVisited(1); + expect(spy).toHaveBeenCalledTimes(1); + expect(spy).toHaveBeenCalledWith(addVisitedAction); }); }); diff --git a/src/app/shared/datatable/containers/datatable.component.ts b/src/app/shared/datatable/containers/datatable.component.ts new file mode 100644 index 0000000000000000000000000000000000000000..54fc7903ed602c9e51aade5717534001b636f31e --- /dev/null +++ b/src/app/shared/datatable/containers/datatable.component.ts @@ -0,0 +1,250 @@ +/** + * 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, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; + +import * as fromDatatable from '../store/datatable.reducer'; +import * as datatableActions from '../store/datatable.action'; +import * as datatableSelector from '../store/datatable.selector'; +import { Pagination, PaginationOrder } from '../store/model'; +import { Attribute, Dataset } from '../../../metamodel/model'; + +/** + * Interface for store state. + * + * @interface StoreState + */ +interface StoreState { + datatable: fromDatatable.State; +} + +@Component({ + selector: 'app-datatable', + templateUrl: 'datatable.component.html', + styleUrls: ['datatable.component.css'] +}) +/** + * @class + * @classdesc Datatable component. + * + * @implements OnInit + */ +export class DatatableComponent implements OnInit { + @Input() dataset: Dataset; + @Input() attributeList: 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 = new EventEmitter(); + @Output() addSelectedData: EventEmitter = new EventEmitter(); + @Output() deleteSelectedData: EventEmitter = new EventEmitter(); + @Output() executeProcess: EventEmitter = new EventEmitter(); + + page: Observable; + nbItems: Observable; + sortedCol: Observable; + sortedOrder: Observable; + visited: Observable<(number|string)[]>; + + currentPage: number = 1; + + constructor(private store: Store) { + this.page = store.select(datatableSelector.getPage); + this.nbItems = store.select(datatableSelector.getNbItems); + this.sortedCol = store.select(datatableSelector.getSortedCol); + this.sortedOrder = store.select(datatableSelector.getSortedOrder); + this.visited = store.select(datatableSelector.getVisited); + } + + ngOnInit() { + if (this.getSortedCol() === null) { + const defaultSortedCol = this.attributeList.find(a => a.search_flag === 'ID').id; + this.store.dispatch(new datatableActions.ChangeSortedColAction(defaultSortedCol)); + } + this.updateDatatable(); + this.currentPage = this.getPage(); + } + + /** + * Returns page. + * + * @return number + */ + getPage(): number { + let page: number; + this.page.subscribe(p => page = p); + return page; + } + + /** + * Returns nbItems. + * + * @return number + */ + getNbItems(): number { + let nbItems: number; + this.nbItems.subscribe(nb => nbItems = nb); + return nbItems; + } + + /** + * Returns sortedCol. + * + * @return number + */ + getSortedCol(): number { + let sortedCol: number; + this.sortedCol.subscribe(col => sortedCol = col); + return sortedCol; + } + + /** + * Returns sortedOrder. + * + * @return PaginationOrder + */ + getSortedOrder(): PaginationOrder { + let sortedOrder: PaginationOrder; + this.sortedOrder.subscribe(order => sortedOrder = order); + return sortedOrder; + } + + /** + * Checks if required parameters to display datatable are defined. + * + * @return boolean + */ + requiredParams(): boolean { + if (this.attributeList.length === 0 || this.outputList.length === 0 || !this.dataLength || !this.getSortedCol()) { + return false; + } + return true; + } + + /** + * Checks if there is no data selected. + * + * @return boolean + */ + noSelectedData(): boolean { + return this.selectedData.length < 1; + } + + /** + * Returns output list from attribute list. + * + * @return Attribute[] + */ + getOutputList(): Attribute[] { + return this.attributeList + .filter(a => this.outputList.includes(a.id)) + .sort((a, b) => a.output_display - b.output_display); + } + + /** + * Emits events to select or unselect data. + * + * @param {any} datum - The data to select or unselect. + * + * @fires EventEmitter + */ + toggleSelection(datum: any): void { + const attribute = this.attributeList.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]); + } + } + + /** + * Checks if data is selected. + * + * @param {any} datum - The data. + * + * @return boolean + */ + isSelected(datum: any): boolean { + const attribute = this.attributeList.find(a => a.search_flag === 'ID'); + + if (this.selectedData.indexOf(datum[attribute.label]) > -1) { + return true; + } + return false; + } + + /** + * Changes the datatable page. + * + * @param {number} page - The datatable page number. + */ + changePage(page: number): void { + this.store.dispatch(new datatableActions.ChangePageAction(page)); + this.updateDatatable(); + } + + /** + * Changes the datatable page. + * + * @param {number} nbItems - The datatable items number. + */ + changeNbItems(nbItems: number): void { + this.store.dispatch(new datatableActions.ChangeNbItemsAction(nbItems)); + this.updateDatatable(); + } + + /** + * Changes the sorted order and the sorted column of the datatable. + * + * @param {number} id - The id of the column to sort. + */ + sort(id: number): void { + if (id === this.getSortedCol()) { + const order = this.getSortedOrder() === PaginationOrder.a ? PaginationOrder.d : PaginationOrder.a; + this.store.dispatch(new datatableActions.ChangeSortedOrderAction(order)); + } else { + this.store.dispatch(new datatableActions.ChangeSortedColAction(id)); + this.store.dispatch(new datatableActions.ChangeSortedOrderAction(PaginationOrder.a)); + } + this.updateDatatable(); + } + + /** + * Emits event to update datatable. + * + * @fires EventEmitter + */ + updateDatatable(): void { + const pagination: Pagination = { + dname: this.dataset.name, + page: this.getPage(), + nbItems: this.getNbItems(), + sortedCol: this.getSortedCol(), + order: this.getSortedOrder() + }; + this.getData.emit(pagination); + } + + /** + * Adds object to the visited list. + * + * @param {number | string} id - The visited object ID . + */ + addVisited(id: number | string): void { + this.store.dispatch(new datatableActions.AddVisitedAction(id)); + } +} diff --git a/src/app/shared/datatable/datatable.component.ts b/src/app/shared/datatable/datatable.component.ts deleted file mode 100644 index 6b576196f5265699123b710980d604f78f12fcdf..0000000000000000000000000000000000000000 --- a/src/app/shared/datatable/datatable.component.ts +++ /dev/null @@ -1,176 +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, EventEmitter, Input, OnInit, Output } from '@angular/core'; - -import { Attribute, Dataset } from '../../metamodel/model'; -import { Pagination, PaginationOrder } from './model'; - - -@Component({ - selector: 'app-datatable', - templateUrl: 'datatable.component.html', - styleUrls: ['datatable.component.css'], -}) -/** - * @class - * @classdesc Datatable component. - * - * @implements OnInit - */ -export class DatatableComponent implements OnInit { - @Input() dataset: Dataset; - @Input() attributeList: 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 = new EventEmitter(); - @Output() addSelectedData: EventEmitter = new EventEmitter(); - @Output() deleteSelectedData: EventEmitter = new EventEmitter(); - @Output() executeProcess: EventEmitter = new EventEmitter(); - nbItems = 10; - page = 1; - sortedCol: number = null; - sortedOrder: PaginationOrder = PaginationOrder.a; - - ngOnInit() { - this.sortedCol = this.attributeList.find(a => a.search_flag === 'ID').id; - } - - /** - * Checks if required parameters to display datatable are passed to the component. - * - * @return boolean - */ - requiredParams(): boolean { - if (this.attributeList.length === 0 || this.outputList.length === 0 || !this.dataLength) { - return false; - } - return true; - } - - /** - * Checks if there is no data selected. - * - * @return boolean - */ - noSelectedData(): boolean { - return this.selectedData.length < 1; - } - - /** - * Returns output list from attribute list. - * - * @return Attribute[] - */ - getOutputList(): Attribute[] { - return this.attributeList - .filter(a => this.outputList.includes(a.id)) - .sort((a, b) => a.output_display - b.output_display); - } - - /** - * Emits events to select or unselect data. - * - * @param {any} datum - The data to select or unselect. - * - * @fires EventEmitter - */ - toggleSelection(datum: any): void { - const attribute = this.attributeList.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]); - } - } - - /** - * Checks if data is selected. - * - * @param {any} datum - The data. - * - * @return boolean - */ - isSelected(datum: any): boolean { - const attribute = this.attributeList.find(a => a.search_flag === 'ID'); - - if (this.selectedData.indexOf(datum[attribute.label]) > -1) { - return true; - } - return false; - } - - /** - * Emits event to change datatable page. - * - * @param {number} nb - The page number to access. - * - * @fires EventEmitter - */ - changePage(nb: number): void { - this.page = nb; - const pagination: Pagination = { - dname: this.dataset.name, - page: this.page, - nbItems: this.nbItems, - sortedCol: this.sortedCol, - order: PaginationOrder.a - }; - this.getData.emit(pagination); - } - - /** - * Emits event to change datatable displayed items. - * - * @param {number} nb - The number of items to display. - * - * @fires EventEmitter - */ - changeNbItems(nb: number): void { - this.nbItems = nb; - const pagination: Pagination = { - dname: this.dataset.name, - page: this.page, - nbItems: this.nbItems, - sortedCol: this.sortedCol, - order: PaginationOrder.a - }; - this.getData.emit(pagination); - } - - /** - * Emits event to change the sorted order and the sorted column of the datatable. - * - * @param {number} id - The id of the column to sort. - * - * @fires EventEmitter - */ - sort(id: number): void { - if (id === this.sortedCol) { - this.sortedOrder = this.sortedOrder === PaginationOrder.a ? PaginationOrder.d : PaginationOrder.a; - } else { - this.sortedCol = id; - this.sortedOrder = PaginationOrder.a; - } - const pagination: Pagination = { - dname: this.dataset.name, - page: this.page, - nbItems: this.nbItems, - sortedCol: this.sortedCol, - order: this.sortedOrder - }; - this.getData.emit(pagination); - } -} diff --git a/src/app/shared/datatable/index.ts b/src/app/shared/datatable/index.ts index 782dd80ce94c872d69572803239cecfc6861ab84..b8f52474dfc1cbf8b42a5c0061737c86c741eadf 100644 --- a/src/app/shared/datatable/index.ts +++ b/src/app/shared/datatable/index.ts @@ -1,9 +1,9 @@ -import { DatatableComponent } from './datatable.component'; -import { DetailComponent } from './renderer/detail.component'; -import { ImageComponent } from './renderer/image.component'; -import { JsonComponent } from './renderer/json.component'; -import { LinkComponent } from './renderer/link.component'; -import { DownloadComponent } from './renderer/download.component'; +import { DatatableComponent } from './containers/datatable.component'; +import { DetailComponent } from './components/detail.component'; +import { ImageComponent } from './components/image.component'; +import { JsonComponent } from './components/json.component'; +import { LinkComponent } from './components/link.component'; +import { DownloadComponent } from './components/download.component'; export const datatableComponents = [ DatatableComponent, diff --git a/src/app/shared/datatable/renderer/detail.component.html b/src/app/shared/datatable/renderer/detail.component.html deleted file mode 100644 index 0b6336b1e1acb9cf348dc857b43bd2fedc62ddc2..0000000000000000000000000000000000000000 --- a/src/app/shared/datatable/renderer/detail.component.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{ value }} - diff --git a/src/app/shared/datatable/renderer/download.component.html b/src/app/shared/datatable/renderer/download.component.html deleted file mode 100644 index 430f6d477c5e259505a4abf00839fd14a9df8856..0000000000000000000000000000000000000000 --- a/src/app/shared/datatable/renderer/download.component.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{ config.text }} - - \ No newline at end of file diff --git a/src/app/shared/datatable/renderer/link.component.html b/src/app/shared/datatable/renderer/link.component.html deleted file mode 100644 index 2ec236b52990cc59eb07abbfdb083c94ff56f00d..0000000000000000000000000000000000000000 --- a/src/app/shared/datatable/renderer/link.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - {{ getText() }} - - \ No newline at end of file diff --git a/src/app/shared/datatable/store/datatable.action.spec.ts b/src/app/shared/datatable/store/datatable.action.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..476f945ee1a5c85a93f3d5476643d7f0e1c18adb --- /dev/null +++ b/src/app/shared/datatable/store/datatable.action.spec.ts @@ -0,0 +1,40 @@ +import * as datatableActions from '../store/datatable.action'; +import { PaginationOrder } from './model'; + +describe('[Shared][Datatable] Action', () => { + it('should create ChangePageAction', () => { + const action = new datatableActions.ChangePageAction(2); + expect(action.type).toEqual(datatableActions.CHANGE_PAGE); + expect(action.payload).toEqual(2); + }); + + it('should create ChangeNbItemsAction', () => { + const action = new datatableActions.ChangeNbItemsAction(2); + expect(action.type).toEqual(datatableActions.CHANGE_NB_ITEMS); + expect(action.payload).toEqual(2); + }); + + it('should create ChangeSortedColAction', () => { + const action = new datatableActions.ChangeSortedColAction(2); + expect(action.type).toEqual(datatableActions.CHANGE_SORTED_COL); + expect(action.payload).toEqual(2); + }); + + it('should create ChangeSortedOrderAction', () => { + const order: PaginationOrder = PaginationOrder.a; + const action = new datatableActions.ChangeSortedOrderAction(order); + expect(action.type).toEqual(datatableActions.CHANGE_SORTED_ORDER); + expect(action.payload).toEqual('a'); + }); + + it('should create AddVisitedAction', () => { + const action = new datatableActions.AddVisitedAction(2); + expect(action.type).toEqual(datatableActions.ADD_VISITED); + expect(action.payload).toEqual(2); + }); + + it('should create ResetDatatableAction', () => { + const action = new datatableActions.ResetDatatableAction(); + expect(action.type).toEqual(datatableActions.RESET_DATATABLE); + }); +}); diff --git a/src/app/shared/datatable/store/datatable.action.ts b/src/app/shared/datatable/store/datatable.action.ts new file mode 100644 index 0000000000000000000000000000000000000000..aee84cd28a825df895d8f49c15d114c24d983164 --- /dev/null +++ b/src/app/shared/datatable/store/datatable.action.ts @@ -0,0 +1,94 @@ +/** + * 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 { Action } from '@ngrx/store'; + +import { PaginationOrder } from './model'; + +export const CHANGE_PAGE = '[Datatable] Change Page'; +export const CHANGE_NB_ITEMS = '[Datatable] Change Nb Items'; +export const CHANGE_SORTED_COL = '[Datatable] Change Sorted Col'; +export const CHANGE_SORTED_ORDER = '[Datatable] Change Sorted Order'; +export const ADD_VISITED = '[Datatable] Add Visited'; +export const RESET_DATATABLE = '[Datatable] Reset Datatable'; + + +/** + * @class + * @classdesc ChangePageAction action. + * @readonly + */ +export class ChangePageAction implements Action { + readonly type = CHANGE_PAGE; + + constructor(public payload: number) { } +} + +/** + * @class + * @classdesc ChangeNbItemsAction action. + * @readonly + */ +export class ChangeNbItemsAction implements Action { + readonly type = CHANGE_NB_ITEMS; + + constructor(public payload: number) { } +} + +/** + * @class + * @classdesc ChangeSortedOrderAction action. + * @readonly + */ +export class ChangeSortedOrderAction implements Action { + readonly type = CHANGE_SORTED_ORDER; + + constructor(public payload: PaginationOrder) { } +} + +/** + * @class + * @classdesc ChangeSortedColAction action. + * @readonly + */ +export class ChangeSortedColAction implements Action { + readonly type = CHANGE_SORTED_COL; + + constructor(public payload: number) { } +} + +/** + * @class + * @classdesc AddVisitedAction action. + * @readonly + */ +export class AddVisitedAction implements Action { + readonly type = ADD_VISITED; + + constructor(public payload: number | string) { } +} + +/** + * @class + * @classdesc ResetDatatableAction action. + * @readonly + */ +export class ResetDatatableAction implements Action { + readonly type = RESET_DATATABLE; + + constructor(public payload: {} = null) { } +} + +export type Actions + = ChangePageAction + | ChangeNbItemsAction + | ChangeSortedColAction + | ChangeSortedOrderAction + | AddVisitedAction + | ResetDatatableAction; diff --git a/src/app/shared/datatable/store/datatable.reducer.spec.ts b/src/app/shared/datatable/store/datatable.reducer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f1c3c0f2f094bd035be0eebc92f09194119b856b --- /dev/null +++ b/src/app/shared/datatable/store/datatable.reducer.spec.ts @@ -0,0 +1,135 @@ +import * as fromDatatable from './datatable.reducer'; +import * as datatableActions from './datatable.action'; +import { PaginationOrder } from './model'; + +describe('[Shared][Datatable] Reducer', () => { + it('should return init state', () => { + const { initialState } = fromDatatable; + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(state).toBe(initialState); + }); + + it('should update page', () => { + const { initialState } = fromDatatable; + const action = new datatableActions.ChangePageAction(2); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(2); + expect(state.nbItems).toEqual(10); + expect(state.sortedCol).toBeNull(); + expect(state.sortedOrder).toEqual('a'); + expect(state.visited.length).toEqual(0); + expect(state).not.toEqual(initialState); + }); + + it('should update nbItems', () => { + const { initialState } = fromDatatable; + const action = new datatableActions.ChangeNbItemsAction(20); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(1); + expect(state.nbItems).toEqual(20); + expect(state.sortedCol).toBeNull(); + expect(state.sortedOrder).toEqual('a'); + expect(state.visited.length).toEqual(0); + expect(state).not.toEqual(initialState); + }); + + it('should update sortedCol', () => { + const { initialState } = fromDatatable; + const action = new datatableActions.ChangeSortedColAction(1); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(1); + expect(state.nbItems).toEqual(10); + expect(state.sortedCol).toEqual(1); + expect(state.sortedOrder).toEqual('a'); + expect(state.visited.length).toEqual(0); + expect(state).not.toEqual(initialState); + }); + + it('should update sortedOrder', () => { + const order: PaginationOrder = PaginationOrder.d; + const { initialState } = fromDatatable; + const action = new datatableActions.ChangeSortedOrderAction(order); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(1); + expect(state.nbItems).toEqual(10); + expect(state.sortedCol).toBeNull(); + expect(state.sortedOrder).toEqual('d'); + expect(state.visited.length).toEqual(0); + expect(state).not.toEqual(initialState); + }); + + it('should update visited', () => { + const { initialState } = fromDatatable; + const action = new datatableActions.AddVisitedAction(1); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(1); + expect(state.nbItems).toEqual(10); + expect(state.sortedCol).toBeNull(); + expect(state.sortedOrder).toEqual('a'); + expect(state.visited.length).toEqual(1); + expect(state.visited).toEqual([1]); + expect(state).not.toEqual(initialState); + }); + + it('should reset to initial state', () => { + const initialState = { + ...fromDatatable.initialState, + page: 2, + nbItems: 20, + sortedCol: 4, + sortedOrder: PaginationOrder.d, + visited: [1, 2, 3] + }; + const action = new datatableActions.ResetDatatableAction(); + const state = fromDatatable.reducer(initialState, action); + + expect(state.page).toEqual(1); + expect(state.nbItems).toEqual(10); + expect(state.sortedCol).toBeNull(); + expect(state.sortedOrder).toEqual('a'); + expect(state.visited.length).toEqual(0); + expect(state).not.toEqual(initialState); + }); + + it('should get page', () => { + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(fromDatatable.getPage(state)).toEqual(1); + }); + + it('should get nbItems', () => { + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(fromDatatable.getNbItems(state)).toEqual(10); + }); + + it('should get sortedCol', () => { + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(fromDatatable.getSortedCol(state)).toBeNull(); + }); + + it('should get sortedOrder', () => { + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(fromDatatable.getSortedOrder(state)).toEqual('a'); + }); + + it('should get visited', () => { + const action = {} as datatableActions.Actions; + const state = fromDatatable.reducer(undefined, action); + + expect(fromDatatable.getVisited(state).length).toEqual(0); + }); +}); diff --git a/src/app/shared/datatable/store/datatable.reducer.ts b/src/app/shared/datatable/store/datatable.reducer.ts new file mode 100644 index 0000000000000000000000000000000000000000..55345c2767cb6ac8849b0dbfa0cd1ab851987a5e --- /dev/null +++ b/src/app/shared/datatable/store/datatable.reducer.ts @@ -0,0 +1,87 @@ +/** + * 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 * as actions from './datatable.action'; +import { PaginationOrder } from './model'; +import * as fromConeSearch from '../../cone-search/store/cone-search.reducer'; + +/** + * Interface for datatable state. + * + * @interface State + */ +export interface State { + page: number; + nbItems: number; + sortedCol: number; + sortedOrder: PaginationOrder; + visited: (number | string)[]; +} + +export const initialState: State = { + page: 1, + nbItems: 10, + sortedCol: null, + sortedOrder: PaginationOrder.a, + visited: [] +}; + +/** + * Reduces state. + * + * @param {State} state - The state. + * @param {actions} action - The action. + * + * @return State + */ +export function reducer(state: State = initialState, action: actions.Actions): State { + switch (action.type) { + case actions.CHANGE_PAGE: + return { + ...state, + page: action.payload + }; + + case actions.CHANGE_NB_ITEMS: + return { + ...state, + nbItems: action.payload + }; + + case actions.CHANGE_SORTED_COL: + return { + ...state, + sortedCol: action.payload + }; + + case actions.CHANGE_SORTED_ORDER: + return { + ...state, + sortedOrder: action.payload + }; + + case actions.ADD_VISITED: + return { + ...state, + visited: [...state.visited, action.payload] + }; + + case actions.RESET_DATATABLE: + return initialState; + + default: + return state; + } +} + +export const getPage = (state: State) => state.page; +export const getNbItems = (state: State) => state.nbItems; +export const getSortedCol = (state: State) => state.sortedCol; +export const getSortedOrder = (state: State) => state.sortedOrder; +export const getVisited = (state: State) => state.visited; diff --git a/src/app/shared/datatable/store/datatable.selector.spec.ts b/src/app/shared/datatable/store/datatable.selector.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3dbaf411e2e99afe9736f4aadbb1c6fb1eef7ad9 --- /dev/null +++ b/src/app/shared/datatable/store/datatable.selector.spec.ts @@ -0,0 +1,29 @@ +import * as datatableSelector from './datatable.selector'; +import * as fromDatatable from './datatable.reducer'; + +describe('[Shared][Datatable] Selector', () => { + it('should get page', () => { + const state = { datatable: { ...fromDatatable.initialState }}; + expect(datatableSelector.getPage(state)).toEqual(1); + }); + + it('should get nbItems', () => { + const state = { datatable: { ...fromDatatable.initialState }}; + expect(datatableSelector.getNbItems(state)).toEqual(10); + }); + + it('should get sortedCol', () => { + const state = { datatable: { ...fromDatatable.initialState }}; + expect(datatableSelector.getSortedCol(state)).toBeNull(); + }); + + it('should get sortedOrder', () => { + const state = { datatable: { ...fromDatatable.initialState }}; + expect(datatableSelector.getSortedOrder(state)).toEqual('a'); + }); + + it('should get visited', () => { + const state = { datatable: { ...fromDatatable.initialState }}; + expect(datatableSelector.getVisited(state).length).toEqual(0); + }); +}); \ No newline at end of file diff --git a/src/app/shared/datatable/store/datatable.selector.ts b/src/app/shared/datatable/store/datatable.selector.ts new file mode 100644 index 0000000000000000000000000000000000000000..2082f6bb6bd7df86ca6d05547219209316abdf42 --- /dev/null +++ b/src/app/shared/datatable/store/datatable.selector.ts @@ -0,0 +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 { createSelector, createFeatureSelector } from '@ngrx/store'; + +import * as datatable from './datatable.reducer'; + +export const getDatatableState = createFeatureSelector('datatable'); + +export const getPage = createSelector( + getDatatableState, + datatable.getPage +); + +export const getNbItems = createSelector( + getDatatableState, + datatable.getNbItems +); + +export const getSortedCol = createSelector( + getDatatableState, + datatable.getSortedCol +); + +export const getSortedOrder = createSelector( + getDatatableState, + datatable.getSortedOrder +); + +export const getVisited = createSelector( + getDatatableState, + datatable.getVisited +); diff --git a/src/app/shared/datatable/model/index.ts b/src/app/shared/datatable/store/model/index.ts similarity index 100% rename from src/app/shared/datatable/model/index.ts rename to src/app/shared/datatable/store/model/index.ts diff --git a/src/app/shared/datatable/model/pagination.model.ts b/src/app/shared/datatable/store/model/pagination.model.ts similarity index 100% rename from src/app/shared/datatable/model/pagination.model.ts rename to src/app/shared/datatable/store/model/pagination.model.ts diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index d9171dfc4ca530291f69f8f7698a1e72e975ff21..7b60d8461a769edd4d6688d35f9a6b6532752ba9 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -11,10 +11,9 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { RouterModule } from '@angular/router'; + import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; - -import { ToastrModule } from 'ngx-toastr'; import { ModalModule, BsModalService } from 'ngx-bootstrap/modal'; import { TabsModule } from 'ngx-bootstrap/tabs'; import { AccordionModule } from 'ngx-bootstrap/accordion'; @@ -23,16 +22,19 @@ import { TooltipModule } from 'ngx-bootstrap/tooltip'; import { PopoverModule } from 'ngx-bootstrap/popover'; import { PaginationModule } from 'ngx-bootstrap/pagination'; import { BsDatepickerModule } from 'ngx-bootstrap/datepicker'; +import { ToastrModule } from 'ngx-toastr'; import { NgxJsonViewerModule } from 'ngx-json-viewer'; -import { ScrollTopService } from './service/sroll-top.service'; -import { PrettyOperatorPipe } from './pretty-operator.pipe'; import { NgSelectModule } from '@ng-select/ng-select'; import { InlineSVGModule } from 'ng-inline-svg'; -import { coneSearchComponents } from './cone-search'; -import { reducer } from './cone-search/store/cone-search.reducer'; + +import { reducer as coneSearchReducer } from './cone-search/store/cone-search.reducer'; import { ConeSearchEffects } from './cone-search/store/cone-search.effects'; import { ConeSearchService } from './cone-search/store/cone-search.service'; +import { coneSearchComponents } from './cone-search'; +import { reducer as datatableReducer } from './datatable/store/datatable.reducer'; import { datatableComponents } from './datatable'; +import { ScrollTopService } from './service/sroll-top.service'; +import { PrettyOperatorPipe } from './pretty-operator.pipe'; @NgModule({ imports: [ @@ -52,7 +54,8 @@ import { datatableComponents } from './datatable'; NgxJsonViewerModule, InlineSVGModule.forRoot(), RouterModule, - StoreModule.forFeature('coneSearch', reducer), + StoreModule.forFeature('coneSearch', coneSearchReducer), + StoreModule.forFeature('datatable', datatableReducer), EffectsModule.forFeature([ConeSearchEffects]) ], exports: [