Skip to content

Commit 280bfec

Browse files
author
Ali Jaber
committed
141627: Migrate legacy unprefixed search params to searchInstanceId-prefixed params on load
1 parent e5376c6 commit 280bfec

4 files changed

Lines changed: 153 additions & 1 deletion

File tree

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,29 @@ describe('SearchConfigurationService', () => {
300300
});
301301
});
302302

303+
describe('isLegacySearchParam', () => {
304+
it('should return true for unprefixed scoped search params', () => {
305+
['configuration', 'scope', 'query', 'dsoType', 'view'].forEach((param: string) => {
306+
expect(service.isLegacySearchParam(param)).toBeTrue();
307+
});
308+
});
309+
310+
it('should return true for unprefixed filter params', () => {
311+
expect(service.isLegacySearchParam('f.author')).toBeTrue();
312+
expect(service.isLegacySearchParam('f.dateIssued.max')).toBeTrue();
313+
});
314+
315+
it('should return false for params that are already prefixed with a search instance id', () => {
316+
expect(service.isLegacySearchParam(`${defaults.pagination.id}.query`)).toBeFalse();
317+
expect(service.isLegacySearchParam(`${defaults.pagination.id}.f.author`)).toBeFalse();
318+
});
319+
320+
it('should return false for unrelated params', () => {
321+
expect(service.isLegacySearchParam('page')).toBeFalse();
322+
expect(service.isLegacySearchParam('spc.page')).toBeFalse();
323+
});
324+
});
325+
303326
describe('unselectAppliedFilterParams', () => {
304327
let appliedFilter: AppliedFilter;
305328

src/app/core/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 { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
1821
import { Store } from '@ngrx/store';
@@ -184,6 +187,9 @@ const routeServiceStub = {
184187
getQueryParamsWithPrefix: () => {
185188
return of(null);
186189
},
190+
getQueryParamMap: () => {
191+
return of(convertToParamMap({}));
192+
},
187193
setParameter: (key: any, value: any) => {
188194
return;
189195
},
@@ -203,6 +209,7 @@ export function configureSearchComponentTestingModule(compType, additionalDeclar
203209
setSearchInstanceId: jasmine.createSpy('setSearchInstanceId'),
204210
});
205211
searchConfigurationServiceStub.getCurrentSearchInstanceParam = (param: string) => `${paginationId}.${param}`;
212+
searchConfigurationServiceStub.isLegacySearchParam = (param: string) => ['configuration', 'scope', 'query', 'dsoType', 'view'].includes(param) || param.startsWith('f.');
206213

207214
searchConfigurationServiceStub.setSearchInstanceId.and.callFake((pageId) => {
208215
paginatedSearchOptions$.next(Object.assign(paginatedSearchOptions$.value, {
@@ -343,6 +350,63 @@ describe('SearchComponent', () => {
343350
expect(comp.resultFound.emit).toHaveBeenCalledWith(expectedResults);
344351
}));
345352

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

348412
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 { TranslateModule } from '@ngx-translate/core';
@@ -32,6 +34,7 @@ import {
3234
filter,
3335
map,
3436
switchMap,
37+
take,
3538
} from 'rxjs/operators';
3639

3740
import {
@@ -380,6 +383,10 @@ export class SearchComponent implements OnDestroy, OnInit {
380383

381384
this.searchConfigService.setSearchInstanceId(this.searchInstanceId);
382385

386+
if (isPlatformBrowser(this.platformId)) {
387+
this.migrateLegacySearchParams();
388+
}
389+
383390
if (hasValue(this.configuration)) {
384391
this.routeService.setParameter(this.searchConfigService.getCurrentSearchInstanceParam('configuration'), this.configuration);
385392
}
@@ -586,6 +593,45 @@ export class SearchComponent implements OnDestroy, OnInit {
586593
}
587594
}
588595

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

0 commit comments

Comments
 (0)