Skip to content
10 changes: 7 additions & 3 deletions src/app/core/pagination/pagination.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { ScrollServiceStub } from '@dspace/core/testing/scroll-service.stub';
import { of } from 'rxjs';

import {
SortDirection,
SortOptions,
} from '../cache/models/sort-options.model';
import { FindListOptions } from '../data/find-list-options.model';
import { ScrollService } from '../scroll/scroll.service';
import { RouterStub } from '../testing/router.stub';
import { PaginationService } from './pagination.service';
import { PaginationComponentOptions } from './pagination-component-options.model';
Expand All @@ -14,6 +16,7 @@ describe('PaginationService', () => {
let service: PaginationService;
let router;
let routeService;
let scrollService: ScrollService;

const defaultPagination = new PaginationComponentOptions();
const defaultSort = new SortOptions('dc.title', SortDirection.ASC);
Expand All @@ -39,8 +42,9 @@ describe('PaginationService', () => {
return of(value);
},
};
scrollService = new ScrollServiceStub() as any;

service = new PaginationService(routeService, router);
service = new PaginationService(routeService, router, scrollService);
});

describe('getCurrentPagination', () => {
Expand Down Expand Up @@ -73,7 +77,7 @@ describe('PaginationService', () => {
return of(value);
},
};
service = new PaginationService(routeService, router);
service = new PaginationService(routeService, router, scrollService);

service.getCurrentSort('test-id', defaultSort).subscribe((currentSort) => {
expect(currentSort).toEqual(defaultSort);
Expand All @@ -97,7 +101,7 @@ describe('PaginationService', () => {
spyOn(service, 'updateRoute');
service.resetPage('test');

expect(service.updateRoute).toHaveBeenCalledWith('test', { page: 1 });
expect(service.updateRoute).toHaveBeenCalledWith('test', { page: 1 }, undefined, undefined);
});
});

Expand Down
41 changes: 30 additions & 11 deletions src/app/core/pagination/pagination.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Injectable } from '@angular/core';
import {
Injectable,
InjectionToken,
} from '@angular/core';
import {
NavigationExtras,
Router,
Expand All @@ -11,6 +14,7 @@ import {
import { isNumeric } from '@dspace/shared/utils/numeric.util';
import { difference } from '@dspace/shared/utils/object.util';
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
Observable,
} from 'rxjs';
Expand All @@ -25,10 +29,13 @@ import {
SortOptions,
} from '../cache/models/sort-options.model';
import { FindListOptions } from '../data/find-list-options.model';
import { ScrollService } from '../scroll/scroll.service';
import { RouteService } from '../services/route.service';
import { PaginationComponentOptions } from './pagination-component-options.model';
import { PaginationRouteParams } from './pagination-route-params.interface';

export const RETAIN_SCROLL_POSITION: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('retainScrollPosition');

@Injectable({
providedIn: 'root',
})
Expand All @@ -53,6 +60,7 @@ export class PaginationService {

constructor(protected routeService: RouteService,
protected router: Router,
protected scrollService: ScrollService,
) {
}

Expand Down Expand Up @@ -124,9 +132,10 @@ export class PaginationService {
/**
* Reset the current page for the provided pagination ID to 1.
* @param paginationId - The pagination id for which to reset the page
* @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page
*/
resetPage(paginationId: string) {
this.updateRoute(paginationId, { page: 1 });
resetPage(paginationId: string, retainScrollPosition?: boolean): void {
this.updateRoute(paginationId, { page: 1 }, undefined, retainScrollPosition);
}


Expand Down Expand Up @@ -155,7 +164,7 @@ export class PaginationService {
* @param url - The url to navigate to
* @param params - The page related params to update in the route
* @param extraParams - Addition params unrelated to the pagination that need to be added to the route
* @param retainScrollPosition - Scroll to the pagination component after updating the route instead of the top of the page
* @param retainScrollPosition - Scroll to the active fragment after updating the route instead of the top of the page
* @param navigationExtras - Extra parameters to pass on to `router.navigate`. Can be used to override values set by this service.
*/
updateRouteWithUrl(
Expand All @@ -170,16 +179,26 @@ export class PaginationService {
const currentParametersWithIdName = this.getParametersWithIdName(paginationId, currentFindListOptions);
const parametersWithIdName = this.getParametersWithIdName(paginationId, params);
if (isNotEmpty(difference(parametersWithIdName, currentParametersWithIdName)) || isNotEmpty(extraParams) || isNotEmpty(this.clearParams)) {
const queryParams = Object.assign({}, this.clearParams, currentParametersWithIdName,
parametersWithIdName, extraParams);
const queryParams = Object.assign({}, currentParametersWithIdName,
parametersWithIdName, extraParams, this.clearParams);
if (retainScrollPosition) {
// By navigating to a non-existing ID, like "prevent-scroll", the browser won't perform any scroll operations
const fragment: string = this.scrollService.activeFragment ?? 'prevent-scroll';
this.scrollService.setFragment(fragment);
this.router.navigate(url, {
queryParams: queryParams,
queryParamsHandling: 'merge',
fragment: `p-${paginationId}`,
fragment: fragment,
...navigationExtras,
}).then((success: boolean) => {
setTimeout(() => {
if (success) {
this.scrollService.scrollToActiveFragment();
}
});
});
} else {
this.scrollService.setFragment(null);
this.router.navigate(url, {
queryParams: queryParams,
queryParamsHandling: 'merge',
Expand Down Expand Up @@ -230,16 +249,16 @@ export class PaginationService {

private getParametersWithIdName(paginationId: string, params: PaginationRouteParams) {
const paramsWithIdName = {};
if (hasValue(params.page)) {
if (hasValue(params?.page)) {
paramsWithIdName[`${paginationId}.page`] = `${params.page}`;
}
if (hasValue(params.pageSize)) {
if (hasValue(params?.pageSize)) {
paramsWithIdName[`${paginationId}.rpp`] = `${params.pageSize}`;
}
if (hasValue(params.sortField)) {
if (hasValue(params?.sortField)) {
paramsWithIdName[`${paginationId}.sf`] = `${params.sortField}`;
}
if (hasValue(params.sortDirection)) {
if (hasValue(params?.sortDirection)) {
paramsWithIdName[`${paginationId}.sd`] = `${params.sortDirection}`;
}
return paramsWithIdName;
Expand Down
41 changes: 41 additions & 0 deletions src/app/core/scroll/scroll.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { DOCUMENT } from '@angular/common';
import {
Inject,
Injectable,
} from '@angular/core';

/**
* Service used to scroll to a specific fragment/ID on the page
*/
@Injectable({
providedIn: 'root',
})
export class ScrollService {

activeFragment: string | null = null;

constructor(
@Inject(DOCUMENT) protected document: Document,
) {
}

/**
* Sets the fragment/ID that the user should jump to when the route is refreshed
*
* @param fragment The fragment/ID
*/
setFragment(fragment: string): void {
this.activeFragment = fragment;
}

/**
* Scrolls to the active fragment/ID if it exists
*/
scrollToActiveFragment(): void {
if (this.activeFragment) {
this.document.getElementById(this.activeFragment)?.scrollIntoView({
block: 'start',
});
}
}
}
2 changes: 1 addition & 1 deletion src/app/core/testing/router.stub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export class RouterStub {
url: string;
routeReuseStrategy = { shouldReuseRoute: {} };
//noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
navigate = jasmine.createSpy('navigate').and.returnValue(Promise.resolve(true));
parseUrl = jasmine.createSpy('parseUrl');
events = of({});
navigateByUrl(url): void {
Expand Down
10 changes: 10 additions & 0 deletions src/app/core/testing/scroll-service.stub.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
export class ScrollServiceStub {

setFragment(fragment: string): void {
}

scrollToActiveFragment(): void {
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ <h4 class="modal-title" id="modal-title">{{ ('submission.sections.describe.relat
[item]="item"
[isEditRelationship]="isEditRelationship"
[toRemove]="toRemove"
[retainScrollPosition]="true"
(selectObject)="select($event)"
(deselectObject)="deselect($event)"
(resultFound)="setTotalInternals($event.pageInfo.totalElements)"
Expand All @@ -47,6 +48,7 @@ <h4 class="modal-title" id="modal-title">{{ ('submission.sections.describe.relat
[context]="context"
[query]="query"
[externalSource]="source"
[retainScrollPosition]="true"
(importedObject)="imported($event)"
class="d-block pt-3">
</ds-dynamic-lookup-relation-external-source-tab>
Expand All @@ -62,6 +64,7 @@ <h4 class="modal-title" id="modal-title">{{ ('submission.sections.describe.relat
[listId]="listId"
[relationshipType]="relationshipOptions.relationshipType"
[repeatable]="repeatable"
[retainScrollPosition]="true"
[context]="context"
(selectObject)="select($event)"
(deselectObject)="deselect($event)"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { RequestParam } from '@dspace/core/cache/models/request-param.model';
import { ExternalSourceDataService } from '@dspace/core/data/external-source-data.service';
import { FindListOptions } from '@dspace/core/data/find-list-options.model';
import { PaginatedList } from '@dspace/core/data/paginated-list.model';
import { PaginationService } from '@dspace/core/pagination/pagination.service';
import { Context } from '@dspace/core/shared/context.model';
import { DSpaceObject } from '@dspace/core/shared/dspace-object.model';
import { ExternalSource } from '@dspace/core/shared/external-source.model';
Expand Down Expand Up @@ -218,6 +219,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
private zone: NgZone,
private store: Store<AppState>,
private router: Router,
protected paginationService: PaginationService,
) {

}
Expand Down Expand Up @@ -366,7 +368,10 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
}

ngOnDestroy() {
this.router.navigate([], {});
this.paginationService.clearPagination(this.searchConfigService.paginationID);
this.paginationService.updateRoute(this.searchConfigService.paginationID, undefined, undefined, true, {
queryParamsHandling: '',
});
Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<div class="row">
<div class="col-4">
<h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.settings' | translate}}</h3>
<ds-page-size-selector></ds-page-size-selector>
<ds-page-size-selector [retainScrollPosition]="retainScrollPosition"></ds-page-size-selector>
</div>
<div class="col-8">
<ds-search-form [query]="(searchConfigService.paginatedSearchOptions | async)?.query"
[inPlaceSearch]="true"
[retainScrollPosition]="retainScrollPosition"
[searchPlaceholder]="'submission.sections.describe.relationship-lookup.selection-tab.search-form.placeholder' | translate">
</ds-search-form>
<div>
Expand All @@ -20,6 +21,7 @@ <h3>{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' +
[context]="context"
[importable]="true"
[importConfig]="importConfig"
[retainScrollPosition]="retainScrollPosition"
(importObject)="import($event)">
</ds-viewable-collection>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
@Input() query: string;

@Input() repeatable: boolean;

/**
* Should scroll to the pagination component after updating the route instead of the top of the page
*/
@Input() retainScrollPosition = false;

/**
* Emit an event when an object has been imported (or selected from similar local entries)
*/
Expand Down Expand Up @@ -234,6 +240,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalComp.relationship = this.relationship;
modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType;
modalComp.retainScrollPosition = this.retainScrollPosition;
}));

this.subs.push(modalComp$.pipe(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ <h5 class="fw-bold">{{ (labelPrefix + 'entities' | translate) }}</h5>
[selectionConfig]="{ repeatable: false, listId: entityListId }"
[linkType]="linkTypes.ExternalLink"
[context]="context"
[retainScrollPosition]="retainScrollPosition"
(deselectObject)="deselectEntity()"
(selectObject)="selectEntity($event)">
</ds-search-results>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { AsyncPipe } from '@angular/common';
import {
Component,
EventEmitter,
Input,
OnInit,
} from '@angular/core';
import { ItemDataService } from '@dspace/core/data/item-data.service';
Expand Down Expand Up @@ -85,22 +86,22 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
/**
* The label to use for all messages (added to the end of relevant i18n keys)
*/
label: string;
@Input() label: string;

/**
* The external source entry
*/
externalSourceEntry: ExternalSourceEntry;
@Input() externalSourceEntry: ExternalSourceEntry;

/**
* The item in submission
*/
item: Item;
@Input() item: Item;

/**
* The current relationship-options used for filtering results
*/
relationship: RelationshipOptions;
@Input() relationship: RelationshipOptions;

/**
* The metadata value for the entry's uri
Expand Down Expand Up @@ -170,7 +171,12 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
/**
* The entity types compatible with the given external source
*/
relatedEntityType: ItemType;
@Input() relatedEntityType: ItemType;

/**
* Should scroll to the pagination component after updating the route instead of the top of the page
*/
@Input() retainScrollPosition = false;

/**
* The modal for the collection selection
Expand Down
Loading