diff --git a/src/app/core/shared/search/search-configuration.service.spec.ts b/src/app/core/shared/search/search-configuration.service.spec.ts index 2cafd67cfeb..e96c67937ef 100644 --- a/src/app/core/shared/search/search-configuration.service.spec.ts +++ b/src/app/core/shared/search/search-configuration.service.spec.ts @@ -90,6 +90,7 @@ describe('SearchConfigurationService', () => { })); service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute as any, linkService, halService, requestService, rdb, environment); + service.searchInstanceId = defaults.pagination.id; }); describe('when the scope is called', () => { @@ -181,7 +182,7 @@ describe('SearchConfigurationService', () => { describe('when subscribeToSearchOptions is called', () => { beforeEach(() => { - (service as any).subscribeToSearchOptions(defaults); + (service as any).subscribeToSearchOptions(defaults.pagination.id, defaults); }); it('should call all getters it needs, but not call any others', () => { expect(service.getCurrentPagination).not.toHaveBeenCalled(); @@ -198,14 +199,14 @@ describe('SearchConfigurationService', () => { beforeEach(() => { (service as any).subscribeToPaginatedSearchOptions(defaults.pagination.id, defaults); }); - it('should call all getters it needs', () => { + it('should call the pagination-specific getters it needs', () => { expect(service.getCurrentPagination).toHaveBeenCalled(); expect(service.getCurrentSort).toHaveBeenCalled(); - expect(service.getCurrentScope).toHaveBeenCalled(); - expect(service.getCurrentConfiguration).toHaveBeenCalled(); - expect(service.getCurrentQuery).toHaveBeenCalled(); - expect(service.getCurrentDSOType).toHaveBeenCalled(); - expect(service.getCurrentFilters).toHaveBeenCalled(); + expect(service.getCurrentScope).not.toHaveBeenCalled(); + expect(service.getCurrentConfiguration).not.toHaveBeenCalled(); + expect(service.getCurrentQuery).not.toHaveBeenCalled(); + expect(service.getCurrentDSOType).not.toHaveBeenCalled(); + expect(service.getCurrentFilters).not.toHaveBeenCalled(); }); }); }); @@ -314,13 +315,13 @@ describe('SearchConfigurationService', () => { it('should return all params except the applied filter', () => { service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator); - expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority'); + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.author`, '1282121b-5394-4689-ab93-78d537764052,authority'); }); it('should be able to remove AppliedFilter without operator', () => { service.unselectAppliedFilterParams('dateIssued.max', '2000'); - expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000'); + expect(routeService.getParamsExceptValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.dateIssued.max`, '2000'); }); it('should reset the page to 1', (done: DoneFn) => { @@ -346,13 +347,13 @@ describe('SearchConfigurationService', () => { it('should return all params with the applied filter', () => { service.selectNewAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator); - expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority'); + expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.author`, '1282121b-5394-4689-ab93-78d537764052,authority'); }); it('should be able to add AppliedFilter without operator', () => { service.selectNewAppliedFilterParams('dateIssued.max', '2000'); - expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.dateIssued.max', '2000'); + expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.dateIssued.max`, '2000'); }); it('should reset the page to 1', (done: DoneFn) => { diff --git a/src/app/core/shared/search/search-configuration.service.ts b/src/app/core/shared/search/search-configuration.service.ts index 9b675d6b93b..195ff2b8fdf 100644 --- a/src/app/core/shared/search/search-configuration.service.ts +++ b/src/app/core/shared/search/search-configuration.service.ts @@ -85,9 +85,9 @@ export class SearchConfigurationService implements OnDestroy { private facetLinkPathPrefix = 'discover/facets/'; /** - * Default pagination id + * Default search instance id */ - public paginationID = 'spc'; + public searchInstanceId = 'spc'; /** * Emits the current search options @@ -103,7 +103,7 @@ export class SearchConfigurationService implements OnDestroy { * Default pagination settings */ protected defaultPagination = Object.assign(new PaginationComponentOptions(), { - id: this.paginationID, + id: this.searchInstanceId, pageSize: 10, currentPage: 1, }); @@ -158,13 +158,15 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current configuration string */ - getCurrentConfiguration(defaultConfiguration: string) { + getCurrentConfiguration(defaultConfiguration: string, searchInstanceId = this.searchInstanceId) { return observableCombineLatest([ + this.routeService.getQueryParameterValue(this.getSearchInstanceParam(searchInstanceId, 'configuration')).pipe(startWith(undefined)), + this.routeService.getRouteParameterValue(this.getSearchInstanceParam(searchInstanceId, 'configuration')).pipe(startWith(undefined)), this.routeService.getQueryParameterValue('configuration').pipe(startWith(undefined)), this.routeService.getRouteParameterValue('configuration').pipe(startWith(undefined)), ]).pipe( - map(([queryConfig, routeConfig]) => { - return queryConfig || routeConfig || defaultConfiguration; + map(([instanceQueryConfig, instanceRouteConfig, queryConfig, routeConfig]) => { + return instanceQueryConfig || instanceRouteConfig || queryConfig || routeConfig || defaultConfiguration; }), ); } @@ -172,8 +174,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current scope's identifier */ - getCurrentScope(defaultScope: string) { - return this.routeService.getQueryParameterValue('scope').pipe(map((scope) => { + getCurrentScope(defaultScope: string, searchInstanceId = this.searchInstanceId) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'scope').pipe(map((scope) => { return scope || defaultScope; })); } @@ -181,17 +183,17 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current query string */ - getCurrentQuery(defaultQuery: string) { - return this.routeService.getQueryParameterValue('query').pipe(map((query) => { - return query !== null ? query : defaultQuery; // Allow querying when the value is empty + getCurrentQuery(defaultQuery: string, searchInstanceId = this.searchInstanceId) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'query').pipe(map((query) => { + return hasValue(query) ? query : defaultQuery; // Allow querying when the value is empty })); } /** * @returns {Observable} Emits the current DSpaceObject type as a number */ - getCurrentDSOType(): Observable { - return this.routeService.getQueryParameterValue('dsoType').pipe( + getCurrentDSOType(searchInstanceId = this.searchInstanceId): Observable { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'dsoType').pipe( filter((type) => isNotEmpty(type) && hasValue(DSpaceObjectType[type.toUpperCase()])), map((type) => DSpaceObjectType[type.toUpperCase()])); } @@ -213,20 +215,21 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current active filters with their values as they are sent to the backend */ - getCurrentFilters(): Observable { - return this.routeService.getQueryParamsWithPrefix('f.').pipe(map((filterParams) => { + getCurrentFilters(searchInstanceId = this.searchInstanceId): Observable { + return this.getCurrentFrontendFilters(searchInstanceId).pipe(map((filterParams) => { if (isNotEmpty(filterParams)) { const filters = []; - Object.keys(filterParams).forEach((key) => { + const backendFilterParams = this.getBackendFilterParams(filterParams, searchInstanceId); + Object.keys(backendFilterParams).forEach((key) => { if (key.endsWith('.min') || key.endsWith('.max')) { const realKey = key.slice(0, -4); if (hasNoValue(filters.find((f) => f.key === realKey))) { - const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*'; - const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*'; + const min = backendFilterParams[realKey + '.min'] ? backendFilterParams[realKey + '.min'][0] : '*'; + const max = backendFilterParams[realKey + '.max'] ? backendFilterParams[realKey + '.max'][0] : '*'; filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'], 'equals')); } } else { - filters.push(new SearchFilter(key, filterParams[key])); + filters.push(new SearchFilter(key, backendFilterParams[key])); } }); return filters; @@ -238,22 +241,32 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current fixed filter as a string */ - getCurrentFixedFilter(): Observable { - return this.routeService.getRouteParameterValue('fixedFilterQuery'); + getCurrentFixedFilter(searchInstanceId = this.searchInstanceId): Observable { + return observableCombineLatest([ + this.routeService.getRouteParameterValue(this.getSearchInstanceParam(searchInstanceId, 'fixedFilterQuery')).pipe(startWith(undefined)), + this.routeService.getRouteParameterValue('fixedFilterQuery').pipe(startWith(undefined)), + ]).pipe( + map(([instanceFixedFilter, fixedFilter]) => instanceFixedFilter || fixedFilter), + ); } /** * @returns {Observable} Emits the current active filters with their values as they are displayed in the frontend URL */ - getCurrentFrontendFilters(): Observable { - return this.routeService.getQueryParamsWithPrefix('f.'); + getCurrentFrontendFilters(searchInstanceId = this.searchInstanceId): Observable { + return observableCombineLatest([ + this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceFilterParamPrefix(searchInstanceId)).pipe(startWith({})), + this.routeService.getQueryParamsWithPrefix('f.').pipe(startWith({})), + ]).pipe(map(([instanceFilters, legacyFilters]) => { + return isNotEmpty(instanceFilters) ? instanceFilters : legacyFilters; + })); } /** * @returns {Observable} Emits the current view mode */ - getCurrentViewMode(defaultViewMode: ViewMode) { - return this.routeService.getQueryParameterValue('view').pipe(map((viewMode) => { + getCurrentViewMode(defaultViewMode: ViewMode, searchInstanceId = this.searchInstanceId) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'view').pipe(map((viewMode) => { return viewMode || defaultViewMode; })); } @@ -296,24 +309,68 @@ export class SearchConfigurationService implements OnDestroy { ); } - setPaginationId(paginationId): void { - if (isNotEmpty(paginationId)) { + setSearchInstanceId(searchInstanceId): void { + if (isNotEmpty(searchInstanceId)) { const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, { pagination: Object.assign({}, currentValue.pagination, { - id: paginationId, + id: searchInstanceId, }), }); - // unsubscribe from subscription related to old pagination id - this.unsubscribeFromSearchOptions(this.paginationID); + // unsubscribe from subscription related to old search instance id + this.unsubscribeFromSearchOptions(this.searchInstanceId); - // change to the new pagination id - this.paginationID = paginationId; + // change to the new search instance id + this.searchInstanceId = searchInstanceId; this.paginatedSearchOptions.next(updatedValue); - this.setSearchSubscription(this.paginationID, this.paginatedSearchOptions.value); + this.setSearchSubscription(this.searchInstanceId, this.paginatedSearchOptions.value); } } + getSearchInstanceParam(searchInstanceId: string, parameterName: string): string { + return `${searchInstanceId}.${parameterName}`; + } + + getCurrentSearchInstanceParam(parameterName: string): string { + return this.getSearchInstanceParam(this.searchInstanceId, parameterName); + } + + getSearchInstanceFilterParam(filterName: string, searchInstanceId = this.searchInstanceId): string { + const filterParamName = filterName.startsWith('f.') ? filterName : `f.${filterName}`; + return this.getSearchInstanceParam(searchInstanceId, filterParamName); + } + + getCurrentSearchInstanceFilterParam(filterName: string): string { + return this.getSearchInstanceFilterParam(filterName, this.searchInstanceId); + } + + getSearchInstanceFilterParamPrefix(searchInstanceId = this.searchInstanceId): string { + return this.getSearchInstanceParam(searchInstanceId, 'f.'); + } + + getCurrentPageParam(): string { + return this.paginationService.getPageParam(this.searchInstanceId); + } + + private getCurrentSearchInstanceQueryParam(searchInstanceId: string, parameterName: string): Observable { + return observableCombineLatest([ + this.routeService.getQueryParameterValue(this.getSearchInstanceParam(searchInstanceId, parameterName)).pipe(startWith(undefined)), + this.routeService.getQueryParameterValue(parameterName).pipe(startWith(undefined)), + ]).pipe( + map(([instanceValue, legacyValue]) => hasValue(instanceValue) ? instanceValue : legacyValue), + ); + } + + private getBackendFilterParams(filterParams: Params, searchInstanceId: string): Params { + const backendFilterParams = {}; + const instancePrefix = `${searchInstanceId}.`; + Object.keys(filterParams).forEach((key) => { + const backendKey = key.startsWith(instancePrefix) ? key.substring(instancePrefix.length) : key; + backendFilterParams[backendKey] = filterParams[key]; + }); + return backendFilterParams; + } + /** * Make sure to unsubscribe from all existing subscription to prevent memory leaks */ @@ -337,17 +394,18 @@ export class SearchConfigurationService implements OnDestroy { const defs = defRD.payload; this.paginatedSearchOptions = new BehaviorSubject(defs); this.searchOptions = new BehaviorSubject(defs); - this.setSearchSubscription(this.paginationID, defs); + this.setSearchSubscription(this.searchInstanceId, defs); }); } - private setSearchSubscription(paginationID: string, defaults: PaginatedSearchOptions) { - this.unsubscribeFromSearchOptions(paginationID); + private setSearchSubscription(searchInstanceId: string, defaults: PaginatedSearchOptions) { + const instanceId = searchInstanceId || defaults.pagination.id; + this.unsubscribeFromSearchOptions(instanceId); const subs = [ - this.subscribeToSearchOptions(defaults), - this.subscribeToPaginatedSearchOptions(paginationID || defaults.pagination.id, defaults), + this.subscribeToSearchOptions(instanceId, defaults), + this.subscribeToPaginatedSearchOptions(instanceId, defaults), ]; - this.subs.set(this.paginationID, subs); + this.subs.set(instanceId, subs); } /** @@ -355,39 +413,38 @@ export class SearchConfigurationService implements OnDestroy { * @param {SearchOptions} defaults Default values for when no parameters are available * @returns {Subscription} The subscription to unsubscribe from */ - private subscribeToSearchOptions(defaults: SearchOptions): Subscription { + private subscribeToSearchOptions(searchInstanceId: string, defaults: SearchOptions): Subscription { return observableMerge( - this.getConfigurationPart(defaults.configuration), - this.getScopePart(defaults.scope), - this.getQueryPart(defaults.query), - this.getDSOTypePart(), - this.getFiltersPart(), - this.getFixedFilterPart(), - this.getViewModePart(defaults.view), + this.getConfigurationPart(searchInstanceId, defaults.configuration), + this.getScopePart(searchInstanceId, defaults.scope), + this.getQueryPart(searchInstanceId, defaults.query), + this.getDSOTypePart(searchInstanceId), + this.getFiltersPart(searchInstanceId), + this.getFixedFilterPart(searchInstanceId), + this.getViewModePart(searchInstanceId, defaults.view), ).subscribe((update) => { const currentValue: SearchOptions = this.searchOptions.getValue(); - const updatedValue: SearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update); + const updatedValue: SearchOptions = Object.assign(new SearchOptions({}), currentValue, update); this.searchOptions.next(updatedValue); }); } /** * Sets up a subscription to all necessary parameters to make sure the paginatedSearchOptions emits a new value every time they update - * @param {string} paginationId The pagination ID + * @param {string} searchInstanceId The search instance id * @param {PaginatedSearchOptions} defaults Default values for when no parameters are available * @returns {Subscription} The subscription to unsubscribe from */ - private subscribeToPaginatedSearchOptions(paginationId: string, defaults: PaginatedSearchOptions): Subscription { + private subscribeToPaginatedSearchOptions(searchInstanceId: string, defaults: PaginatedSearchOptions): Subscription { return observableMerge( - this.getConfigurationPart(defaults.configuration), - this.getPaginationPart(paginationId, defaults.pagination), - this.getSortPart(paginationId, defaults.sort), - this.getScopePart(defaults.scope), - this.getQueryPart(defaults.query), - this.getDSOTypePart(), - this.getFiltersPart(), - this.getFixedFilterPart(), - this.getViewModePart(defaults.view), + this.searchOptions.pipe(map((searchOptions: any) => { + const update = Object.assign({}, searchOptions); + delete update.pagination; + delete update.sort; + return update; + })), + this.getPaginationPart(searchInstanceId, defaults.pagination), + this.getSortPart(searchInstanceId, defaults.sort), ).subscribe((update) => { const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue(); const updatedValue: PaginatedSearchOptions = Object.assign(new PaginatedSearchOptions({}), currentValue, update); @@ -396,23 +453,23 @@ export class SearchConfigurationService implements OnDestroy { } /** - * Unsubscribe from all subscriptions related to the given paginationID - * @param paginationId The pagination id + * Unsubscribe from all subscriptions related to the given search instance id + * @param searchInstanceId The search instance id */ - private unsubscribeFromSearchOptions(paginationId: string): void { - if (this.subs.has(this.paginationID)) { - this.subs.get(this.paginationID) + private unsubscribeFromSearchOptions(searchInstanceId: string): void { + if (this.subs.has(searchInstanceId)) { + this.subs.get(searchInstanceId) .filter((sub) => hasValue(sub)) .forEach((sub) => sub.unsubscribe()); - this.subs.delete(paginationId); + this.subs.delete(searchInstanceId); } } /** * @returns {Observable} Emits the current configuration settings as a partial SearchOptions object */ - private getConfigurationPart(defaultConfiguration: string): Observable { - return this.getCurrentConfiguration(defaultConfiguration).pipe(map((configuration) => { + private getConfigurationPart(searchInstanceId: string, defaultConfiguration: string): Observable { + return this.getCurrentConfiguration(defaultConfiguration, searchInstanceId).pipe(map((configuration) => { return { configuration }; })); } @@ -420,8 +477,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current scope's identifier */ - private getScopePart(defaultScope: string): Observable { - return this.getCurrentScope(defaultScope).pipe(map((scope) => { + private getScopePart(searchInstanceId: string, defaultScope: string): Observable { + return this.getCurrentScope(defaultScope, searchInstanceId).pipe(map((scope) => { return { scope }; })); } @@ -429,8 +486,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current query string as a partial SearchOptions object */ - private getQueryPart(defaultQuery: string): Observable { - return this.getCurrentQuery(defaultQuery).pipe(map((query) => { + private getQueryPart(searchInstanceId: string, defaultQuery: string): Observable { + return this.getCurrentQuery(defaultQuery, searchInstanceId).pipe(map((query) => { return { query }; })); } @@ -438,8 +495,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current query string as a partial SearchOptions object */ - private getDSOTypePart(): Observable { - return this.getCurrentDSOType().pipe(map((dsoType) => { + private getDSOTypePart(searchInstanceId: string): Observable { + return this.getCurrentDSOType(searchInstanceId).pipe(map((dsoType) => { return { dsoType }; })); } @@ -465,8 +522,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current active filters as a partial SearchOptions object */ - private getFiltersPart(): Observable { - return this.getCurrentFilters().pipe(map((filters) => { + private getFiltersPart(searchInstanceId: string): Observable { + return this.getCurrentFilters(searchInstanceId).pipe(map((filters) => { return { filters }; })); } @@ -474,8 +531,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current fixed filter as a partial SearchOptions object */ - private getFixedFilterPart(): Observable { - return this.getCurrentFixedFilter().pipe( + private getFixedFilterPart(searchInstanceId: string): Observable { + return this.getCurrentFixedFilter(searchInstanceId).pipe( isNotEmptyOperator(), map((fixedFilter) => { return { fixedFilter }; @@ -579,9 +636,11 @@ export class SearchConfigurationService implements OnDestroy { * @param operator The {@link AppliedFilter}'s optional operator */ unselectAppliedFilterParams(filterName: string, value: string, operator?: string): Observable { - return this.routeService.getParamsExceptValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( + const filterParam: string = this.getCurrentSearchInstanceFilterParam(filterName); + return this.routeService.getParamsExceptValue(filterParam, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( map((params: Params) => Object.assign(params, { - [this.paginationService.getPageParam(this.paginationID)]: 1, + [`f.${filterName}`]: null, + [this.paginationService.getPageParam(this.searchInstanceId)]: 1, })), ); } @@ -594,9 +653,11 @@ export class SearchConfigurationService implements OnDestroy { * @param operator The {@link AppliedFilter}'s optional operator */ selectNewAppliedFilterParams(filterName: string, value: string, operator?: string): Observable { - return this.routeService.getParamsWithAdditionalValue(`f.${filterName}`, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( + const filterParam: string = this.getCurrentSearchInstanceFilterParam(filterName); + return this.routeService.getParamsWithAdditionalValue(filterParam, hasValue(operator) ? addOperatorToFilterValue(value, operator) : value).pipe( map((params: Params) => Object.assign(params, { - [this.paginationService.getPageParam(this.paginationID)]: 1, + [`f.${filterName}`]: null, + [this.paginationService.getPageParam(this.searchInstanceId)]: 1, })), ); } @@ -604,8 +665,8 @@ export class SearchConfigurationService implements OnDestroy { /** * @returns {Observable} Emits the current view mode as a partial SearchOptions object */ - private getViewModePart(defaultViewMode: ViewMode): Observable { - return this.getCurrentViewMode(defaultViewMode).pipe(map((view) => { + private getViewModePart(searchInstanceId: string, defaultViewMode: ViewMode): Observable { + return this.getCurrentViewMode(defaultViewMode, searchInstanceId).pipe(map((view) => { return { view }; })); } diff --git a/src/app/core/shared/search/search-filter.service.ts b/src/app/core/shared/search/search-filter.service.ts index ddf19632c54..506f76fd264 100644 --- a/src/app/core/shared/search/search-filter.service.ts +++ b/src/app/core/shared/search/search-filter.service.ts @@ -1,5 +1,4 @@ import { Injectable } from '@angular/core'; -import { Params } from '@angular/router'; import { createSelector, MemoizedSelector, @@ -75,7 +74,14 @@ export class SearchFilterService { * @param {string} filterValue The value for which to search * @returns {Observable} Emit true when the filter is active with the given value */ - isFilterActiveWithValue(paramName: string, filterValue: string): Observable { + isFilterActiveWithValue(paramName: string, filterValue: string, searchInstanceId?: string): Observable { + if (hasValue(searchInstanceId)) { + return observableCombineLatest( + this.routeService.hasQueryParamWithValue(this.getSearchInstanceParam(searchInstanceId, paramName), filterValue), + this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')), + this.routeService.hasQueryParamWithValue(paramName, filterValue), + ).pipe(map(([instanceActive, instanceFilters, legacyActive]) => instanceActive || (!isNotEmpty(instanceFilters) && legacyActive))); + } return this.routeService.hasQueryParamWithValue(paramName, filterValue); } @@ -84,7 +90,14 @@ export class SearchFilterService { * @param {string} paramName The parameter name of the filter's configuration for which to search * @returns {Observable} Emit true when the filter is active with any value */ - isFilterActive(paramName: string): Observable { + isFilterActive(paramName: string, searchInstanceId?: string): Observable { + if (hasValue(searchInstanceId)) { + return observableCombineLatest( + this.routeService.hasQueryParam(this.getSearchInstanceParam(searchInstanceId, paramName)), + this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')), + this.routeService.hasQueryParam(paramName), + ).pipe(map(([instanceActive, instanceFilters, legacyActive]) => instanceActive || (!isNotEmpty(instanceFilters) && legacyActive))); + } return this.routeService.hasQueryParam(paramName); } @@ -92,16 +105,16 @@ export class SearchFilterService { * Fetch the current active scope from the query parameters * @returns {Observable} */ - getCurrentScope(): Observable { - return this.routeService.getQueryParameterValue('scope'); + getCurrentScope(searchInstanceId?: string) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'scope'); } /** * Fetch the current query from the query parameters * @returns {Observable} */ - getCurrentQuery(): Observable { - return this.routeService.getQueryParameterValue('query'); + getCurrentQuery(searchInstanceId?: string) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'query'); } /** @@ -142,7 +155,13 @@ export class SearchFilterService { * Fetch the current active filters from the query parameters * @returns {Observable} */ - getCurrentFilters(): Observable { + getCurrentFilters(searchInstanceId?: string) { + if (hasValue(searchInstanceId)) { + return observableCombineLatest( + this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')), + this.routeService.getQueryParamsWithPrefix('f.'), + ).pipe(map(([instanceFilters, legacyFilters]) => isNotEmpty(instanceFilters) ? instanceFilters : legacyFilters)); + } return this.routeService.getQueryParamsWithPrefix('f.'); } @@ -150,8 +169,8 @@ export class SearchFilterService { * Fetch the current view from the query parameters * @returns {Observable} */ - getCurrentView(): Observable { - return this.routeService.getQueryParameterValue('view'); + getCurrentView(searchInstanceId?: string) { + return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'view'); } /** @@ -190,6 +209,20 @@ export class SearchFilterService { return `${new EmphasizePipe().transform(facet.value, query)} (${facet.count})`; } + private getSearchInstanceParam(searchInstanceId: string, parameterName: string): string { + return `${searchInstanceId}.${parameterName}`; + } + + private getCurrentSearchInstanceQueryParam(searchInstanceId: string | undefined, parameterName: string): Observable { + if (hasValue(searchInstanceId)) { + return observableCombineLatest( + this.routeService.getQueryParameterValue(this.getSearchInstanceParam(searchInstanceId, parameterName)), + this.routeService.getQueryParameterValue(parameterName), + ).pipe(map(([instanceValue, legacyValue]) => hasValue(instanceValue) ? instanceValue : legacyValue)); + } + return this.routeService.getQueryParameterValue(parameterName); + } + /** * Checks if the state of a given filter is currently collapsed or not * @param {string} filterName The filtername for which the collapsed state is checked diff --git a/src/app/core/shared/search/search.service.spec.ts b/src/app/core/shared/search/search.service.spec.ts index d10283fd8e5..6a6ac68837c 100644 --- a/src/app/core/shared/search/search.service.spec.ts +++ b/src/app/core/shared/search/search.service.spec.ts @@ -97,13 +97,13 @@ describe('SearchService', () => { it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { service.setViewMode(ViewMode.ListElement); - expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement }); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { 'test-id.view': ViewMode.ListElement }); }); it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { service.setViewMode(ViewMode.GridElement); - expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement }); + expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { 'test-id.view': ViewMode.GridElement }); }); }); diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index c694c6ce24d..50d2dae3b90 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -342,8 +342,10 @@ export class SearchService { */ getViewMode(): Observable { return this.routeService.getQueryParamMap().pipe(map((params) => { - if (isNotEmpty(params.get('view')) && hasValue(params.get('view'))) { - return params.get('view'); + const viewParam = this.searchConfigurationService.getCurrentSearchInstanceParam('view'); + const view = hasValue(params.get(viewParam)) ? params.get(viewParam) : params.get('view'); + if (isNotEmpty(view) && hasValue(view)) { + return view; } else { return ViewMode.ListElement; } @@ -356,16 +358,16 @@ export class SearchService { * @param {string[]} searchLinkParts */ setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) { - this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1)) + this.paginationService.getCurrentPagination(this.searchConfigurationService.searchInstanceId, new PaginationComponentOptions()).pipe(take(1)) .subscribe((config) => { let pageParams = { page: 1 }; - const queryParams = { view: viewMode }; + const queryParams = { [this.searchConfigurationService.getCurrentSearchInstanceParam('view')]: viewMode }; if (viewMode === ViewMode.DetailedListElement) { pageParams = Object.assign(pageParams, { pageSize: 1 }); } else if (config.pageSize === 1) { pageParams = Object.assign(pageParams, { pageSize: 10 }); } - this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams); + this.paginationService.updateRouteWithUrl(this.searchConfigurationService.searchInstanceId, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams); }); } diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.ts b/src/app/home-page/recent-item-list/recent-item-list.component.ts index d83993bd7b9..469169bfc34 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.ts +++ b/src/app/home-page/recent-item-list/recent-item-list.component.ts @@ -127,7 +127,7 @@ export class RecentItemListComponent implements OnInit, OnDestroy { } onLoadMore(): void { - this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, ['search'], { + this.paginationService.updateRouteWithUrl(this.searchConfigurationService.searchInstanceId, ['search'], { sortField: environment.homePage.recentSubmissions.sortField, sortDirection: 'DESC' as SortDirection, page: 1, diff --git a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts index 098e1f51cc6..0d44fbcecee 100644 --- a/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts +++ b/src/app/my-dspace-page/my-dspace-configuration.service.spec.ts @@ -151,7 +151,7 @@ describe('MyDSpaceConfigurationService', () => { describe('when subscribeToSearchOptions is called', () => { beforeEach(() => { - (service as any).subscribeToSearchOptions(defaults); + (service as any).subscribeToSearchOptions(defaults.pagination.id, defaults); }); it('should call all getters it needs, but not call any others', () => { expect(service.getCurrentPagination).not.toHaveBeenCalled(); @@ -168,14 +168,14 @@ describe('MyDSpaceConfigurationService', () => { beforeEach(() => { (service as any).subscribeToPaginatedSearchOptions('id', defaults); }); - it('should call all getters it needs', () => { + it('should call the pagination-specific getters it needs', () => { expect(service.getCurrentPagination).toHaveBeenCalled(); expect(service.getCurrentSort).toHaveBeenCalled(); - expect(service.getCurrentScope).toHaveBeenCalled(); - expect(service.getCurrentConfiguration).toHaveBeenCalled(); - expect(service.getCurrentQuery).toHaveBeenCalled(); - expect(service.getCurrentDSOType).toHaveBeenCalled(); - expect(service.getCurrentFilters).toHaveBeenCalled(); + expect(service.getCurrentScope).not.toHaveBeenCalled(); + expect(service.getCurrentConfiguration).not.toHaveBeenCalled(); + expect(service.getCurrentQuery).not.toHaveBeenCalled(); + expect(service.getCurrentDSOType).not.toHaveBeenCalled(); + expect(service.getCurrentFilters).not.toHaveBeenCalled(); }); }); }); diff --git a/src/app/my-dspace-page/my-dspace-configuration.service.ts b/src/app/my-dspace-page/my-dspace-configuration.service.ts index 9a7f3764046..f958af9297a 100644 --- a/src/app/my-dspace-page/my-dspace-configuration.service.ts +++ b/src/app/my-dspace-page/my-dspace-configuration.service.ts @@ -47,6 +47,11 @@ export const SEARCH_CONFIG_SERVICE: InjectionToken = */ @Injectable({ providedIn: 'root' }) export class MyDSpaceConfigurationService extends SearchConfigurationService { + /** + * Search instance id used for the MyDSpace search component. + */ + public searchInstanceId = 'mydspace-page'; + /** * Default pagination settings */ diff --git a/src/app/my-dspace-page/my-dspace.guard.ts b/src/app/my-dspace-page/my-dspace.guard.ts index c174c9594ee..3f1f24dabaf 100644 --- a/src/app/my-dspace-page/my-dspace.guard.ts +++ b/src/app/my-dspace-page/my-dspace.guard.ts @@ -26,9 +26,11 @@ export const myDSpaceGuard: CanActivateFn = ( configurationService: MyDSpaceConfigurationService = inject(MyDSpaceConfigurationService), router: Router = inject(Router), ): Observable => { + const configurationParam = configurationService.getCurrentSearchInstanceParam('configuration'); + const configuration = route.queryParamMap.get(configurationParam) || route.queryParamMap.get('configuration'); return configurationService.getAvailableConfigurationTypes().pipe( first(), - map((configurationList) => validateConfigurationParam(router, route.queryParamMap.get('configuration'), configurationList))); + map((configurationList) => validateConfigurationParam(router, configurationService, configuration, configurationList))); }; /** @@ -36,18 +38,22 @@ export const myDSpaceGuard: CanActivateFn = ( * * @param router * the service router + * @param configurationService + * the MyDSpace configuration service * @param configuration * the configuration to validate * @param configurationList * the list of available configuration * */ -function validateConfigurationParam(router: Router, configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean { +function validateConfigurationParam(router: Router, configurationService: MyDSpaceConfigurationService, configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean { const configurationDefault: string = configurationList[0]; if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) { // If configuration param is empty or is not included in available configurations redirect to a default configuration value const navigationExtras: NavigationExtras = { - queryParams: { configuration: configurationDefault }, + queryParams: { + [configurationService.getCurrentSearchInstanceParam('configuration')]: configurationDefault, + }, }; router.navigate([MYDSPACE_ROUTE], navigationExtras); diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts index 3d383cbcef3..2792330d0ac 100644 --- a/src/app/search-navbar/search-navbar.component.spec.ts +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -21,7 +21,9 @@ import { TranslateModule, } from '@ngx-translate/core'; +import { PaginationService } from '../core/pagination/pagination.service'; import { SearchService } from '../core/shared/search/search.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock'; import { SearchNavbarComponent } from './search-navbar.component'; @@ -54,6 +56,14 @@ describe('SearchNavbarComponent', () => { ], providers: [ { provide: SearchService, useValue: mockSearchService }, + { provide: PaginationService, useValue: { getPageParam: (id: string) => `${id}.page` } }, + { + provide: SearchConfigurationService, + useValue: { + searchInstanceId: 'spc', + getCurrentSearchInstanceParam: (param: string) => `spc.${param}`, + }, + }, ], }) .compileComponents(); @@ -100,7 +110,7 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with empty query', () => { - const extras: NavigationExtras = { queryParams: { query: '' } }; + const extras: NavigationExtras = { queryParams: { 'spc.query': '', 'spc.page': 1 } }; expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); expect(router.navigate).toHaveBeenCalledWith(['search'], extras); }); @@ -125,7 +135,7 @@ describe('SearchNavbarComponent', () => { fixture.detectChanges(); })); it('to search page with query', async () => { - const extras: NavigationExtras = { queryParams: { query: 'test' } }; + const extras: NavigationExtras = { queryParams: { 'spc.query': 'test', 'spc.page': 1 } }; expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); expect(router.navigate).toHaveBeenCalledWith(['search'], extras); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts index 6e9ca7c637f..ef4e85d1d91 100644 --- a/src/app/search-navbar/search-navbar.component.ts +++ b/src/app/search-navbar/search-navbar.component.ts @@ -11,7 +11,9 @@ import { import { Router } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; +import { PaginationService } from '../core/pagination/pagination.service'; import { SearchService } from '../core/shared/search/search.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { expandSearchInput } from '../shared/animations/slide'; import { BrowserOnlyPipe } from '../shared/utils/browser-only.pipe'; import { ClickOutsideDirective } from '../shared/utils/click-outside.directive'; @@ -43,7 +45,11 @@ export class SearchNavbarComponent { // Search input field @ViewChild('searchInput') searchField: ElementRef; - constructor(private formBuilder: UntypedFormBuilder, private router: Router, private searchService: SearchService) { + constructor(private formBuilder: UntypedFormBuilder, + private router: Router, + private searchService: SearchService, + private searchConfigurationService: SearchConfigurationService, + private paginationService: PaginationService) { this.searchForm = this.formBuilder.group(({ query: '', })); @@ -80,7 +86,10 @@ export class SearchNavbarComponent { */ onSubmit(data: any) { this.collapse(); - const queryParams = Object.assign({}, data); + const queryParams = { + [this.searchConfigurationService.getCurrentSearchInstanceParam('query')]: data.query, + [this.paginationService.getPageParam(this.searchConfigurationService.searchInstanceId)]: 1, + }; const linkToNavigateTo = [this.searchService.getSearchLink().replace('/', '')]; this.searchForm.reset(); diff --git a/src/app/search-page/configuration-search-page.component.spec.ts b/src/app/search-page/configuration-search-page.component.spec.ts index 862d6087914..c524c365e8d 100644 --- a/src/app/search-page/configuration-search-page.component.spec.ts +++ b/src/app/search-page/configuration-search-page.component.spec.ts @@ -64,8 +64,8 @@ describe('ConfigurationSearchPageComponent', () => { expect(comp.configuration).toBe(CONFIGURATION); expect(comp.fixedFilterQuery).toBe(QUERY); - expect(routeService.setParameter).toHaveBeenCalledWith('configuration', CONFIGURATION); - expect(routeService.setParameter).toHaveBeenCalledWith('fixedFilterQuery', QUERY); + expect(routeService.setParameter).toHaveBeenCalledWith('search-test-page-id.configuration', CONFIGURATION); + expect(routeService.setParameter).toHaveBeenCalledWith('search-test-page-id.fixedFilterQuery', QUERY); }); }); diff --git a/src/app/search-page/configuration-search-page.component.ts b/src/app/search-page/configuration-search-page.component.ts index 4fc03d349cc..dc971998fb1 100644 --- a/src/app/search-page/configuration-search-page.component.ts +++ b/src/app/search-page/configuration-search-page.component.ts @@ -44,6 +44,11 @@ import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-sw provide: SEARCH_CONFIG_SERVICE, useClass: SearchConfigurationService, }, + { + provide: SearchConfigurationService, + useExisting: SEARCH_CONFIG_SERVICE, + }, + SearchService, ], imports: [ AsyncPipe, diff --git a/src/app/search-page/search-page.component.ts b/src/app/search-page/search-page.component.ts index d5df0ec65c6..30732c13211 100644 --- a/src/app/search-page/search-page.component.ts +++ b/src/app/search-page/search-page.component.ts @@ -1,5 +1,6 @@ import { Component } from '@angular/core'; +import { SearchService } from '../core/shared/search/search.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SEARCH_CONFIG_SERVICE } from '../my-dspace-page/my-dspace-configuration.service'; import { ThemedSearchComponent } from '../shared/search/themed-search.component'; @@ -12,6 +13,11 @@ import { ThemedSearchComponent } from '../shared/search/themed-search.component' provide: SEARCH_CONFIG_SERVICE, useClass: SearchConfigurationService, }, + { + provide: SearchConfigurationService, + useExisting: SEARCH_CONFIG_SERVICE, + }, + SearchService, ], imports: [ ThemedSearchComponent, diff --git a/src/app/search-page/themed-configuration-search-page.component.ts b/src/app/search-page/themed-configuration-search-page.component.ts index 82bed710092..08377032cea 100644 --- a/src/app/search-page/themed-configuration-search-page.component.ts +++ b/src/app/search-page/themed-configuration-search-page.component.ts @@ -59,9 +59,9 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent; + private viewMode$: BehaviorSubject = new BehaviorSubject(undefined); + /** * The available view modes */ @@ -227,10 +240,9 @@ export class ObjectCollectionComponent implements OnInit { } ngOnInit(): void { - this.currentMode$ = this.route - .queryParams + this.currentMode$ = observableCombineLatest([this.route.queryParams, this.viewMode$]) .pipe( - map((params) => isEmpty(params?.view) ? ViewMode.ListElement : params.view), + map(([params, viewMode]) => viewMode || (isEmpty(params?.view) ? ViewMode.ListElement : params.view)), distinctUntilChanged(), ); if (isPlatformBrowser(this.platformId)) { diff --git a/src/app/shared/search-form/search-form.component.spec.ts b/src/app/shared/search-form/search-form.component.spec.ts index a1c02f3a8e9..81c7950175c 100644 --- a/src/app/shared/search-form/search-form.component.spec.ts +++ b/src/app/shared/search-form/search-form.component.spec.ts @@ -35,8 +35,10 @@ describe('SearchFormComponent', () => { const searchService = new SearchServiceStub(); let searchFilterService: SearchFilterServiceStub; const paginationService = new PaginationServiceStub(); - const searchConfigService = { paginationID: 'test-id' }; - const firstPage = { 'spc.page': 1 }; + const searchConfigService = { + searchInstanceId: 'test-id', + getCurrentSearchInstanceParam: (param: string) => `test-id.${param}`, + }; const dspaceObjectService = { findById: () => createSuccessfulRemoteDataObject$(undefined), }; @@ -111,11 +113,17 @@ describe('SearchFormComponent', () => { const scope = 'MCU'; let searchQuery = {}; - it('should navigate to the search first page even when no parameters are provided', () => { + beforeEach(() => { + searchQuery = {}; + router.navigate.calls.reset(); + }); + + it('should navigate to the search page even when no parameters are provided', () => { + const expectedQueryParams = { 'test-id.page': 1 }; comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { - queryParams: { ...searchQuery, ...firstPage }, + queryParams: expectedQueryParams, queryParamsHandling: 'merge', }); }); @@ -124,11 +132,15 @@ describe('SearchFormComponent', () => { searchQuery = { query: query, }; + const expectedQueryParams = { + 'test-id.page': 1, + 'test-id.query': query, + }; comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { - queryParams: { ...searchQuery, ...firstPage }, + queryParams: expectedQueryParams, queryParamsHandling: 'merge', }); }); @@ -137,11 +149,15 @@ describe('SearchFormComponent', () => { searchQuery = { scope: scope, }; + const expectedQueryParams = { + 'test-id.page': 1, + 'test-id.scope': scope, + }; comp.updateSearch(searchQuery); expect(router.navigate).toHaveBeenCalledWith(comp.getSearchLinkParts(), { - queryParams: { ...searchQuery, ...firstPage }, + queryParams: expectedQueryParams, queryParamsHandling: 'merge', }); }); diff --git a/src/app/shared/search-form/search-form.component.ts b/src/app/shared/search-form/search-form.component.ts index 0d118f5cf69..591972e089d 100644 --- a/src/app/shared/search-form/search-form.component.ts +++ b/src/app/shared/search-form/search-form.component.ts @@ -7,7 +7,10 @@ import { Output, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { Router } from '@angular/router'; +import { + Params, + Router, +} from '@angular/router'; import { NgbModal, NgbTooltip, @@ -146,17 +149,15 @@ export class SearchFormComponent implements OnChanges { * @param data Updated parameters */ updateSearch(data: any) { - const goToFirstPage = { 'spc.page': 1 }; - - const queryParams = Object.assign( - { - ...goToFirstPage, - }, - data, - ); - if (hasValue(data.scope) && this.hideScopeInUrl) { - delete queryParams.scope; - } + const queryParams: Params = { + [this.paginationService.getPageParam(this.searchConfig.searchInstanceId)]: 1, + }; + Object.keys(data).forEach((key) => { + if (key === 'scope' && hasValue(data.scope) && this.hideScopeInUrl) { + return; + } + queryParams[this.searchConfig.getCurrentSearchInstanceParam(key)] = data[key]; + }); void this.router.navigate(this.getSearchLinkParts(), { queryParams: queryParams, diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts index f527592b7ef..a974382b947 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.ts @@ -73,7 +73,7 @@ export class SearchFacetOptionComponent implements OnInit { */ searchLink: string; - paginationId: string; + searchInstanceId: string; constructor(protected searchService: SearchService, protected filterService: SearchFilterService, @@ -89,7 +89,7 @@ export class SearchFacetOptionComponent implements OnInit { * Initializes all observable instance variables and starts listening to them */ ngOnInit(): void { - this.paginationId = this.searchConfigService.paginationID; + this.searchInstanceId = this.searchConfigService.searchInstanceId; this.searchLink = this.getSearchLink(); this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked)); this.addQueryParams$ = this.updateAddParams(); @@ -99,7 +99,7 @@ export class SearchFacetOptionComponent implements OnInit { * Checks if a value for this filter is currently active */ isChecked(): Observable { - return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue()); + return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.getFacetValue(), this.searchInstanceId); } /** diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts index 4f46a5dd2a7..df0144b94f9 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.spec.ts @@ -82,7 +82,8 @@ describe('SearchFacetRangeOptionComponent', () => { { provide: SearchConfigurationService, useValue: { searchOptions: of({}), - paginationId: 'page-id', + searchInstanceId: 'page-id', + getCurrentSearchInstanceFilterParam: (param: string) => `page-id.${param}`, }, }, { @@ -136,8 +137,10 @@ describe('SearchFacetRangeOptionComponent', () => { }; (comp as any).updateChangeParams(); expect(comp.changeQueryParams).toEqual({ - [mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [50], - [mockFilterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: [60], + [`page-id.${mockFilterConfig.paramName}${RANGE_FILTER_MIN_SUFFIX}`]: [50], + [`page-id.${mockFilterConfig.paramName}${RANGE_FILTER_MAX_SUFFIX}`]: [60], + [mockFilterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: null, + [mockFilterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: null, ['page-id.page']: 1, }); }); diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts index c3120148670..08e8736a86c 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.ts @@ -106,7 +106,7 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { * Checks if a value for this filter is currently active */ private isChecked(): Observable { - return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value); + return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value, this.searchConfigService.searchInstanceId); } /** @@ -126,10 +126,13 @@ export class SearchFacetRangeOptionComponent implements OnInit, OnDestroy { const parts = this.filterValue.value.split(rangeDelimiter); const min = parts.length > 1 ? Number(parts[0].trim()) : this.filterValue.value; const max = parts.length > 1 ? Number(parts[1].trim()) : this.filterValue.value; - const page = this.paginationService.getPageParam(this.searchConfigService.paginationID); + const page = this.paginationService.getPageParam(this.searchConfigService.searchInstanceId); + const filterParamName = this.searchConfigService.getCurrentSearchInstanceFilterParam(this.filterConfig.paramName); this.changeQueryParams = { - [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min], - [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: max === new Date().getUTCFullYear() ? null : [max], + [filterParamName + RANGE_FILTER_MIN_SUFFIX]: [min], + [filterParamName + RANGE_FILTER_MAX_SUFFIX]: max === new Date().getUTCFullYear() ? null : [max], + [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: null, + [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: null, [page]: 1, }; } diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 1f55941f00b..ba46e87f5e6 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -148,8 +148,11 @@ describe('SearchRangeFilterComponent', () => { it('should call navigate on the router with the right searchlink and parameters', () => { expect(router.navigate).toHaveBeenCalledWith(searchUrl.split('/'), { queryParams: { - [mockFilterConfig.paramName + minSuffix]: [1900], - [mockFilterConfig.paramName + maxSuffix]: [1950], + [`test-id.${mockFilterConfig.paramName}${minSuffix}`]: [1900], + [`test-id.${mockFilterConfig.paramName}${maxSuffix}`]: [1950], + [mockFilterConfig.paramName + minSuffix]: null, + [mockFilterConfig.paramName + maxSuffix]: null, + 'test-id.page': 1, }, queryParamsHandling: 'merge', }); diff --git a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index 9592fee78a0..1eae352716d 100644 --- a/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -134,8 +134,15 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple this.max = yearFromString(this.filterConfig.maxValue) || this.max; this.minLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.min.placeholder'); this.maxLabel = this.translateService.instant('search.filters.filter.' + this.filterConfig.name + '.max.placeholder'); - const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)); - const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)); + const filterParamName = this.searchConfigService.getCurrentSearchInstanceFilterParam(this.filterConfig.paramName); + const iniMin = observableCombineLatest([ + this.route.getQueryParameterValue(filterParamName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)), + this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined)), + ]).pipe(map(([instanceMin, legacyMin]: [string, string]) => hasValue(instanceMin) ? instanceMin : legacyMin)); + const iniMax = observableCombineLatest([ + this.route.getQueryParameterValue(filterParamName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)), + this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined)), + ]).pipe(map(([instanceMax, legacyMax]: [string, string]) => hasValue(instanceMax) ? instanceMax : legacyMax)); this.subs.push(observableCombineLatest([iniMin, iniMax]).pipe( map(([min, max]: [string, string]) => { const minimum = hasValue(min) ? Number(min) : this.min; @@ -176,8 +183,11 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple void this.router.navigate(this.getSearchLinkParts(), { queryParams: { - [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin, - [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: newMax, + [this.searchConfigService.getCurrentSearchInstanceFilterParam(this.filterConfig.paramName) + RANGE_FILTER_MIN_SUFFIX]: newMin, + [this.searchConfigService.getCurrentSearchInstanceFilterParam(this.filterConfig.paramName) + RANGE_FILTER_MAX_SUFFIX]: newMax, + [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: null, + [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: null, + [this.searchConfigService.getCurrentPageParam()]: 1, }, queryParamsHandling: 'merge', }); diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index e1039b8d31b..865b77759f8 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -30,11 +30,13 @@

{{ (configuration ? configuration + '.search.results.head' : 'search.results [selectionConfig]="selectionConfig" [linkType]="linkType" [context]="context" + [viewMode]="searchConfig?.view" [hidePaginationDetail]="hidePaginationDetail" [showThumbnails]="showThumbnails" (contentChange)="contentChange.emit($event)" (deselectObject)="deselectObject.emit($event)" (selectObject)="selectObject.emit($event)"> + } @@ -54,7 +56,7 @@

{{ (configuration ? configuration + '.search.results.head' : 'search.results
{{ 'search.results.no-results' | translate }} {{"search.results.no-results-link" | translate}} diff --git a/src/app/shared/search/search-results/search-results.component.ts b/src/app/shared/search/search-results/search-results.component.ts index 77b9e3f0648..08dda1270f8 100644 --- a/src/app/shared/search/search-results/search-results.component.ts +++ b/src/app/shared/search/search-results/search-results.component.ts @@ -5,7 +5,10 @@ import { Input, Output, } from '@angular/core'; -import { RouterLink } from '@angular/router'; +import { + Params, + RouterLink, +} from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { @@ -187,4 +190,15 @@ export class SearchResultsComponent { return result; } + + getExactMatchQueryParams(): Params { + if (hasNoValue(this.searchConfig?.pagination?.id)) { + return { + query: this.surroundStringWithQuotes(this.searchConfig?.query), + }; + } + return { + [`${this.searchConfig?.pagination?.id}.query`]: this.surroundStringWithQuotes(this.searchConfig?.query), + }; + } } diff --git a/src/app/shared/search/search-settings/search-settings.component.ts b/src/app/shared/search/search-settings/search-settings.component.ts index 6a29531e87b..3085d5f48c1 100644 --- a/src/app/shared/search/search-settings/search-settings.component.ts +++ b/src/app/shared/search/search-settings/search-settings.component.ts @@ -55,7 +55,7 @@ export class SearchSettingsComponent { */ reloadOrder(event: Event) { const values = (event.target as HTMLInputElement).value.split(','); - this.paginationService.updateRoute(this.searchConfigurationService.paginationID, { + this.paginationService.updateRoute(this.searchConfigurationService.searchInstanceId, { sortField: values[0], sortDirection: values[1] as SortDirection, page: 1, diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts index 28fd6c793dd..ebdf413ef74 100644 --- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts @@ -111,7 +111,7 @@ describe('SearchSwitchConfigurationComponent', () => { spyOn((comp as any).changeConfiguration, 'emit'); comp.selectedOption = configurationList[1]; const navigationExtras: NavigationExtras = { - queryParams: { configuration: MyDSpaceConfigurationValueType.Workflow }, + queryParams: { 'test-id.configuration': MyDSpaceConfigurationValueType.Workflow }, }; fixture.detectChanges(); diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts index 47401220457..385c34cd836 100644 --- a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts @@ -87,7 +87,9 @@ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit { */ onSelect() { const navigationExtras: NavigationExtras = { - queryParams: { configuration: this.selectedOption.value }, + queryParams: { + [this.searchConfigService.getCurrentSearchInstanceParam('configuration')]: this.selectedOption.value, + }, }; this.changeConfiguration.emit(this.selectedOption); diff --git a/src/app/shared/search/search.component.spec.ts b/src/app/shared/search/search.component.spec.ts index 0a8d63f1ab7..62e0daf9ce9 100644 --- a/src/app/shared/search/search.component.spec.ts +++ b/src/app/shared/search/search.component.spec.ts @@ -200,10 +200,11 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar getCurrentScope: of('test-id'), getCurrentSort: of(sortOptionsList[0]), updateFixedFilter: jasmine.createSpy('updateFixedFilter'), - setPaginationId: jasmine.createSpy('setPaginationId'), + setSearchInstanceId: jasmine.createSpy('setSearchInstanceId'), }); + searchConfigurationServiceStub.getCurrentSearchInstanceParam = (param: string) => `${paginationId}.${param}`; - searchConfigurationServiceStub.setPaginationId.and.callFake((pageId) => { + searchConfigurationServiceStub.setSearchInstanceId.and.callFake((pageId) => { paginatedSearchOptions$.next(Object.assign(paginatedSearchOptions$.value, { pagination: Object.assign(new PaginationComponentOptions(), { id: pageId, @@ -251,13 +252,6 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(compType, { - add: { - changeDetection: ChangeDetectionStrategy.Default, - providers: [{ - provide: SearchConfigurationService, - useValue: searchConfigurationServiceStub, - }], - }, remove: { imports: [ PageWithSidebarComponent, @@ -268,7 +262,14 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar SearchLabelsComponent, ], }, - + }).overrideComponent(compType, { + set: { + changeDetection: ChangeDetectionStrategy.Default, + providers: [{ + provide: SearchConfigurationService, + useValue: searchConfigurationServiceStub, + }], + }, }).compileComponents(); } @@ -281,7 +282,7 @@ describe('SearchComponent', () => { fixture = TestBed.createComponent(SearchComponent); comp = fixture.componentInstance; // SearchComponent test instance comp.inPlaceSearch = false; - comp.paginationId = paginationId; + comp.searchInstanceId = paginationId; comp.hiddenQuery = hiddenQuery; spyOn((comp as any), 'getSearchOptions').and.returnValue(paginatedSearchOptions$.asObservable()); diff --git a/src/app/shared/search/search.component.ts b/src/app/shared/search/search.component.ts index 30537134ddf..135a7536669 100644 --- a/src/app/shared/search/search.component.ts +++ b/src/app/shared/search/search.component.ts @@ -152,9 +152,9 @@ export class SearchComponent implements OnDestroy, OnInit { @Input() linkType: CollectionElementLinkType; /** - * The pagination id used in the search + * The search instance id used in the search */ - @Input() paginationId = 'spc'; + @Input() searchInstanceId = 'spc'; /** * Whether or not the search bar should be visible @@ -373,17 +373,17 @@ export class SearchComponent implements OnDestroy, OnInit { } if (this.useUniquePageId) { - // Create an unique pagination id related to the instance of the SearchComponent - this.paginationId = uniqueId(this.paginationId); + // Create a unique search instance id related to this SearchComponent. + this.searchInstanceId = uniqueId(this.searchInstanceId); } - this.searchConfigService.setPaginationId(this.paginationId); + this.searchConfigService.setSearchInstanceId(this.searchInstanceId); if (hasValue(this.configuration)) { - this.routeService.setParameter('configuration', this.configuration); + this.routeService.setParameter(this.searchConfigService.getCurrentSearchInstanceParam('configuration'), this.configuration); } if (hasValue(this.fixedFilterQuery)) { - this.routeService.setParameter('fixedFilterQuery', this.fixedFilterQuery); + this.routeService.setParameter(this.searchConfigService.getCurrentSearchInstanceParam('fixedFilterQuery'), this.fixedFilterQuery); } this.currentScope$ = this.routeService.getQueryParameterValue('scope').pipe( @@ -405,7 +405,7 @@ export class SearchComponent implements OnDestroy, OnInit { const sortOption$: Observable = searchSortOptions$.pipe( switchMap((searchSortOptions: SortOptions[]) => { const defaultSort: SortOptions = searchSortOptions[0]; - return this.searchConfigService.getCurrentSort(this.paginationId, defaultSort); + return this.searchConfigService.getCurrentSort(this.searchInstanceId, defaultSort); }), distinctUntilChanged(), ); @@ -413,8 +413,8 @@ export class SearchComponent implements OnDestroy, OnInit { this.subs.push(combineLatest([configuration$, searchSortOptions$, searchOptions$, sortOption$, this.currentScope$]).pipe( filter(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => { - // filter for search options related to instanced paginated id - return searchOptions.pagination.id === this.paginationId; + // filter for search options related to this search instance id + return searchOptions.pagination.id === this.searchInstanceId; }), debounceTime(100), ).subscribe(([configuration, searchSortOptions, searchOptions, sortOption, scope]: [string, SortOptions[], PaginatedSearchOptions, SortOptions, string]) => { diff --git a/src/app/shared/search/themed-search.component.ts b/src/app/shared/search/themed-search.component.ts index f60f8652f3c..91ba8784690 100644 --- a/src/app/shared/search/themed-search.component.ts +++ b/src/app/shared/search/themed-search.component.ts @@ -34,7 +34,7 @@ export class ThemedSearchComponent extends ThemedComponent { 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', - 'paginationId', + 'searchInstanceId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', @@ -72,7 +72,7 @@ export class ThemedSearchComponent extends ThemedComponent { @Input() linkType: CollectionElementLinkType; - @Input() paginationId: string; + @Input() searchInstanceId: string; @Input() searchEnabled: boolean; diff --git a/src/app/shared/testing/pagination-service.stub.ts b/src/app/shared/testing/pagination-service.stub.ts index 16a2f48ce75..7119fea3e0f 100644 --- a/src/app/shared/testing/pagination-service.stub.ts +++ b/src/app/shared/testing/pagination-service.stub.ts @@ -24,6 +24,6 @@ export class PaginationServiceStub { updateRouteWithUrl = jasmine.createSpy('updateRouteWithUrl'); clearPagination = jasmine.createSpy('clearPagination'); getRouteParameterValue = jasmine.createSpy('getRouteParameterValue').and.returnValue(of('')); - getPageParam = jasmine.createSpy('getPageParam').and.returnValue(`${this.pagination.id}.page`); + getPageParam = jasmine.createSpy('getPageParam').and.callFake((paginationId: string) => `${paginationId}.page`); } diff --git a/src/app/shared/testing/search-configuration-service.stub.ts b/src/app/shared/testing/search-configuration-service.stub.ts index e06f2ab6910..0c481f230e2 100644 --- a/src/app/shared/testing/search-configuration-service.stub.ts +++ b/src/app/shared/testing/search-configuration-service.stub.ts @@ -17,7 +17,7 @@ import { SearchOptions } from '../search/models/search-options.model'; */ export class SearchConfigurationServiceStub { - public paginationID = 'test-id'; + public searchInstanceId = 'test-id'; public searchOptions: BehaviorSubject = new BehaviorSubject(new SearchOptions({})); public paginatedSearchOptions: BehaviorSubject = new BehaviorSubject(new PaginatedSearchOptions({})); @@ -30,6 +30,18 @@ export class SearchConfigurationServiceStub { return of([]); } + getCurrentSearchInstanceParam(param: string) { + return `${this.searchInstanceId}.${param}`; + } + + getCurrentSearchInstanceFilterParam(param: string) { + return `${this.searchInstanceId}.${param}`; + } + + getCurrentPageParam() { + return `${this.searchInstanceId}.page`; + } + getCurrentScope(a) { return of('test-id'); }