Skip to content

Commit fcc277b

Browse files
author
Ali Jaber
committed
Merge branch 'backport-5739-w2p-141627_search-instance-params-9.0' into w2p-141627_search-instance-params-10.x_contribute
2 parents 61a2e46 + 280bfec commit fcc277b

4 files changed

Lines changed: 153 additions & 1 deletion

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,29 @@ describe('SearchConfigurationService', () => {
310310
});
311311
});
312312

313+
describe('isLegacySearchParam', () => {
314+
it('should return true for unprefixed scoped search params', () => {
315+
['configuration', 'scope', 'query', 'dsoType', 'view'].forEach((param: string) => {
316+
expect(service.isLegacySearchParam(param)).toBeTrue();
317+
});
318+
});
319+
320+
it('should return true for unprefixed filter params', () => {
321+
expect(service.isLegacySearchParam('f.author')).toBeTrue();
322+
expect(service.isLegacySearchParam('f.dateIssued.max')).toBeTrue();
323+
});
324+
325+
it('should return false for params that are already prefixed with a search instance id', () => {
326+
expect(service.isLegacySearchParam(`${defaults.pagination.id}.query`)).toBeFalse();
327+
expect(service.isLegacySearchParam(`${defaults.pagination.id}.f.author`)).toBeFalse();
328+
});
329+
330+
it('should return false for unrelated params', () => {
331+
expect(service.isLegacySearchParam('page')).toBeFalse();
332+
expect(service.isLegacySearchParam('spc.page')).toBeFalse();
333+
});
334+
});
335+
313336
describe('unselectAppliedFilterParams', () => {
314337
let appliedFilter: AppliedFilter;
315338

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@ export class SearchConfigurationService implements OnDestroy {
8989
*/
9090
public searchInstanceId = 'spc';
9191

92+
/**
93+
* The names of the query parameters that are scoped to a search instance, but that still accept a
94+
* legacy (unprefixed) value for backwards compatibility. Filter parameters (prefixed with `f.`) are
95+
* handled separately, see {@link isLegacySearchParam}.
96+
*/
97+
protected readonly legacyScopedQueryParams: string[] = ['configuration', 'scope', 'query', 'dsoType', 'view'];
98+
9299
/**
93100
* Emits the current search options
94101
*/
@@ -348,6 +355,18 @@ export class SearchConfigurationService implements OnDestroy {
348355
return this.getSearchInstanceParam(searchInstanceId, 'f.');
349356
}
350357

358+
/**
359+
* Whether the given query parameter is a legacy (unprefixed) search parameter that has a
360+
* search-instance-prefixed equivalent (e.g. `query`, `scope` or a `f.xxx` filter). These
361+
* parameters are still read for backwards compatibility, but should be migrated to their
362+
* prefixed form so they don't get mixed with newly added prefixed parameters.
363+
*
364+
* @param paramName The query parameter name
365+
*/
366+
isLegacySearchParam(paramName: string): boolean {
367+
return this.legacyScopedQueryParams.includes(paramName) || paramName.startsWith('f.');
368+
}
369+
351370
getCurrentPageParam(): string {
352371
return this.paginationService.getPageParam(this.searchInstanceId);
353372
}

src/app/shared/search/search.component.spec.ts

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
} from '@angular/core/testing';
1313
import { By } from '@angular/platform-browser';
1414
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
15-
import { ActivatedRoute } from '@angular/router';
15+
import {
16+
ActivatedRoute,
17+
convertToParamMap,
18+
} from '@angular/router';
1619
import { RouterTestingModule } from '@angular/router/testing';
1720
import { APP_CONFIG } from '@dspace/config/app-config.interface';
1821
import { SearchManager } from '@dspace/core/browse/search-manager';
@@ -188,6 +191,9 @@ const routeServiceStub = {
188191
getQueryParamsWithPrefix: () => {
189192
return of(null);
190193
},
194+
getQueryParamMap: () => {
195+
return of(convertToParamMap({}));
196+
},
191197
setParameter: (key: any, value: any) => {
192198
return;
193199
},
@@ -207,6 +213,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
207213
setSearchInstanceId: jasmine.createSpy('setSearchInstanceId'),
208214
});
209215
searchConfigurationServiceStub.getCurrentSearchInstanceParam = (param: string) => `${paginationId}.${param}`;
216+
searchConfigurationServiceStub.isLegacySearchParam = (param: string) => ['configuration', 'scope', 'query', 'dsoType', 'view'].includes(param) || param.startsWith('f.');
210217

211218
searchConfigurationServiceStub.setSearchInstanceId.and.callFake((pageId) => {
212219
paginatedSearchOptions$.next(Object.assign(paginatedSearchOptions$.value, {
@@ -348,6 +355,63 @@ describe('SearchComponent', () => {
348355
expect(comp.resultFound.emit).toHaveBeenCalledWith(expectedResults);
349356
}));
350357

358+
describe('migrateLegacySearchParams', () => {
359+
let navigateSpy: jasmine.Spy;
360+
361+
beforeEach(() => {
362+
comp.platformId = 'browser';
363+
comp.searchInstanceId = paginationId;
364+
navigateSpy = spyOn((comp as any).router, 'navigate');
365+
});
366+
367+
it('should replace legacy (unprefixed) search params with their prefixed equivalent', () => {
368+
spyOn((comp as any).routeService, 'getQueryParamMap').and.returnValue(of(convertToParamMap({
369+
query: 'cats',
370+
'f.author': 'Smith',
371+
})));
372+
373+
(comp as any).migrateLegacySearchParams();
374+
375+
expect(navigateSpy).toHaveBeenCalledWith([], {
376+
queryParams: {
377+
query: null,
378+
[`${paginationId}.query`]: 'cats',
379+
'f.author': null,
380+
[`${paginationId}.f.author`]: 'Smith',
381+
},
382+
queryParamsHandling: 'merge',
383+
replaceUrl: true,
384+
});
385+
});
386+
387+
it('should not navigate when there are no legacy search params', () => {
388+
spyOn((comp as any).routeService, 'getQueryParamMap').and.returnValue(of(convertToParamMap({
389+
[`${paginationId}.query`]: 'cats',
390+
})));
391+
392+
(comp as any).migrateLegacySearchParams();
393+
394+
expect(navigateSpy).not.toHaveBeenCalled();
395+
});
396+
397+
it('should drop the legacy param without overriding an existing prefixed param', () => {
398+
spyOn((comp as any).routeService, 'getQueryParamMap').and.returnValue(of(convertToParamMap({
399+
query: 'legacy',
400+
[`${paginationId}.query`]: 'prefixed',
401+
})));
402+
403+
(comp as any).migrateLegacySearchParams();
404+
405+
expect(navigateSpy).toHaveBeenCalledWith([], {
406+
queryParams: {
407+
query: null,
408+
},
409+
queryParamsHandling: 'merge',
410+
replaceUrl: true,
411+
});
412+
});
413+
});
414+
351415
describe('when the open sidebar button is clicked in mobile view', () => {
352416

353417
beforeEach(() => {

src/app/shared/search/search.component.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
AsyncPipe,
3+
isPlatformBrowser,
34
isPlatformServer,
45
NgTemplateOutlet,
56
} from '@angular/common';
@@ -16,6 +17,7 @@ import {
1617
} from '@angular/core';
1718
import {
1819
NavigationStart,
20+
Params,
1921
Router,
2022
} from '@angular/router';
2123
import {
@@ -67,6 +69,7 @@ import {
6769
filter,
6870
map,
6971
switchMap,
72+
take,
7073
} from 'rxjs/operators';
7174

7275
import { environment } from '../../../environments/environment';
@@ -383,6 +386,10 @@ export class SearchComponent implements OnDestroy, OnInit {
383386

384387
this.searchConfigService.setSearchInstanceId(this.searchInstanceId);
385388

389+
if (isPlatformBrowser(this.platformId)) {
390+
this.migrateLegacySearchParams();
391+
}
392+
386393
if (hasValue(this.configuration)) {
387394
this.routeService.setParameter(this.searchConfigService.getCurrentSearchInstanceParam('configuration'), this.configuration);
388395
}
@@ -589,6 +596,45 @@ export class SearchComponent implements OnDestroy, OnInit {
589596
}
590597
}
591598

599+
/**
600+
* Detect legacy (unprefixed) search parameters in the current URL and replace them with their
601+
* search-instance-prefixed equivalents (e.g. `query` becomes `spc.query`, `f.author` becomes
602+
* `spc.f.author`). This keeps backwards compatibility with old search URLs while making sure that
603+
* subsequent search interactions (entering a new query, adding a filter, ...) operate on the
604+
* prefixed parameters only, instead of mixing or dropping the legacy ones.
605+
* @private
606+
*/
607+
private migrateLegacySearchParams(): void {
608+
this.subs.push(this.routeService.getQueryParamMap().pipe(take(1)).subscribe((queryParamMap) => {
609+
const prefix = `${this.searchInstanceId}.`;
610+
const updatedParams: Params = {};
611+
let hasLegacyParams = false;
612+
613+
queryParamMap.keys
614+
.filter((key: string) => this.searchConfigService.isLegacySearchParam(key))
615+
.forEach((key: string) => {
616+
hasLegacyParams = true;
617+
// Remove the legacy parameter from the URL
618+
updatedParams[key] = null;
619+
// Only move its value(s) to the prefixed parameter when no prefixed value is set yet,
620+
// so an existing prefixed parameter always takes precedence over the legacy one.
621+
const prefixedKey = `${prefix}${key}`;
622+
if (!queryParamMap.has(prefixedKey)) {
623+
const values: string[] = queryParamMap.getAll(key);
624+
updatedParams[prefixedKey] = values.length > 1 ? values : values[0];
625+
}
626+
});
627+
628+
if (hasLegacyParams) {
629+
void this.router.navigate([], {
630+
queryParams: updatedParams,
631+
queryParamsHandling: 'merge',
632+
replaceUrl: true,
633+
});
634+
}
635+
}));
636+
}
637+
592638
/**
593639
* Get the UUID from a DSO url
594640
* Return null if the url isn't an object page (allowedObjectPaths) or the UUID couldn't be found

0 commit comments

Comments
 (0)