Skip to content

Commit 7aca074

Browse files
[Port to dtq-dev] Improve back navigation logic in ItemComponent (#1184)
* Improve back navigation logic in ItemComponent (ufal#80) * Improve back navigation logic in ItemComponent Enhances the navigation logic to use window.history.back() when the previous URL does not match the expected route pattern. Also updates the back button visibility filter to allow empty URLs. * Fix back button visibility logic in ItemComponent Updated the showBackButton observable to correctly determine when the back button should be shown based on the previous route. Also added ngOnInit call in the related test to ensure proper initialization. * Remove unused 'filter' import from item.component.ts The 'filter' operator from 'rxjs/operators' was imported but not used in item.component.ts. This commit cleans up the import statements. * Add session storage for previous URL in item page Introduces generic methods in RouteService to store and retrieve URLs in session storage. ItemComponent now uses session storage to persist and retrieve the previous URL for improved back navigation, falling back to browser history if no valid URL is found. * Add session URL helpers to RouteService stubs in tests Extended RouteService stubs in test files to include storeUrlInSession and getUrlFromSession methods, matching the interface used by ItemComponent and related tests. This ensures test mocks are up-to-date with the service's API. * Simplify back navigation logic in ItemComponent Removed conditional check for previous URL and always navigate using router.navigateByUrl with storedPreviousUrl. (cherry picked from commit 5dfb106) * Treat /home as previous route and add tests Include "/home" in ItemComponent's previousRoute regex so the back button is shown for home. Add unit tests to verify the back button appears for a home previous URL and that a home previous URL is prioritized over a session-stored URL (ensuring the session is updated and navigation uses the home URL). (cherry picked from commit 3eb32e5) --------- Co-authored-by: Amad Ul Hassan <hassan@ufal.mff.cuni.cz>
1 parent e7ab712 commit 7aca074

6 files changed

Lines changed: 135 additions & 12 deletions

File tree

src/app/core/services/route.service.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,34 @@ export class RouteService {
160160
});
161161
}
162162

