Skip to content

Commit e5376c6

Browse files
author
Ali Jaber
committed
141627: Support multiple search instances by prefixing search params with searchInstanceId
Adds support for running multiple search components on a single page by prefixing all search-related route and query parameters with a unique search instance id (${searchInstanceId}.${parameterName}). Previously only sort and pagination parameters were scoped; configuration, fixedFilterQuery, scope, query, dsoType, view and filter queries (f.xxx) are now scoped too.
1 parent bcc437d commit e5376c6

36 files changed

Lines changed: 421 additions & 204 deletions

File tree

src/app/core/shared/search/search-configuration.service.spec.ts

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ describe('SearchConfigurationService', () => {
9090
}));
9191

9292
service = new SearchConfigurationService(routeService, paginationService as any, activatedRoute as any, linkService, halService, requestService, rdb, environment);
93+
service.searchInstanceId = defaults.pagination.id;
9394
});
9495

9596
describe('when the scope is called', () => {
@@ -181,7 +182,7 @@ describe('SearchConfigurationService', () => {
181182

182183
describe('when subscribeToSearchOptions is called', () => {
183184
beforeEach(() => {
184-
(service as any).subscribeToSearchOptions(defaults);
185+
(service as any).subscribeToSearchOptions(defaults.pagination.id, defaults);
185186
});
186187
it('should call all getters it needs, but not call any others', () => {
187188
expect(service.getCurrentPagination).not.toHaveBeenCalled();
@@ -198,14 +199,14 @@ describe('SearchConfigurationService', () => {
198199
beforeEach(() => {
199200
(service as any).subscribeToPaginatedSearchOptions(defaults.pagination.id, defaults);
200201
});
201-
it('should call all getters it needs', () => {
202+
it('should call the pagination-specific getters it needs', () => {
202203
expect(service.getCurrentPagination).toHaveBeenCalled();
203204
expect(service.getCurrentSort).toHaveBeenCalled();
204-
expect(service.getCurrentScope).toHaveBeenCalled();
205-
expect(service.getCurrentConfiguration).toHaveBeenCalled();
206-
expect(service.getCurrentQuery).toHaveBeenCalled();
207-
expect(service.getCurrentDSOType).toHaveBeenCalled();
208-
expect(service.getCurrentFilters).toHaveBeenCalled();
205+
expect(service.getCurrentScope).not.toHaveBeenCalled();
206+
expect(service.getCurrentConfiguration).not.toHaveBeenCalled();
207+
expect(service.getCurrentQuery).not.toHaveBeenCalled();
208+
expect(service.getCurrentDSOType).not.toHaveBeenCalled();
209+
expect(service.getCurrentFilters).not.toHaveBeenCalled();
209210
});
210211
});
211212
});
@@ -314,13 +315,13 @@ describe('SearchConfigurationService', () => {
314315
it('should return all params except the applied filter', () => {
315316
service.unselectAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator);
316317

317-
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority');
318+
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.author`, '1282121b-5394-4689-ab93-78d537764052,authority');
318319
});
319320

320321
it('should be able to remove AppliedFilter without operator', () => {
321322
service.unselectAppliedFilterParams('dateIssued.max', '2000');
322323

323-
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith('f.dateIssued.max', '2000');
324+
expect(routeService.getParamsExceptValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.dateIssued.max`, '2000');
324325
});
325326

326327
it('should reset the page to 1', (done: DoneFn) => {
@@ -346,13 +347,13 @@ describe('SearchConfigurationService', () => {
346347
it('should return all params with the applied filter', () => {
347348
service.selectNewAppliedFilterParams(appliedFilter.filter, appliedFilter.value, appliedFilter.operator);
348349

349-
expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.author', '1282121b-5394-4689-ab93-78d537764052,authority');
350+
expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.author`, '1282121b-5394-4689-ab93-78d537764052,authority');
350351
});
351352

352353
it('should be able to add AppliedFilter without operator', () => {
353354
service.selectNewAppliedFilterParams('dateIssued.max', '2000');
354355

355-
expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith('f.dateIssued.max', '2000');
356+
expect(routeService.getParamsWithAdditionalValue).toHaveBeenCalledWith(`${defaults.pagination.id}.f.dateIssued.max`, '2000');
356357
});
357358

358359
it('should reset the page to 1', (done: DoneFn) => {

src/app/core/shared/search/search-configuration.service.ts

Lines changed: 144 additions & 83 deletions
Large diffs are not rendered by default.

src/app/core/shared/search/search-filter.service.ts

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Injectable } from '@angular/core';
2-
import { Params } from '@angular/router';
32
import {
43
createSelector,
54
MemoizedSelector,
@@ -75,7 +74,14 @@ export class SearchFilterService {
7574
* @param {string} filterValue The value for which to search
7675
* @returns {Observable<boolean>} Emit true when the filter is active with the given value
7776
*/
78-
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
77+
isFilterActiveWithValue(paramName: string, filterValue: string, searchInstanceId?: string): Observable<boolean> {
78+
if (hasValue(searchInstanceId)) {
79+
return observableCombineLatest(
80+
this.routeService.hasQueryParamWithValue(this.getSearchInstanceParam(searchInstanceId, paramName), filterValue),
81+
this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')),
82+
this.routeService.hasQueryParamWithValue(paramName, filterValue),
83+
).pipe(map(([instanceActive, instanceFilters, legacyActive]) => instanceActive || (!isNotEmpty(instanceFilters) && legacyActive)));
84+
}
7985
return this.routeService.hasQueryParamWithValue(paramName, filterValue);
8086
}
8187

@@ -84,24 +90,31 @@ export class SearchFilterService {
8490
* @param {string} paramName The parameter name of the filter's configuration for which to search
8591
* @returns {Observable<boolean>} Emit true when the filter is active with any value
8692
*/
87-
isFilterActive(paramName: string): Observable<boolean> {
93+
isFilterActive(paramName: string, searchInstanceId?: string): Observable<boolean> {
94+
if (hasValue(searchInstanceId)) {
95+
return observableCombineLatest(
96+
this.routeService.hasQueryParam(this.getSearchInstanceParam(searchInstanceId, paramName)),
97+
this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')),
98+
this.routeService.hasQueryParam(paramName),
99+
).pipe(map(([instanceActive, instanceFilters, legacyActive]) => instanceActive || (!isNotEmpty(instanceFilters) && legacyActive)));
100+
}
88101
return this.routeService.hasQueryParam(paramName);
89102
}
90103

91104
/**
92105
* Fetch the current active scope from the query parameters
93106
* @returns {Observable<string>}
94107
*/
95-
getCurrentScope(): Observable<string> {
96-
return this.routeService.getQueryParameterValue('scope');
108+
getCurrentScope(searchInstanceId?: string) {
109+
return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'scope');
97110
}
98111

99112
/**
100113
* Fetch the current query from the query parameters
101114
* @returns {Observable<string>}
102115
*/
103-
getCurrentQuery(): Observable<string> {
104-
return this.routeService.getQueryParameterValue('query');
116+
getCurrentQuery(searchInstanceId?: string) {
117+
return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'query');
105118
}
106119

107120
/**
@@ -142,16 +155,22 @@ export class SearchFilterService {
142155
* Fetch the current active filters from the query parameters
143156
* @returns {Observable<Params>}
144157
*/
145-
getCurrentFilters(): Observable<Params> {
158+
getCurrentFilters(searchInstanceId?: string) {
159+
if (hasValue(searchInstanceId)) {
160+
return observableCombineLatest(
161+
this.routeService.getQueryParamsWithPrefix(this.getSearchInstanceParam(searchInstanceId, 'f.')),
162+
this.routeService.getQueryParamsWithPrefix('f.'),
163+
).pipe(map(([instanceFilters, legacyFilters]) => isNotEmpty(instanceFilters) ? instanceFilters : legacyFilters));
164+
}
146165
return this.routeService.getQueryParamsWithPrefix('f.');
147166
}
148167

149168
/**
150169
* Fetch the current view from the query parameters
151170
* @returns {Observable<string>}
152171
*/
153-
getCurrentView(): Observable<string> {
154-
return this.routeService.getQueryParameterValue('view');
172+
getCurrentView(searchInstanceId?: string) {
173+
return this.getCurrentSearchInstanceQueryParam(searchInstanceId, 'view');
155174
}
156175

157176
/**
@@ -190,6 +209,20 @@ export class SearchFilterService {
190209
return `${new EmphasizePipe().transform(facet.value, query)} (${facet.count})`;
191210
}
192211

212+
private getSearchInstanceParam(searchInstanceId: string, parameterName: string): string {
213+
return `${searchInstanceId}.${parameterName}`;
214+
}
215+
216+
private getCurrentSearchInstanceQueryParam(searchInstanceId: string | undefined, parameterName: string): Observable<string> {
217+
if (hasValue(searchInstanceId)) {
218+
return observableCombineLatest(
219+
this.routeService.getQueryParameterValue(this.getSearchInstanceParam(searchInstanceId, parameterName)),
220+
this.routeService.getQueryParameterValue(parameterName),
221+
).pipe(map(([instanceValue, legacyValue]) => hasValue(instanceValue) ? instanceValue : legacyValue));
222+
}
223+
return this.routeService.getQueryParameterValue(parameterName);
224+
}
225+
193226
/**
194227
* Checks if the state of a given filter is currently collapsed or not
195228
* @param {string} filterName The filtername for which the collapsed state is checked

src/app/core/shared/search/search.service.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ describe('SearchService', () => {
9898
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
9999
service.setViewMode(ViewMode.ListElement);
100100

101-
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement });
101+
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { 'test-id.view': ViewMode.ListElement });
102102
});
103103

104104
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
105105
service.setViewMode(ViewMode.GridElement);
106106

107-
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement });
107+
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { 'test-id.view': ViewMode.GridElement });
108108
});
109109
});
110110

src/app/core/shared/search/search.service.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -342,8 +342,10 @@ export class SearchService {
342342
*/
343343
getViewMode(): Observable<ViewMode> {
344344
return this.routeService.getQueryParamMap().pipe(map((params) => {
345-
if (isNotEmpty(params.get('view')) && hasValue(params.get('view'))) {
346-
return params.get('view');
345+
const viewParam = this.searchConfigurationService.getCurrentSearchInstanceParam('view');
346+
const view = hasValue(params.get(viewParam)) ? params.get(viewParam) : params.get('view');
347+
if (isNotEmpty(view) && hasValue(view)) {
348+
return view;
347349
} else {
348350
return ViewMode.ListElement;
349351
}
@@ -356,16 +358,16 @@ export class SearchService {
356358
* @param {string[]} searchLinkParts
357359
*/
358360
setViewMode(viewMode: ViewMode, searchLinkParts?: string[]) {
359-
this.paginationService.getCurrentPagination(this.searchConfigurationService.paginationID, new PaginationComponentOptions()).pipe(take(1))
361+
this.paginationService.getCurrentPagination(this.searchConfigurationService.searchInstanceId, new PaginationComponentOptions()).pipe(take(1))
360362
.subscribe((config) => {
361363
let pageParams = { page: 1 };
362-
const queryParams = { view: viewMode };
364+
const queryParams = { [this.searchConfigurationService.getCurrentSearchInstanceParam('view')]: viewMode };
363365
if (viewMode === ViewMode.DetailedListElement) {
364366
pageParams = Object.assign(pageParams, { pageSize: 1 });
365367
} else if (config.pageSize === 1) {
366368
pageParams = Object.assign(pageParams, { pageSize: 10 });
367369
}
368-
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams);
370+
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.searchInstanceId, hasValue(searchLinkParts) ? searchLinkParts : [this.getSearchLink()], pageParams, queryParams);
369371
});
370372
}
371373

src/app/home-page/recent-item-list/recent-item-list.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ export class RecentItemListComponent implements OnInit, OnDestroy {
128128
}
129129

130130
onLoadMore(): void {
131-
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.paginationID, ['search'], {
131+
this.paginationService.updateRouteWithUrl(this.searchConfigurationService.searchInstanceId, ['search'], {
132132
sortField: environment.homePage.recentSubmissions.sortField,
133133
sortDirection: 'DESC' as SortDirection,
134134
page: 1,

src/app/my-dspace-page/my-dspace-configuration.service.spec.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ describe('MyDSpaceConfigurationService', () => {
151151

152152
describe('when subscribeToSearchOptions is called', () => {
153153
beforeEach(() => {
154-
(service as any).subscribeToSearchOptions(defaults);
154+
(service as any).subscribeToSearchOptions(defaults.pagination.id, defaults);
155155
});
156156
it('should call all getters it needs, but not call any others', () => {
157157
expect(service.getCurrentPagination).not.toHaveBeenCalled();
@@ -168,14 +168,14 @@ describe('MyDSpaceConfigurationService', () => {
168168
beforeEach(() => {
169169
(service as any).subscribeToPaginatedSearchOptions('id', defaults);
170170
});
171-
it('should call all getters it needs', () => {
171+
it('should call the pagination-specific getters it needs', () => {
172172
expect(service.getCurrentPagination).toHaveBeenCalled();
173173
expect(service.getCurrentSort).toHaveBeenCalled();
174-
expect(service.getCurrentScope).toHaveBeenCalled();
175-
expect(service.getCurrentConfiguration).toHaveBeenCalled();
176-
expect(service.getCurrentQuery).toHaveBeenCalled();
177-
expect(service.getCurrentDSOType).toHaveBeenCalled();
178-
expect(service.getCurrentFilters).toHaveBeenCalled();
174+
expect(service.getCurrentScope).not.toHaveBeenCalled();
175+
expect(service.getCurrentConfiguration).not.toHaveBeenCalled();
176+
expect(service.getCurrentQuery).not.toHaveBeenCalled();
177+
expect(service.getCurrentDSOType).not.toHaveBeenCalled();
178+
expect(service.getCurrentFilters).not.toHaveBeenCalled();
179179
});
180180
});
181181
});

src/app/my-dspace-page/my-dspace-configuration.service.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> =
4747
*/
4848
@Injectable({ providedIn: 'root' })
4949
export class MyDSpaceConfigurationService extends SearchConfigurationService {
50+
/**
51+
* Search instance id used for the MyDSpace search component.
52+
*/
53+
public searchInstanceId = 'mydspace-page';
54+
5055
/**
5156
* Default pagination settings
5257
*/

src/app/my-dspace-page/my-dspace.guard.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,34 @@ export const myDSpaceGuard: CanActivateFn = (
2626
configurationService: MyDSpaceConfigurationService = inject(MyDSpaceConfigurationService),
2727
router: Router = inject(Router),
2828
): Observable<boolean> => {
29+
const configurationParam = configurationService.getCurrentSearchInstanceParam('configuration');
30+
const configuration = route.queryParamMap.get(configurationParam) || route.queryParamMap.get('configuration');
2931
return configurationService.getAvailableConfigurationTypes().pipe(
3032
first(),
31-
map((configurationList) => validateConfigurationParam(router, route.queryParamMap.get('configuration'), configurationList)));
33+
map((configurationList) => validateConfigurationParam(router, configurationService, configuration, configurationList)));
3234
};
3335

3436
/**
3537
* Check if the given configuration is present in the list of those available
3638
*
3739
* @param router
3840
* the service router
41+
* @param configurationService
42+
* the MyDSpace configuration service
3943
* @param configuration
4044
* the configuration to validate
4145
* @param configurationList
4246
* the list of available configuration
4347
*
4448
*/
45-
function validateConfigurationParam(router: Router, configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
49+
function validateConfigurationParam(router: Router, configurationService: MyDSpaceConfigurationService, configuration: string, configurationList: MyDSpaceConfigurationValueType[]): boolean {
4650
const configurationDefault: string = configurationList[0];
4751
if (isEmpty(configuration) || !configurationList.includes(configuration as MyDSpaceConfigurationValueType)) {
4852
// If configuration param is empty or is not included in available configurations redirect to a default configuration value
4953
const navigationExtras: NavigationExtras = {
50-
queryParams: { configuration: configurationDefault },
54+
queryParams: {
55+
[configurationService.getCurrentSearchInstanceParam('configuration')]: configurationDefault,
56+
},
5157
};
5258

5359
router.navigate([MYDSPACE_ROUTE], navigationExtras);

src/app/search-navbar/search-navbar.component.spec.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ import {
2121
TranslateModule,
2222
} from '@ngx-translate/core';
2323

24+
import { PaginationService } from '../core/pagination/pagination.service';
2425
import { SearchService } from '../core/shared/search/search.service';
26+
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
2527
import { TranslateLoaderMock } from '../shared/mocks/translate-loader.mock';
2628
import { SearchNavbarComponent } from './search-navbar.component';
2729

@@ -54,6 +56,14 @@ describe('SearchNavbarComponent', () => {
5456
],
5557
providers: [
5658
{ provide: SearchService, useValue: mockSearchService },
59+
{ provide: PaginationService, useValue: { getPageParam: (id: string) => `${id}.page` } },
60+
{
61+
provide: SearchConfigurationService,
62+
useValue: {
63+
searchInstanceId: 'spc',
64+
getCurrentSearchInstanceParam: (param: string) => `spc.${param}`,
65+
},
66+
},
5767
],
5868
})
5969
.compileComponents();
@@ -100,7 +110,7 @@ describe('SearchNavbarComponent', () => {
100110
fixture.detectChanges();
101111
}));
102112
it('to search page with empty query', () => {
103-
const extras: NavigationExtras = { queryParams: { query: '' } };
113+
const extras: NavigationExtras = { queryParams: { 'spc.query': '', 'spc.page': 1 } };
104114
expect(component.onSubmit).toHaveBeenCalledWith({ query: '' });
105115
expect(router.navigate).toHaveBeenCalledWith(['search'], extras);
106116
});
@@ -125,7 +135,7 @@ describe('SearchNavbarComponent', () => {
125135
fixture.detectChanges();
126136
}));
127137
it('to search page with query', async () => {
128-
const extras: NavigationExtras = { queryParams: { query: 'test' } };
138+
const extras: NavigationExtras = { queryParams: { 'spc.query': 'test', 'spc.page': 1 } };
129139
expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' });
130140

131141
expect(router.navigate).toHaveBeenCalledWith(['search'], extras);

0 commit comments

Comments
 (0)