diff --git a/.travis.yml b/.travis.yml index c35e903d0e0..d46202f5f30 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,6 @@ cache: bundler_args: --retry 5 -before_install: - - travis_retry yarn run global - install: - travis_retry yarn install diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 4260568a7e2..8b4b38fa255 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -597,6 +597,18 @@ "objectpeople": { "placeholder": "People", "head": "People" + }, + "jobTitle": { + "placeholder": "Job Title", + "head": "Job Title" + }, + "knowsLanguage": { + "placeholder": "Known language", + "head": "Known language" + }, + "birthDate": { + "placeholder": "Birth Date", + "head": "Birth Date" } } } diff --git a/src/app/+search-page/filtered-search-page.component.spec.ts b/src/app/+search-page/filtered-search-page.component.spec.ts index 5c49767ed21..59ab9d7b0d1 100644 --- a/src/app/+search-page/filtered-search-page.component.spec.ts +++ b/src/app/+search-page/filtered-search-page.component.spec.ts @@ -18,20 +18,4 @@ describe('FilteredSearchPageComponent', () => { searchConfigService = (comp as any).searchConfigService; fixture.detectChanges(); }); - - describe('when fixedFilterQuery is defined', () => { - const fixedFilterQuery = 'fixedFilterQuery'; - - beforeEach(() => { - spyOn(searchConfigService, 'updateFixedFilter').and.callThrough(); - comp.fixedFilterQuery = fixedFilterQuery; - comp.ngOnInit(); - fixture.detectChanges(); - }); - - it('should update the paginated search options', () => { - expect(searchConfigService.updateFixedFilter).toHaveBeenCalledWith(fixedFilterQuery); - }); - }); - }); diff --git a/src/app/+search-page/filtered-search-page.component.ts b/src/app/+search-page/filtered-search-page.component.ts index d577c2c44c5..66c619b8237 100644 --- a/src/app/+search-page/filtered-search-page.component.ts +++ b/src/app/+search-page/filtered-search-page.component.ts @@ -2,20 +2,22 @@ import { HostWindowService } from '../shared/host-window.service'; import { SearchService } from './search-service/search.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SearchPageComponent } from './search-page.component'; -import { ChangeDetectionStrategy, Component, Inject, Input } from '@angular/core'; +import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { pushInOut } from '../shared/animations/push'; import { RouteService } from '../shared/services/route.service'; import { SearchConfigurationService } from './search-service/search-configuration.service'; import { Observable } from 'rxjs'; import { PaginatedSearchOptions } from './paginated-search-options.model'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; +import { map } from 'rxjs/operators'; /** * This component renders a simple item page. * The route parameter 'id' is used to request the item it represents. * All fields of the item that should be displayed, are defined in its template. */ -@Component({selector: 'ds-filtered-search-page', +@Component({ + selector: 'ds-filtered-search-page', styleUrls: ['./search-page.component.scss'], templateUrl: './search-page.component.html', changeDetection: ChangeDetectionStrategy.OnPush, @@ -28,8 +30,7 @@ import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.compone ] }) -export class FilteredSearchPageComponent extends SearchPageComponent { - +export class FilteredSearchPageComponent extends SearchPageComponent implements OnInit { /** * The actual query for the fixed filter. * If empty, the query will be determined by the route parameter called 'filter' @@ -44,6 +45,17 @@ export class FilteredSearchPageComponent extends SearchPageComponent { super(service, sidebarService, windowService, searchConfigService, routeService); } + /** + * Listening to changes in the paginated search options + * If something changes, update the search results + * + * Listen to changes in the scope + * If something changes, update the list of scopes for the dropdown + */ + ngOnInit(): void { + super.ngOnInit(); + } + /** * Get the current paginated search options after updating the fixed filter using the fixedFilterQuery input * This is to make sure the fixed filter is included in the paginated search options, as it is not part of any @@ -51,8 +63,11 @@ export class FilteredSearchPageComponent extends SearchPageComponent { * @returns {Observable} */ protected getSearchOptions(): Observable { - this.searchConfigService.updateFixedFilter(this.fixedFilterQuery); - return this.searchConfigService.paginatedSearchOptions; + return this.searchConfigService.paginatedSearchOptions.pipe( + map((options: PaginatedSearchOptions) => { + const filter = this.fixedFilterQuery || options.fixedFilter; + return Object.assign(options, { fixedFilter: filter }); + }) + ); } - } diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts index 32073455642..3f6c2ef1334 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.spec.ts @@ -17,7 +17,7 @@ describe('SearchFixedFilterService', () => { configure: () => {}, /* tslint:enable:no-empty */ generateRequestId: () => 'fake-id', - getByUUID: () => observableOf(Object.assign(new RequestEntry(), { + getByHref: () => observableOf(Object.assign(new RequestEntry(), { response: new FilteredDiscoveryQueryResponse(filterQuery, 200, 'OK') })) }) as RequestService; @@ -56,5 +56,4 @@ describe('SearchFixedFilterService', () => { expect(query).toContain(itemUUID); }); }); - }); diff --git a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts index 7d59e5a4461..0f17b508c9b 100644 --- a/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-fixed-filter.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; -import { flatMap, map } from 'rxjs/operators'; -import { Observable , of as observableOf } from 'rxjs'; +import { flatMap, map, switchMap, tap } from 'rxjs/operators'; +import { Observable, of as observableOf } from 'rxjs'; import { HALEndpointService } from '../../../core/shared/hal-endpoint.service'; import { GetRequest, RestRequest } from '../../../core/data/request.models'; import { RequestService } from '../../../core/data/request.service'; @@ -33,7 +33,7 @@ export class SearchFixedFilterService { getQueryByFilterName(filterName: string): Observable { if (hasValue(filterName)) { const requestUuid = this.requestService.generateRequestId(); - this.halService.getEndpoint(this.queryByFilterPath).pipe( + const requestObs = this.halService.getEndpoint(this.queryByFilterPath).pipe( map((url: string) => { url += ('/' + filterName); const request = new GetRequest(requestUuid, url); @@ -44,10 +44,12 @@ export class SearchFixedFilterService { }); }), configureRequest(this.requestService) - ).subscribe(); + ); - // get search results from response cache - const filterQuery: Observable = this.requestService.getByUUID(requestUuid).pipe( + const requestEntryObs = requestObs.pipe( + switchMap((request: RestRequest) => this.requestService.getByHref(request.href)), + ); + const filterQuery = requestEntryObs.pipe( getResponseFromEntry(), map((response: FilteredDiscoveryQueryResponse) => response.filterQuery @@ -75,5 +77,4 @@ export class SearchFixedFilterService { getFilterByRelation(relationType: string, itemUUID: string): string { return `f.${relationType}=${itemUUID}`; } - } diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index c11e863429f..b4d8c70f111 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -2,7 +2,7 @@
+ [resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="inPlaceSearch">
this.service.search(options).pipe(getSucceededRemoteData()))) + switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(observableOf(undefined))))) .subscribe((results) => { this.resultsRD$.next(results); }); diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index d7ecb0357ee..824d5311554 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -6,7 +6,7 @@