163+
/**
164+
* Store a URL in session storage for later retrieval
165+
* Generic method that can be used by any component
166+
* @param key The session storage key
167+
* @param url The URL to store
168+
*/
169+
public storeUrlInSession(key: string, url: string): void {
170+
if (typeof window !== 'undefined' && hasValue(window.sessionStorage)) {
171+
// Only write if the value is different to avoid unnecessary writes
172+
const currentValue = window.sessionStorage.getItem(key);
173+
if (currentValue !== url) {
174+
window.sessionStorage.setItem(key, url);
175+
}
176+
}
177+
}
178+
179+
/**
180+
* Retrieve a URL from session storage
181+
* Generic method that can be used by any component
182+
* @param key The session storage key
183+
*/
184+
public getUrlFromSession(key: string): string | null {
185+
if (typeof window !== 'undefined' && hasValue(window.sessionStorage)) {
186+
return window.sessionStorage.getItem(key);
187+
}
188+
return null;
189+
}
190+
163191
private getRouteParams(): Observable<Params> {
164192
let active = this.route;
165193
while (active.firstChild) {

src/app/item-page/simple/item-types/publication/publication.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ describe('PublicationComponent', () => {
156156
const localMockRouteService = {
157157
getPreviousUrl(): Observable<string> {
158158
return of('/search?query=test%20query&fakeParam=true');
159+
},
160+
storeUrlInSession(key: string, url: string): void {
161+
// no-op
162+
},
163+
getUrlFromSession(key: string): string | null {
164+
return null;
159165
}
160166
};
161167
beforeEach(waitForAsync(() => {
@@ -186,6 +192,12 @@ describe('PublicationComponent', () => {
186192
const localMockRouteService = {
187193
getPreviousUrl(): Observable<string> {
188194
return of('/item');
195+
},
196+
storeUrlInSession(key: string, url: string): void {
197+
// no-op
198+
},
199+
getUrlFromSession(key: string): string | null {
200+
return null;
189201
}
190202
};
191203
beforeEach(waitForAsync(() => {

src/app/item-page/simple/item-types/shared/item.component.spec.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,12 @@ export function getIIIFEnabled(enabled: boolean): MetadataValue {
7676
export const mockRouteService = {
7777
getPreviousUrl(): Observable<string> {
7878
return observableOf('');
79+
},
80+
storeUrlInSession(key: string, url: string): void {
81+
// no-op
82+
},
83+
getUrlFromSession(key: string): string | null {
84+
return null;
7985
}
8086
};
8187

@@ -425,6 +431,7 @@ describe('ItemComponent', () => {
425431

426432
const searchUrl = '/search?query=test&spc.page=2';
427433
const browseUrl = '/browse/title?scope=0cc&bbm.page=3';
434+
const homeUrl = '/home';
428435
const recentSubmissionsUrl = '/collections/be7b8430-77a5-4016-91c9-90863e50583a?cp.page=3';
429436

430437
beforeEach(waitForAsync(() => {
@@ -485,6 +492,7 @@ describe('ItemComponent', () => {
485492

486493
it('should hide back button',() => {
487494
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf('/item'));
495+
comp.ngOnInit();
488496
comp.showBackButton.subscribe((val) => {
489497
expect(val).toBeFalse();
490498
});
@@ -510,6 +518,32 @@ describe('ItemComponent', () => {
510518
expect(val).toBeTrue();
511519
});
512520
});
521+
522+
it('should show back button for home', () => {
523+
spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(homeUrl));
524+
comp.ngOnInit();
525+
comp.showBackButton.subscribe((val) => {
526+
expect(val).toBeTrue();
527+
});
528+
});
529+
530+
it('should prioritize home previous url over session fallback', () => {
531+
const staleSessionUrl = searchUrl;
532+
const getPreviousUrlSpy = spyOn(mockRouteService, 'getPreviousUrl').and.returnValue(observableOf(homeUrl));
533+
const getUrlFromSessionSpy = spyOn(mockRouteService, 'getUrlFromSession').and.returnValue(staleSessionUrl);
534+
const storeUrlInSessionSpy = spyOn(mockRouteService, 'storeUrlInSession');
535+
536+
comp.ngOnInit();
537+
comp.showBackButton.subscribe((val) => {
538+
expect(val).toBeTrue();
539+
expect(getPreviousUrlSpy).toHaveBeenCalled();
540+
expect(getUrlFromSessionSpy).not.toHaveBeenCalled();
541+
expect(storeUrlInSessionSpy).toHaveBeenCalledWith('item-previous-url', homeUrl);
542+
543+
comp.back();
544+
expect(router.navigateByUrl).toHaveBeenCalledWith(homeUrl);
545+
});
546+
});
513547
});
514548

515549
});

src/app/item-page/simple/item-types/shared/item.component.ts

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { getItemPageRoute } from '../../../item-page-routing-paths';
55
import { RouteService } from '../../../../core/services/route.service';
66
import { Observable } from 'rxjs';
77
import { getDSpaceQuery, isIiifEnabled, isIiifSearchEnabled } from './item-iiif-utils';
8-
import { filter, map, take } from 'rxjs/operators';
8+
import { map, take } from 'rxjs/operators';
99
import { Router } from '@angular/router';
1010
import { select, Store } from '@ngrx/store';
1111
import { AppState } from 'src/app/app.reducer';
@@ -22,11 +22,16 @@ import { APP_CONFIG, AppConfig } from 'src/config/app-config.interface';
2222
export class ItemComponent implements OnInit {
2323
@Input() object: Item;
2424

25+
/**
26+
* Session storage key for storing the previous URL before entering item page
27+
*/
28+
private readonly ITEM_PREVIOUS_URL_SESSION_KEY = 'item-previous-url';
29+
2530
/**
2631
* This regex matches previous routes. The button is shown
2732
* for matching paths and hidden in other cases.
2833
*/
29-
previousRoute = /^(\/search|\/browse|\/collections|\/admin\/search|\/mydspace)/;
34+
previousRoute = /^(\/home|\/search|\/browse|\/collections|\/admin\/search|\/mydspace)/;
3035

3136
/**
3237
* Used to show or hide the back to results button in the view.
@@ -57,6 +62,11 @@ export class ItemComponent implements OnInit {
5762

5863
isAuthenticated$: Observable<boolean>;
5964

65+
/**
66+
* Stores the previous URL retrieved either from RouteService or sessionStorage
67+
*/
68+
private storedPreviousUrl: string;
69+
6070
constructor(protected routeService: RouteService,
6171
protected router: Router,
6272
private store: Store<AppState>,
@@ -66,26 +76,36 @@ export class ItemComponent implements OnInit {
6676

6777
/**
6878
* The function used to return to list from the item.
79+
* Uses stored previous URL if available, otherwise falls back to browser history.
6980
*/
7081
back = () => {
71-
this.routeService.getPreviousUrl().pipe(
72-
take(1)
73-
).subscribe(
74-
(url => {
75-
this.router.navigateByUrl(url);
76-
})
77-
);
82+
this.router.navigateByUrl(this.storedPreviousUrl);
7883
};
7984

8085
ngOnInit(): void {
81-
8286
this.itemPageRoute = getItemPageRoute(this.object);
8387
// hide/show the back button
8488
this.showBackButton = this.routeService.getPreviousUrl().pipe(
85-
filter(url => this.previousRoute.test(url)),
8689
take(1),
87-
map(() => true)
90+
map(url => {
91+
const fromRoute = this.pickAllowedPrevious(url);
92+
93+
if (fromRoute) {
94+
this.routeService.storeUrlInSession(this.ITEM_PREVIOUS_URL_SESSION_KEY, fromRoute);
95+
this.storedPreviousUrl = fromRoute;
96+
return true;
97+
}
98+
99+
const storedUrl = this.routeService.getUrlFromSession(this.ITEM_PREVIOUS_URL_SESSION_KEY);
100+
if (this.pickAllowedPrevious(storedUrl)) {
101+
this.storedPreviousUrl = storedUrl;
102+
return true;
103+
}
104+
105+
return false;
106+
})
88107
);
108+
89109
// check to see if iiif viewer is required.
90110
this.iiifEnabled = isIiifEnabled(this.object);
91111
this.iiifSearchEnabled = isIiifSearchEnabled(this.object);
@@ -95,6 +115,13 @@ export class ItemComponent implements OnInit {
95115
this.isAuthenticated$ = this.store.pipe(select(isAuthenticated));
96116
}
97117

118+
/**
119+
* Helper to check if a URL is from an allowed previous route and return it, otherwise null
120+
*/
121+
private pickAllowedPrevious(url?: string | null): string | null {
122+
return url && this.previousRoute.test(url) ? url : null;
123+
}
124+
98125
get hasConfiguredStatistics(): boolean {
99126
return !!this.appConfig.statistics?.baseUrl && !!this.appConfig.statistics?.endpoint;
100127
}

src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,12 @@ describe('UntypedItemComponent', () => {
162162
const localMockRouteService = {
163163
getPreviousUrl(): Observable<string> {
164164
return of('/search?query=test%20query&fakeParam=true');
165+
},
166+
storeUrlInSession(key: string, url: string): void {
167+
// no-op
168+
},
169+
getUrlFromSession(key: string): string | null {
170+
return null;
165171
}
166172
};
167173
beforeEach(waitForAsync(() => {
@@ -193,6 +199,12 @@ describe('UntypedItemComponent', () => {
193199
const localMockRouteService = {
194200
getPreviousUrl(): Observable<string> {
195201
return of('/item');
202+
},
203+
storeUrlInSession(key: string, url: string): void {
204+
// no-op
205+
},
206+
getUrlFromSession(key: string): string | null {
207+
return null;
196208
}
197209
};
198210
beforeEach(waitForAsync(() => {

src/app/shared/testing/route-service.stub.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,16 @@ export const routeServiceStub: any = {
3737
},
3838
getPreviousUrl: () => {
3939
return observableOf('/home');
40+
},
41+
// Added generic session helpers used by ItemComponent
42+
storeUrlInSession: (key: string, url: string) => {
43+
// no-op for tests
44+
},
45+
getUrlFromSession: (key: string): string | null => {
46+
return null;
47+
},
48+
clearUrlFromSession: (key: string) => {
49+
// no-op for tests
4050
}
4151
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
4252
};

0 commit comments

Comments
 (0)