{{ getTitleKey() | translate }}

[objects]="searchResults" [hideGear]="true">
- +
{{ 'search.results.no-results' | translate }} diff --git a/src/app/+search-page/search-results/search-results.component.ts b/src/app/+search-page/search-results/search-results.component.ts index 9656ba95748..3ee2ed3e329 100644 --- a/src/app/+search-page/search-results/search-results.component.ts +++ b/src/app/+search-page/search-results/search-results.component.ts @@ -6,7 +6,7 @@ import { SetViewMode } from '../../shared/view-mode'; import { SearchOptions } from '../search-options.model'; import { SearchResult } from '../search-result.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasNoValue, isNotEmpty } from '../../shared/empty.util'; import { SortOptions } from '../../core/cache/models/sort-options.model'; @Component({ @@ -22,6 +22,8 @@ import { SortOptions } from '../../core/cache/models/sort-options.model'; * Component that represents all results from a search */ export class SearchResultsComponent { + hasNoValue = hasNoValue; + /** * The actual search result objects */ diff --git a/src/app/+search-page/search-service/search-configuration.service.spec.ts b/src/app/+search-page/search-service/search-configuration.service.spec.ts index 79932805c10..fb95ab8d047 100644 --- a/src/app/+search-page/search-service/search-configuration.service.spec.ts +++ b/src/app/+search-page/search-service/search-configuration.service.spec.ts @@ -171,20 +171,4 @@ describe('SearchConfigurationService', () => { expect((service as any).routeService.getRouteParameterValue).toHaveBeenCalledWith('filter'); }); }); - - describe('when updateFixedFilter is called', () => { - const filter = 'filter'; - - beforeEach(() => { - service.updateFixedFilter(filter); - }); - - it('should update the paginated search options with the correct fixed filter', () => { - expect(service.paginatedSearchOptions.getValue().fixedFilter).toEqual(filter); - }); - - it('should update the search options with the correct fixed filter', () => { - expect(service.searchOptions.getValue().fixedFilter).toEqual(filter); - }); - }); }); diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts index 39acd19ccda..14fcdd8d605 100644 --- a/src/app/+search-page/search-service/search-configuration.service.ts +++ b/src/app/+search-page/search-service/search-configuration.service.ts @@ -9,7 +9,7 @@ import { of as observableOf, Subscription } from 'rxjs'; -import { filter, flatMap, map } from 'rxjs/operators'; +import { filter, flatMap, map, switchMap, tap } from 'rxjs/operators'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SearchOptions } from '../search-options.model'; @@ -44,7 +44,7 @@ export class SearchConfigurationService implements OnDestroy { /** * Default configuration parameter setting */ - protected defaultConfiguration = 'default'; + protected defaultConfiguration; /** * Default scope setting @@ -99,10 +99,8 @@ export class SearchConfigurationService implements OnDestroy { const defs = defRD.payload; this.paginatedSearchOptions = new BehaviorSubject(defs); this.searchOptions = new BehaviorSubject(defs); - this.subs.push(this.subscribeToSearchOptions(defs)); this.subs.push(this.subscribeToPaginatedSearchOptions(defs)); - } ) } @@ -206,7 +204,7 @@ export class SearchConfigurationService implements OnDestroy { */ getCurrentFixedFilter(): Observable { return this.routeService.getRouteParameterValue('filter').pipe( - flatMap((f) => this.fixedFilterService.getQueryByFilterName(f)) + switchMap((f) => this.fixedFilterService.getQueryByFilterName(f)) ); } @@ -357,21 +355,7 @@ export class SearchConfigurationService implements OnDestroy { isNotEmptyOperator(), map((fixedFilter) => { return { fixedFilter } - }) + }), ); } - - /** - * Update the fixed filter in paginated and non-paginated search options with a given value - * @param {string} fixedFilter - */ - public updateFixedFilter(fixedFilter: string) { - const currentPaginatedValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); - const updatedPaginatedValue: PaginatedSearchOptions = Object.assign(currentPaginatedValue, { fixedFilter: fixedFilter }); - this.paginatedSearchOptions.next(updatedPaginatedValue); - - const currentValue: SearchOptions = this.searchOptions.getValue(); - const updatedValue: SearchOptions = Object.assign(currentValue, { fixedFilter: fixedFilter }); - this.searchOptions.next(updatedValue); - } } diff --git a/src/app/+search-page/search-settings/search-settings.component.html b/src/app/+search-page/search-settings/search-settings.component.html index d693196dae9..7bdbaa73a88 100644 --- a/src/app/+search-page/search-settings/search-settings.component.html +++ b/src/app/+search-page/search-settings/search-settings.component.html @@ -1,3 +1,4 @@ +

{{ 'search.sidebar.settings.title' | translate}}

diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.html b/src/app/+search-page/search-sidebar/search-sidebar.component.html index 50877052ec8..7a5857fcff4 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.component.html +++ b/src/app/+search-page/search-sidebar/search-sidebar.component.html @@ -10,7 +10,7 @@
diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts index b3efc240e12..602dee33e6a 100644 --- a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts +++ b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts @@ -94,7 +94,7 @@ describe('SearchSwitchConfigurationComponent', () => { }); it('should navigate to the route when selecting an option', () => { - (comp as any).searchService.getSearchLink.and.returnValue(MYDSPACE_ROUTE); + spyOn((comp as any), 'getSearchLinkParts').and.returnValue([MYDSPACE_ROUTE]); comp.selectedOption = MyDSpaceConfigurationValueType.Workflow; const navigationExtras: NavigationExtras = { queryParams: {configuration: MyDSpaceConfigurationValueType.Workflow}, diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts index c34fe203035..1ce1bf84ecc 100644 --- a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts +++ b/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts @@ -20,6 +20,10 @@ import { SearchService } from '../search-service/search.service'; */ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit { + /** + * True when the search component should show results on the current page + */ + @Input() inPlaceSearch; /** * The list of available configuration options */ @@ -56,7 +60,7 @@ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit { queryParams: {configuration: this.selectedOption}, }; - this.router.navigate([this.searchService.getSearchLink()], navigationExtras); + this.router.navigate(this.getSearchLinkParts(), navigationExtras); } /** @@ -77,4 +81,24 @@ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit { this.sub.unsubscribe(); } } + + /** + * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true + */ + public getSearchLink(): string { + if (this.inPlaceSearch) { + return './'; + } + return this.searchService.getSearchLink(); + } + + /** + * @returns {string[]} The base path to the search page, or the current page when inPlaceSearch is true, split in separate pieces + */ + public getSearchLinkParts(): string[] { + if (this.searchService) { + return []; + } + return this.getSearchLink().split('/'); + } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index da01b1297a7..37cc7915582 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -57,7 +57,6 @@ export class AppComponent implements OnInit, AfterViewInit { private angulartics2GoogleAnalytics: Angulartics2GoogleAnalytics, private authService: AuthService, private router: Router, - private routeService: RouteService, private cssService: CSSVariableService, private menuService: MenuService, private windowService: HostWindowService @@ -77,13 +76,10 @@ export class AppComponent implements OnInit, AfterViewInit { metadata.listenForRouteChange(); - routeService.saveRouting(); - if (config.debug) { console.info(config); } this.storeCSSVariables(); - } ngOnInit() { diff --git a/src/app/core/core.effects.ts b/src/app/core/core.effects.ts index bb25c49a7af..9ade23e6c59 100644 --- a/src/app/core/core.effects.ts +++ b/src/app/core/core.effects.ts @@ -6,6 +6,7 @@ import { AuthEffects } from './auth/auth.effects'; import { JsonPatchOperationsEffects } from './json-patch/json-patch-operations.effects'; import { ServerSyncBufferEffects } from './cache/server-sync-buffer.effects'; import { ObjectUpdatesEffects } from './data/object-updates/object-updates.effects'; +import { RouteEffects } from '../shared/services/route.effects'; export const coreEffects = [ RequestEffects, @@ -14,5 +15,6 @@ export const coreEffects = [ AuthEffects, JsonPatchOperationsEffects, ServerSyncBufferEffects, - ObjectUpdatesEffects + ObjectUpdatesEffects, + RouteEffects ]; diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index c93b4bf44b7..7aecb91a7ad 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -13,6 +13,7 @@ import { objectUpdatesReducer, ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; +import { routeReducer, RouteState } from '../shared/services/route.reducer'; export interface CoreState { 'cache/object': ObjectCacheState, @@ -21,7 +22,8 @@ export interface CoreState { 'data/request': RequestState, 'index': MetaIndexState, 'auth': AuthState, - 'json/patch': JsonPatchOperationsState + 'json/patch': JsonPatchOperationsState, + 'route': RouteState } export const coreReducers: ActionReducerMap = { @@ -31,5 +33,6 @@ export const coreReducers: ActionReducerMap = { 'data/request': requestReducer, 'index': indexReducer, 'auth': authReducer, - 'json/patch': jsonPatchOperationsReducer + 'json/patch': jsonPatchOperationsReducer, + 'route': routeReducer }; diff --git a/src/app/core/index/index.actions.ts b/src/app/core/index/index.actions.ts index 98d07d59d5b..42804dbe26a 100644 --- a/src/app/core/index/index.actions.ts +++ b/src/app/core/index/index.actions.ts @@ -14,7 +14,7 @@ export const IndexActionTypes = { /* tslint:disable:max-classes-per-file */ /** - * An ngrx action to add an value to the index + * An ngrx action to add a value to the index */ export class AddToIndexAction implements Action { type = IndexActionTypes.ADD; @@ -40,7 +40,7 @@ export class AddToIndexAction implements Action { } /** - * An ngrx action to remove an value from the index + * An ngrx action to remove a value from the index */ export class RemoveFromIndexByValueAction implements Action { type = IndexActionTypes.REMOVE_BY_VALUE; diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts index ef4660fdd97..a370d3a6329 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-search-result-list-element.component.spec.ts @@ -75,7 +75,6 @@ describe('ItemSearchResultListElementComponent', () => { it('should show the relationship type badge', () => { const badge = fixture.debugElement.query(By.css('span.badge')); - console.log(itemSearchResultListElementComponent.dso); expect(badge.nativeElement.textContent).toContain(type.toLowerCase()); }); }); diff --git a/src/app/shared/services/route.actions.ts b/src/app/shared/services/route.actions.ts new file mode 100644 index 00000000000..968319d260f --- /dev/null +++ b/src/app/shared/services/route.actions.ts @@ -0,0 +1,116 @@ +import { Action } from '@ngrx/store'; +import { type } from '../../shared/ngrx/type'; +import { Params } from '@angular/router'; + +/** + * The list of HrefIndexAction type definitions + */ +export const RouteActionTypes = { + SET_QUERY_PARAMETERS: type('dspace/core/route/SET_QUERY_PARAMETERS'), + SET_PARAMETERS: type('dspace/core/route/SET_PARAMETERS'), + ADD_QUERY_PARAMETER: type('dspace/core/route/ADD_QUERY_PARAMETER'), + ADD_PARAMETER: type('dspace/core/route/ADD_PARAMETER'), + RESET: type('dspace/core/route/RESET'), +}; + +/* tslint:disable:max-classes-per-file */ +/** + * An ngrx action to set the query parameters + */ +export class SetQueryParametersAction implements Action { + type = RouteActionTypes.SET_QUERY_PARAMETERS; + payload: Params; + + /** + * Create a new SetQueryParametersAction + * + * @param parameters + * the query parameters + */ + constructor(parameters: Params) { + this.payload = parameters; + } +} + +/** + * An ngrx action to set the parameters + */ +export class SetParametersAction implements Action { + type = RouteActionTypes.SET_PARAMETERS; + payload: Params; + + /** + * Create a new SetParametersAction + * + * @param parameters + * the parameters + */ + constructor(parameters: Params) { + this.payload = parameters; + } +} + +/** + * An ngrx action to add a query parameter + */ +export class AddQueryParameterAction implements Action { + type = RouteActionTypes.ADD_QUERY_PARAMETER; + payload: { + key: string; + value: string; + }; + + /** + * Create a new AddQueryParameterAction + * + * @param key + * the key to add + * @param value + * the value of this key + */ + constructor(key: string, value: string) { + this.payload = { key, value }; + } +} + +/** + * An ngrx action to add a parameter + */ +export class AddParameterAction implements Action { + type = RouteActionTypes.ADD_PARAMETER; + payload: { + key: string; + value: string; + }; + + /** + * Create a new AddParameterAction + * + * @param key + * the key to add + * @param value + * the value of this key + */ + constructor(key: string, value: string) { + this.payload = { key, value }; + } +} + +/** + * An ngrx action to reset the route state + */ +export class ResetRouteStateAction implements Action { + type = RouteActionTypes.RESET; +} + +/* tslint:enable:max-classes-per-file */ + +/** + * A type to encompass all RouteActions + */ +export type RouteActions = + SetQueryParametersAction + | SetParametersAction + | AddQueryParameterAction + | AddParameterAction + | ResetRouteStateAction; diff --git a/src/app/shared/services/route.effects.ts b/src/app/shared/services/route.effects.ts new file mode 100644 index 00000000000..687f8f99210 --- /dev/null +++ b/src/app/shared/services/route.effects.ts @@ -0,0 +1,23 @@ +import { map } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects' +import * as fromRouter from '@ngrx/router-store'; +import { ResetRouteStateAction } from './route.actions'; + +@Injectable() +export class RouteEffects { + /** + * Effect that resets the route state on reroute + * @type {Observable} + */ + @Effect() routeChange$ = this.actions$ + .pipe( + ofType(fromRouter.ROUTER_NAVIGATION), + map(() => new ResetRouteStateAction()) + ); + + constructor(private actions$: Actions) { + + } + +} diff --git a/src/app/shared/services/route.reducer.ts b/src/app/shared/services/route.reducer.ts new file mode 100644 index 00000000000..b078521c114 --- /dev/null +++ b/src/app/shared/services/route.reducer.ts @@ -0,0 +1,74 @@ +import { Params } from '@angular/router'; +import { + AddParameterAction, + AddQueryParameterAction, + RouteActions, + RouteActionTypes, SetParametersAction, SetQueryParametersAction +} from './route.actions'; + +/** + * Interface to represent the parameter state of a current route in the store + */ +export interface RouteState { + queryParams: Params; + params: Params; +} + +/** + * The initial route state + */ +const initialState: RouteState = { + queryParams: {}, + params: {} +}; + +/** + * Reducer function to save the current route parameters and query parameters in the store + * @param state The current or initial state + * @param action The action to perform on the state + */ +export function routeReducer(state = initialState, action: RouteActions): RouteState { + switch (action.type) { + case RouteActionTypes.RESET: { + return initialState + } + case RouteActionTypes.SET_PARAMETERS: { + return setParameters(state, action as SetParametersAction, 'params'); + } + case RouteActionTypes.SET_QUERY_PARAMETERS: { + return setParameters(state, action as SetQueryParametersAction, 'queryParams'); + } + case RouteActionTypes.ADD_PARAMETER: { + return addParameter(state, action as AddParameterAction, 'params'); + } + case RouteActionTypes.ADD_QUERY_PARAMETER: { + return addParameter(state, action as AddQueryParameterAction, 'queryParams'); + } + default: { + return state; + } + } +} + +/** + * Add a route or query parameter in the store + * @param state The current state + * @param action The add action to perform on the current state + * @param paramType The type of parameter to add: route or query parameter + */ +function addParameter(state: RouteState, action: AddParameterAction | AddQueryParameterAction, paramType: string): RouteState { + const subState = state[paramType]; + const existingValues = subState[action.payload.key] || []; + const newValues = [...existingValues, action.payload.value]; + const newSubstate = Object.assign(subState, { [action.payload.key]: newValues }); + return Object.assign({}, state, { [paramType]: newSubstate }); +} +/** + * Set a route or query parameter in the store + * @param state The current state + * @param action The set action to perform on the current state + * @param paramType The type of parameter to set: route or query parameter + */ +function setParameters(state: RouteState, action: SetParametersAction | SetQueryParametersAction, paramType: string): RouteState { + return Object.assign({}, state, { [paramType]: action.payload }); +} diff --git a/src/app/shared/services/route.service.spec.ts b/src/app/shared/services/route.service.spec.ts index c9b3710ee6c..c6003521a78 100644 --- a/src/app/shared/services/route.service.spec.ts +++ b/src/app/shared/services/route.service.spec.ts @@ -42,6 +42,7 @@ describe('RouteService', () => { provide: ActivatedRoute, useValue: { queryParams: observableOf(paramObject), + params: observableOf(paramObject), queryParamMap: observableOf(convertToParamMap(paramObject)) }, }, diff --git a/src/app/shared/services/route.service.ts b/src/app/shared/services/route.service.ts index a94b7e56daa..dc626484c12 100644 --- a/src/app/shared/services/route.service.ts +++ b/src/app/shared/services/route.service.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, filter, map, mergeMap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, tap } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { ActivatedRoute, @@ -8,24 +8,67 @@ import { RouterStateSnapshot, } from '@angular/router'; -import { Observable } from 'rxjs'; -import { select, Store } from '@ngrx/store'; +import { combineLatest, Observable } from 'rxjs'; +import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { isEqual } from 'lodash'; -import { AppState } from '../../app.reducer'; import { AddUrlToHistoryAction } from '../history/history.actions'; import { historySelector } from '../history/selectors'; +import { SetParametersAction, SetQueryParametersAction } from './route.actions'; +import { CoreState } from '../../core/core.reducers'; +import { hasValue } from '../empty.util'; +import { coreSelector } from '../../core/core.selectors'; + +/** + * Selector to select all route parameters from the store + */ +export const routeParametersSelector = createSelector( + coreSelector, + (state: CoreState) => state.route.params +); + +/** + * Selector to select all query parameters from the store + */ +export const queryParametersSelector = createSelector( + coreSelector, + (state: CoreState) => state.route.queryParams +); + +/** + * Selector to select a specific route parameter from the store + * @param key The key of the parameter + */ +export const routeParameterSelector = (key: string) => parameterSelector(key, routeParametersSelector); + +/** + * Selector to select a specific query parameter from the store + * @param key The key of the parameter + */ +export const queryParameterSelector = (key: string) => parameterSelector(key, queryParametersSelector); + +/** + * Function to select a specific parameter from the store + * @param key The key to look for + * @param paramsSelector The selector that selects the parameters to search in + */ +export function parameterSelector(key: string, paramsSelector: (state: CoreState) => Params): MemoizedSelector { + return createSelector(paramsSelector, (state: Params) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} /** * Service to keep track of the current query parameters */ @Injectable() export class RouteService { - params: Observable; - - constructor(private route: ActivatedRoute, private router: Router, private store: Store) { - this.subscribeToRouterParams(); - + constructor(private route: ActivatedRoute, private router: Router, private store: Store) { + this.saveRouting(); } /** @@ -74,11 +117,11 @@ export class RouteService { } getRouteParameterValue(paramName: string): Observable { - return this.params.pipe(map((params) => params[paramName]),distinctUntilChanged(),); + return this.store.pipe(select(routeParameterSelector(paramName))); } getRouteDataValue(datafield: string): Observable { - return this.route.data.pipe(map((data) => data[datafield]),distinctUntilChanged(),); + return this.route.data.pipe(map((data) => data[datafield]), distinctUntilChanged(),); } /** @@ -114,23 +157,21 @@ export class RouteService { } public saveRouting(): void { - this.router.events - .pipe(filter((event) => event instanceof NavigationEnd)) - .subscribe(({ urlAfterRedirects }: NavigationEnd) => { - this.store.dispatch(new AddUrlToHistoryAction(urlAfterRedirects)) + combineLatest(this.router.events, this.getRouteParams(), this.route.queryParams) + .pipe(filter(([event, params, queryParams]) => event instanceof NavigationEnd)) + .subscribe(([event, params, queryParams]: [NavigationEnd, Params, Params]) => { + this.store.dispatch(new SetParametersAction(params)); + this.store.dispatch(new SetQueryParametersAction(queryParams)); + this.store.dispatch(new AddUrlToHistoryAction(event.urlAfterRedirects)); }); } - subscribeToRouterParams() { - this.params = this.router.events.pipe( - mergeMap((event) => { - let active = this.route; - while (active.firstChild) { - active = active.firstChild; - } - return active.params; - }) - ); + private getRouteParams(): Observable { + let active = this.route; + while (active.firstChild) { + active = active.firstChild; + } + return active.params; } public getHistory(): Observable { diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index 6547a3cc3c2..37ada351559 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -38,7 +38,7 @@ title="{{ listItem.collection.name }}" (click)="onSelect(listItem)">
    -
  • +
  • {{ item.name}}
  • {{ listItem.collection.name}}
  • diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index 2fe424bd3f1..b5768340919 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -237,7 +237,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { if (isEmpty(searchTerm)) { return listCollection; } else { - return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5) + return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); } })); }