Merge remote-tracking branch 'upstream/main' into #885-media-viewer

This commit is contained in:
Dániel Péter Sipos
2020-11-06 13:14:15 +01:00
26 changed files with 301 additions and 133 deletions

View File

@@ -1,7 +1,7 @@
## References ## References
_Add references/links to any related issues or PRs. These may include:_ _Add references/links to any related issues or PRs. These may include:_
* Fixes [GitHub issue](https://github.com/DSpace/dspace-angular/issues), if any * Fixes #[issue-number]
* Requires [REST API PR](https://github.com/DSpace/DSpace/pulls), if any * Requires DSpace/DSpace#[pr-number] (if a REST API PR is required to test this)
## Description ## Description
Short summary of changes (1-2 sentences). Short summary of changes (1-2 sentences).

View File

@@ -18,6 +18,7 @@ import { WorkflowItemSearchResult } from '../../../../../shared/object-collectio
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock'; import { getMockLinkService } from '../../../../../shared/mocks/link-service.mock';
import { of as observableOf } from 'rxjs';
describe('WorkflowItemAdminWorkflowGridElementComponent', () => { describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent; let component: WorkflowItemSearchResultAdminWorkflowGridElementComponent;
@@ -50,7 +51,9 @@ describe('WorkflowItemAdminWorkflowGridElementComponent', () => {
], ],
providers: [ providers: [
{ provide: LinkService, useValue: linkService }, { provide: LinkService, useValue: linkService },
{ provide: TruncatableService, useValue: {} }, { provide: TruncatableService, useValue: {
isCollapsed: () => observableOf(true),
} },
{ provide: BitstreamDataService, useValue: {} }, { provide: BitstreamDataService, useValue: {} },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]

View File

@@ -6,6 +6,7 @@ import { find } from 'rxjs/operators';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { Bitstream } from '../core/shared/bitstream.model'; import { Bitstream } from '../core/shared/bitstream.model';
import { BitstreamDataService } from '../core/data/bitstream-data.service'; import { BitstreamDataService } from '../core/data/bitstream-data.service';
import {followLink, FollowLinkConfig} from '../shared/utils/follow-link-config.model';
/** /**
* This class represents a resolver that requests a specific bitstream before the route is activated * This class represents a resolver that requests a specific bitstream before the route is activated
@@ -23,9 +24,20 @@ export class BitstreamPageResolver implements Resolve<RemoteData<Bitstream>> {
* or an error if something went wrong * or an error if something went wrong
*/ */
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Bitstream>> {
return this.bitstreamService.findById(route.params.id) return this.bitstreamService.findById(route.params.id, ...this.followLinks)
.pipe( .pipe(
find((RD) => hasValue(RD.error) || RD.hasSucceeded), find((RD) => hasValue(RD.error) || RD.hasSucceeded),
); );
} }
/**
* Method that returns the follow links to already resolve
* The self links defined in this list are expected to be requested somewhere in the near future
* Requesting them as embeds will limit the number of requests
*/
get followLinks(): Array<FollowLinkConfig<Bitstream>> {
return [
followLink('bundle', undefined, true, followLink('item')),
followLink('format')
];
}
} }

View File

@@ -4,7 +4,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs/internal/observable/of';
import { ActivatedRoute } from '@angular/router'; import {ActivatedRoute, Router} from '@angular/router';
import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
@@ -22,6 +22,11 @@ import { PageInfo } from '../../core/shared/page-info.model';
import { FileSizePipe } from '../../shared/utils/file-size-pipe'; import { FileSizePipe } from '../../shared/utils/file-size-pipe';
import { RestResponse } from '../../core/cache/response.models'; import { RestResponse } from '../../core/cache/response.models';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import {
createSuccessfulRemoteDataObject$
} from '../../shared/remote-data.utils';
import {RouterStub} from '../../shared/testing/router.stub';
import { getItemEditRoute } from '../../+item-page/item-page-routing-paths';
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
@@ -34,6 +39,8 @@ let bitstreamFormatService: BitstreamFormatDataService;
let bitstream: Bitstream; let bitstream: Bitstream;
let selectedFormat: BitstreamFormat; let selectedFormat: BitstreamFormat;
let allFormats: BitstreamFormat[]; let allFormats: BitstreamFormat[];
let router: Router;
let routerStub;
describe('EditBitstreamPageComponent', () => { describe('EditBitstreamPageComponent', () => {
let comp: EditBitstreamPageComponent; let comp: EditBitstreamPageComponent;
@@ -105,7 +112,12 @@ describe('EditBitstreamPageComponent', () => {
format: observableOf(new RemoteData(false, false, true, null, selectedFormat)), format: observableOf(new RemoteData(false, false, true, null, selectedFormat)),
_links: { _links: {
self: 'bitstream-selflink' self: 'bitstream-selflink'
} },
bundle: createSuccessfulRemoteDataObject$({
item: createSuccessfulRemoteDataObject$({
uuid: 'some-uuid'
})
})
}); });
bitstreamService = jasmine.createSpyObj('bitstreamService', { bitstreamService = jasmine.createSpyObj('bitstreamService', {
findById: observableOf(new RemoteData(false, false, true, null, bitstream)), findById: observableOf(new RemoteData(false, false, true, null, bitstream)),
@@ -118,6 +130,10 @@ describe('EditBitstreamPageComponent', () => {
findAll: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), allFormats))) findAll: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), allFormats)))
}); });
const itemPageUrl = `fake-url/some-uuid`;
routerStub = Object.assign(new RouterStub(), {
url: `${itemPageUrl}`
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule], imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective], declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
@@ -127,6 +143,7 @@ describe('EditBitstreamPageComponent', () => {
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: new RemoteData(false, false, true, null, bitstream) }), snapshot: { queryParams: {} } } }, { provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: new RemoteData(false, false, true, null, bitstream) }), snapshot: { queryParams: {} } } },
{ provide: BitstreamDataService, useValue: bitstreamService }, { provide: BitstreamDataService, useValue: bitstreamService },
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService }, { provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
{ provide: Router, useValue: routerStub },
ChangeDetectorRef ChangeDetectorRef
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -138,6 +155,7 @@ describe('EditBitstreamPageComponent', () => {
fixture = TestBed.createComponent(EditBitstreamPageComponent); fixture = TestBed.createComponent(EditBitstreamPageComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
fixture.detectChanges(); fixture.detectChanges();
router = (comp as any).router;
}); });
describe('on startup', () => { describe('on startup', () => {
@@ -213,4 +231,25 @@ describe('EditBitstreamPageComponent', () => {
}); });
}); });
}); });
describe('when the cancel button is clicked', () => {
it('should call navigateToItemEditBitstreams method', () => {
spyOn(comp, 'navigateToItemEditBitstreams');
comp.onCancel();
expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled();
});
});
describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
comp.itemId = 'some-uuid1'
comp.navigateToItemEditBitstreams();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid1'), 'bitstreams']);
});
});
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
comp.itemId = undefined;
comp.navigateToItemEditBitstreams();
expect(routerStub.navigate).toHaveBeenCalledWith([getItemEditRoute('some-uuid'), 'bitstreams']);
});
});
}); });

View File

@@ -1,7 +1,7 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { filter, map, switchMap } from 'rxjs/operators'; import { map, mergeMap, switchMap} from 'rxjs/operators';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
import { import {
@@ -19,7 +19,7 @@ import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-f
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
import { import {
getAllSucceededRemoteData, getAllSucceededRemoteDataPayload, getAllSucceededRemoteDataPayload,
getFirstSucceededRemoteDataPayload, getFirstSucceededRemoteDataPayload,
getRemoteDataPayload, getRemoteDataPayload,
getSucceededRemoteData getSucceededRemoteData
@@ -35,8 +35,9 @@ import { Location } from '@angular/common';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { getItemEditRoute } from '../../+item-page/item-page-routing-paths'; import { getItemEditRoute } from '../../+item-page/item-page-routing-paths';
import {Bundle} from '../../core/shared/bundle.model';
import {Item} from '../../core/shared/item.model';
@Component({ @Component({
selector: 'ds-edit-bitstream-page', selector: 'ds-edit-bitstream-page',
@@ -299,12 +300,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
const bitstream$ = this.bitstreamRD$.pipe( const bitstream$ = this.bitstreamRD$.pipe(
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload()
switchMap((bitstream: Bitstream) => this.bitstreamService.findById(bitstream.id, followLink('format')).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
filter((bs: Bitstream) => hasValue(bs)))
)
); );
const allFormats$ = this.bitstreamFormatsRD$.pipe( const allFormats$ = this.bitstreamFormatsRD$.pipe(
@@ -501,14 +497,18 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
} }
/** /**
* When the item ID is present, navigate back to the item's edit bitstreams page, otherwise go back to the previous * When the item ID is present, navigate back to the item's edit bitstreams page,
* page the user came from * otherwise retrieve the item ID based on the owning bundle's link
*/ */
navigateToItemEditBitstreams() { navigateToItemEditBitstreams() {
if (hasValue(this.itemId)) { if (hasValue(this.itemId)) {
this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']); this.router.navigate([getItemEditRoute(this.itemId), 'bitstreams']);
} else { } else {
this.location.back(); this.bitstream.bundle.pipe(getFirstSucceededRemoteDataPayload(),
mergeMap((bundle: Bundle) => bundle.item.pipe(getFirstSucceededRemoteDataPayload(), map((item: Item) => item.uuid))))
.subscribe((item) => {
this.router.navigate(([getItemEditRoute(item), 'bitstreams']));
});
} }
} }

View File

@@ -17,7 +17,7 @@ import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
import { import {
getSucceededRemoteData, getSucceededRemoteData,
redirectToPageNotFoundOn404, redirectOn404Or401,
toDSpaceObjectListRD toDSpaceObjectListRD
} from '../core/shared/operators'; } from '../core/shared/operators';
@@ -63,7 +63,7 @@ export class CollectionPageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.collectionRD$ = this.route.data.pipe( this.collectionRD$ = this.route.data.pipe(
map((data) => data.dso as RemoteData<Collection>), map((data) => data.dso as RemoteData<Collection>),
redirectToPageNotFoundOn404(this.router), redirectOn404Or401(this.router),
take(1) take(1)
); );
this.logoRD$ = this.collectionRD$.pipe( this.logoRD$ = this.collectionRD$.pipe(

View File

@@ -13,7 +13,7 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade'; import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { redirectToPageNotFoundOn404 } from '../core/shared/operators'; import { redirectOn404Or401 } from '../core/shared/operators';
@Component({ @Component({
selector: 'ds-community-page', selector: 'ds-community-page',
@@ -47,7 +47,7 @@ export class CommunityPageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.communityRD$ = this.route.data.pipe( this.communityRD$ = this.route.data.pipe(
map((data) => data.dso as RemoteData<Community>), map((data) => data.dso as RemoteData<Community>),
redirectToPageNotFoundOn404(this.router) redirectOn404Or401(this.router)
); );
this.logoRD$ = this.communityRD$.pipe( this.logoRD$ = this.communityRD$.pipe(
map((rd: RemoteData<Community>) => rd.payload), map((rd: RemoteData<Community>) => rd.payload),

View File

@@ -11,7 +11,7 @@ import { Item } from '../../core/shared/item.model';
import { MetadataService } from '../../core/metadata/metadata.service'; import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { redirectOn404Or401 } from '../../core/shared/operators';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
/** /**
@@ -56,7 +56,7 @@ export class ItemPageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.itemRD$ = this.route.data.pipe( this.itemRD$ = this.route.data.pipe(
map((data) => data.item as RemoteData<Item>), map((data) => data.item as RemoteData<Item>),
redirectToPageNotFoundOn404(this.router) redirectOn404Or401(this.router)
); );
this.metadataService.processRemoteData(this.itemRD$); this.metadataService.processRemoteData(this.itemRD$);
} }

View File

@@ -55,12 +55,18 @@ export function getDSORoute(dso: DSpaceObject): string {
} }
} }
export const UNAUTHORIZED_PATH = 'unauthorized'; export const UNAUTHORIZED_PATH = '401';
export function getUnauthorizedRoute() { export function getUnauthorizedRoute() {
return `/${UNAUTHORIZED_PATH}`; return `/${UNAUTHORIZED_PATH}`;
} }
export const PAGE_NOT_FOUND_PATH = '404';
export function getPageNotFoundRoute() {
return `/${PAGE_NOT_FOUND_PATH}`;
}
export const INFO_MODULE_PATH = 'info'; export const INFO_MODULE_PATH = 'info';
export function getInfoModulePath() { export function getInfoModulePath() {
return `/${INFO_MODULE_PATH}`; return `/${INFO_MODULE_PATH}`;

View File

@@ -3,12 +3,13 @@ import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
import { DataService } from '../data/data.service'; import { DataService } from '../data/data.service';
import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; import { getRemoteDataPayload } from '../shared/operators';
import { map } from 'rxjs/operators'; import { filter, map, take } from 'rxjs/operators';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { ChildHALResource } from '../shared/child-hal-resource.model'; import { ChildHALResource } from '../shared/child-hal-resource.model';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
import { hasValue } from '../../shared/empty.util';
/** /**
* The class that resolves the BreadcrumbConfig object for a DSpaceObject * The class that resolves the BreadcrumbConfig object for a DSpaceObject
@@ -29,12 +30,17 @@ export abstract class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceO
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
const uuid = route.params.id; const uuid = route.params.id;
return this.dataService.findById(uuid, ...this.followLinks).pipe( return this.dataService.findById(uuid, ...this.followLinks).pipe(
getSucceededRemoteData(), filter((rd) => hasValue(rd.error) || hasValue(rd.payload)),
take(1),
getRemoteDataPayload(), getRemoteDataPayload(),
map((object: T) => { map((object: T) => {
if (hasValue(object)) {
const fullPath = state.url; const fullPath = state.url;
const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid;
return {provider: this.breadcrumbService, key: object, url: url}; return {provider: this.breadcrumbService, key: object, url: url};
} else {
return undefined;
}
}) })
); );
} }

View File

@@ -8,6 +8,8 @@ import { BITSTREAM } from './bitstream.resource-type';
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { HALLink } from './hal-link.model'; import { HALLink } from './hal-link.model';
import { HALResource } from './hal-resource.model'; import { HALResource } from './hal-resource.model';
import {BUNDLE} from './bundle.resource-type';
import {Bundle} from './bundle.model';
@typedObject @typedObject
@inheritSerialization(DSpaceObject) @inheritSerialization(DSpaceObject)
@@ -57,4 +59,10 @@ export class Bitstream extends DSpaceObject implements HALResource {
@link(BITSTREAM_FORMAT, false, 'format') @link(BITSTREAM_FORMAT, false, 'format')
format?: Observable<RemoteData<BitstreamFormat>>; format?: Observable<RemoteData<BitstreamFormat>>;
/**
* The owning bundle for this Bitstream
* Will be undefined unless the bundle{@link HALLink} has been resolved.
*/
@link(BUNDLE)
bundle?: Observable<RemoteData<Bundle>>;
} }

View File

@@ -10,6 +10,8 @@ import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list'; import { PaginatedList } from '../data/paginated-list';
import { BITSTREAM } from './bitstream.resource-type'; import { BITSTREAM } from './bitstream.resource-type';
import { Bitstream } from './bitstream.model'; import { Bitstream } from './bitstream.model';
import {ITEM} from './item.resource-type';
import {Item} from './item.model';
@typedObject @typedObject
@inheritSerialization(DSpaceObject) @inheritSerialization(DSpaceObject)
@@ -24,6 +26,7 @@ export class Bundle extends DSpaceObject {
self: HALLink; self: HALLink;
primaryBitstream: HALLink; primaryBitstream: HALLink;
bitstreams: HALLink; bitstreams: HALLink;
item: HALLink;
}; };
/** /**
@@ -39,4 +42,11 @@ export class Bundle extends DSpaceObject {
*/ */
@link(BITSTREAM, true) @link(BITSTREAM, true)
bitstreams?: Observable<RemoteData<PaginatedList<Bitstream>>>; bitstreams?: Observable<RemoteData<PaginatedList<Bitstream>>>;
/**
* The owning item for this Bundle
* Will be undefined unless the Item{@link HALLink} has been resolved.
*/
@link(ITEM)
item?: Observable<RemoteData<Item>>;
} }

View File

@@ -13,7 +13,8 @@ import {
getRequestFromRequestUUID, getRequestFromRequestUUID,
getResourceLinksFromResponse, getResourceLinksFromResponse,
getResponseFromEntry, getResponseFromEntry,
getSucceededRemoteData, redirectToPageNotFoundOn404 getSucceededRemoteData,
redirectOn404Or401
} from './operators'; } from './operators';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { RemoteDataError } from '../data/remote-data-error'; import { RemoteDataError } from '../data/remote-data-error';
@@ -199,7 +200,7 @@ describe('Core Module - RxJS Operators', () => {
}); });
}); });
describe('redirectToPageNotFoundOn404', () => { describe('redirectOn404Or401', () => {
let router; let router;
beforeEach(() => { beforeEach(() => {
router = jasmine.createSpyObj('router', ['navigateByUrl']); router = jasmine.createSpyObj('router', ['navigateByUrl']);
@@ -208,21 +209,28 @@ describe('Core Module - RxJS Operators', () => {
it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => { it('should call navigateByUrl to a 404 page, when the remote data contains a 404 error', () => {
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(404, 'Not Found', 'Object was not found')); const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(404, 'Not Found', 'Object was not found'));
observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true }); expect(router.navigateByUrl).toHaveBeenCalledWith('/404', { skipLocationChange: true });
}); });
it('should not call navigateByUrl to a 404 page, when the remote data contains another error than a 404', () => { it('should call navigateByUrl to a 401 page, when the remote data contains a 401 error', () => {
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(401, 'Unauthorized', 'The current user is unauthorized'));
observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
expect(router.navigateByUrl).toHaveBeenCalledWith('/401', { skipLocationChange: true });
});
it('should not call navigateByUrl to a 404 or 401 page, when the remote data contains another error than a 404 or 401', () => {
const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(500, 'Server Error', 'Something went wrong')); const testRD = createFailedRemoteDataObject(undefined, new RemoteDataError(500, 'Server Error', 'Something went wrong'));
observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
expect(router.navigateByUrl).not.toHaveBeenCalled(); expect(router.navigateByUrl).not.toHaveBeenCalled();
}); });
it('should not call navigateByUrl to a 404 page, when the remote data contains no error', () => { it('should not call navigateByUrl to a 404 or 401 page, when the remote data contains no error', () => {
const testRD = createSuccessfulRemoteDataObject(undefined); const testRD = createSuccessfulRemoteDataObject(undefined);
observableOf(testRD).pipe(redirectToPageNotFoundOn404(router)).subscribe(); observableOf(testRD).pipe(redirectOn404Or401(router)).subscribe();
expect(router.navigateByUrl).not.toHaveBeenCalled(); expect(router.navigateByUrl).not.toHaveBeenCalled();
}); });
}); });

View File

@@ -13,7 +13,7 @@ import { MetadataField } from '../metadata/metadata-field.model';
import { MetadataSchema } from '../metadata/metadata-schema.model'; import { MetadataSchema } from '../metadata/metadata-schema.model';
import { BrowseDefinition } from './browse-definition.model'; import { BrowseDefinition } from './browse-definition.model';
import { DSpaceObject } from './dspace-object.model'; import { DSpaceObject } from './dspace-object.model';
import { getUnauthorizedRoute } from '../../app-routing-paths'; import { getPageNotFoundRoute, getUnauthorizedRoute } from '../../app-routing-paths';
import { getEndUserAgreementPath } from '../../info/info-routing-paths'; import { getEndUserAgreementPath } from '../../info/info-routing-paths';
/** /**
@@ -171,16 +171,20 @@ export const getAllSucceededRemoteListPayload = () =>
); );
/** /**
* Operator that checks if a remote data object contains a page not found error * Operator that checks if a remote data object returned a 401 or 404 error
* When it does contain such an error, it will redirect the user to a page not found, without altering the current URL * When it does contain such an error, it will redirect the user to the related error page, without altering the current URL
* @param router The router used to navigate to a new page * @param router The router used to navigate to a new page
*/ */
export const redirectToPageNotFoundOn404 = (router: Router) => export const redirectOn404Or401 = (router: Router) =>
<T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> => <T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> =>
source.pipe( source.pipe(
tap((rd: RemoteData<T>) => { tap((rd: RemoteData<T>) => {
if (rd.hasFailed && rd.error.statusCode === 404) { if (rd.hasFailed) {
router.navigateByUrl('/404', { skipLocationChange: true }); if (rd.error.statusCode === 404) {
router.navigateByUrl(getPageNotFoundRoute(), {skipLocationChange: true});
} else if (rd.error.statusCode === 401) {
router.navigateByUrl(getUnauthorizedRoute(), {skipLocationChange: true});
}
} }
})); }));

View File

@@ -4,7 +4,7 @@ import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Process } from '../processes/process.model'; import { Process } from '../processes/process.model';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { getFirstSucceededRemoteDataPayload, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload, redirectOn404Or401 } from '../../core/shared/operators';
import { AlertType } from '../../shared/alert/aletr-type'; import { AlertType } from '../../shared/alert/aletr-type';
import { ProcessDataService } from '../../core/data/processes/process-data.service'; import { ProcessDataService } from '../../core/data/processes/process-data.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
@@ -49,7 +49,7 @@ export class ProcessDetailComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.processRD$ = this.route.data.pipe( this.processRD$ = this.route.data.pipe(
map((data) => data.process as RemoteData<Process>), map((data) => data.process as RemoteData<Process>),
redirectToPageNotFoundOn404(this.router) redirectOn404Or401(this.router)
); );
this.filesRD$ = this.processRD$.pipe( this.filesRD$ = this.processRD$.pipe(

View File

@@ -50,8 +50,7 @@ export const externalSourceMyStaffDb: ExternalSource = {
/** /**
* Mock for [[ExternalSourceService]] * Mock for [[ExternalSourceService]]
*/ */
export function getMockExternalSourceService(): export function getMockExternalSourceService(): ExternalSourceService {
ExternalSourceService {
return jasmine.createSpyObj('ExternalSourceService', { return jasmine.createSpyObj('ExternalSourceService', {
findAll: jasmine.createSpy('findAll'), findAll: jasmine.createSpy('findAll'),
getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'), getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'),

View File

@@ -19,6 +19,7 @@ $ds-wrapper-grid-spacing: $spacer/2;
.card-columns { .card-columns {
margin-left: -$ds-wrapper-grid-spacing; margin-left: -$ds-wrapper-grid-spacing;
margin-right: -$ds-wrapper-grid-spacing; margin-right: -$ds-wrapper-grid-spacing;
column-gap: 0;
.card-column { .card-column {
padding-left: $ds-wrapper-grid-spacing; padding-left: $ds-wrapper-grid-spacing;

View File

@@ -32,9 +32,6 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
protected bitstreamDataService: BitstreamDataService protected bitstreamDataService: BitstreamDataService
) { ) {
super(); super();
if (hasValue(this.object)) {
this.isCollapsed$ = this.isCollapsed();
}
} }
/** /**
@@ -43,6 +40,7 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
ngOnInit(): void { ngOnInit(): void {
if (hasValue(this.object)) { if (hasValue(this.object)) {
this.dso = this.object.indexableObject; this.dso = this.object.indexableObject;
this.isCollapsed$ = this.isCollapsed();
} }
} }

View File

@@ -8,12 +8,11 @@
[innerHTML]="firstMetadataValue('dc.title')"></span> [innerHTML]="firstMetadataValue('dc.title')"></span>
<span class="text-muted"> <span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">
<ng-container *ngIf="dso.firstMetadataValue('dc.publisher') || dso.firstMetadataValue('dc.date.issued')">(<span class="item-list-publisher" <ng-container *ngIf="dso.firstMetadataValue('dc.publisher') || dso.firstMetadataValue('dc.date.issued')">
[innerHTML]="firstMetadataValue('dc.publisher')">, </span><span (<span *ngIf="dso.firstMetadataValue('dc.publisher')" class="item-list-publisher" [innerHTML]="firstMetadataValue('dc.publisher') + ', '"></span>
*ngIf="dso.firstMetadataValue('dc.date.issued')" class="item-list-date" <span *ngIf="dso.firstMetadataValue('dc.date.issued')" class="item-list-date" [innerHTML]="firstMetadataValue('dc.date.issued')"></span>)
[innerHTML]="firstMetadataValue('dc.date.issued')"></span>)</ng-container> </ng-container>
<span *ngIf="dso.allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" <span *ngIf="dso.allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-list-authors">
class="item-list-authors">
<span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;"> <span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
<span [innerHTML]="author"><span [innerHTML]="author"></span></span> <span [innerHTML]="author"><span [innerHTML]="author"></span></span>
<span *ngIf="!last">; </span> <span *ngIf="!last">; </span>

View File

@@ -4,7 +4,7 @@ import { UsageReportService } from '../../core/statistics/usage-report-data.serv
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { UsageReport } from '../../core/statistics/models/usage-report.model'; import { UsageReport } from '../../core/statistics/models/usage-report.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { getRemoteDataPayload, getSucceededRemoteData, redirectToPageNotFoundOn404 } from '../../core/shared/operators'; import { getRemoteDataPayload, getSucceededRemoteData, redirectOn404Or401 } from '../../core/shared/operators';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
@@ -55,7 +55,7 @@ export abstract class StatisticsPageComponent<T extends DSpaceObject> implements
protected getScope$(): Observable<DSpaceObject> { protected getScope$(): Observable<DSpaceObject> {
return this.route.data.pipe( return this.route.data.pipe(
map((data) => data.scope as RemoteData<T>), map((data) => data.scope as RemoteData<T>),
redirectToPageNotFoundOn404(this.router), redirectOn404Or401(this.router),
getSucceededRemoteData(), getSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
); );

View File

@@ -13,7 +13,7 @@
.scrollable-menu { .scrollable-menu {
height: auto; height: auto;
max-height: $dropdown-menu-max-height; max-height: $dropdown-menu-max-height / 2;
overflow-x: hidden; overflow-x: hidden;
} }

View File

@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { of as observableOf, Observable } from 'rxjs'; import { Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { ExternalSourceService } from '../../../core/data/external-source.service'; import { ExternalSourceService } from '../../../core/data/external-source.service';
@@ -12,6 +12,7 @@ import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.ut
import { FindListOptions } from '../../../core/data/request.models'; import { FindListOptions } from '../../../core/data/request.models';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowService } from '../../../shared/host-window.service';
import { hasValue } from '../../../shared/empty.util';
/** /**
* Interface for the selected external source element. * Interface for the selected external source element.
@@ -37,7 +38,7 @@ export interface ExternalSourceData {
styleUrls: ['./submission-import-external-searchbar.component.scss'], styleUrls: ['./submission-import-external-searchbar.component.scss'],
templateUrl: './submission-import-external-searchbar.component.html' templateUrl: './submission-import-external-searchbar.component.html'
}) })
export class SubmissionImportExternalSearchbarComponent implements OnInit { export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDestroy {
/** /**
* The init external source value. * The init external source value.
*/ */
@@ -76,6 +77,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
*/ */
protected findListOptions: FindListOptions; protected findListOptions: FindListOptions;
/**
* The subscription to unsubscribe
*/
protected sub: Subscription;
/** /**
* Initialize the component variables. * Initialize the component variables.
* @param {ExternalSourceService} externalService * @param {ExternalSourceService} externalService
@@ -101,7 +107,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
this.sourceList = []; this.sourceList = [];
this.findListOptions = Object.assign({}, new FindListOptions(), { this.findListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: 5, elementsPerPage: 5,
currentPage: 0, currentPage: 1,
}); });
this.externalService.findAll(this.findListOptions).pipe( this.externalService.findAll(this.findListOptions).pipe(
catchError(() => { catchError(() => {
@@ -139,13 +145,13 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
* Load the next pages of external sources. * Load the next pages of external sources.
*/ */
public onScroll(): void { public onScroll(): void {
if (!this.sourceListLoading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { if (!this.sourceListLoading && ((this.pageInfo.currentPage + 1) <= this.pageInfo.totalPages)) {
this.sourceListLoading = true; this.sourceListLoading = true;
this.findListOptions = Object.assign({}, new FindListOptions(), { this.findListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: 5, elementsPerPage: 5,
currentPage: this.findListOptions.currentPage + 1, currentPage: this.findListOptions.currentPage + 1,
}); });
this.externalService.findAll(this.findListOptions).pipe( this.sub = this.externalService.findAll(this.findListOptions).pipe(
catchError(() => { catchError(() => {
const pageInfo = new PageInfo(); const pageInfo = new PageInfo();
const paginatedList = new PaginatedList(pageInfo, []); const paginatedList = new PaginatedList(pageInfo, []);
@@ -159,7 +165,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
}) })
this.pageInfo = externalSource.payload.pageInfo; this.pageInfo = externalSource.payload.pageInfo;
this.cdr.detectChanges(); this.cdr.detectChanges();
}) });
} }
} }
@@ -169,4 +175,13 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
public search(): void { public search(): void {
this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString }); this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString });
} }
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
} }

View File

@@ -4,14 +4,14 @@
<h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2> <h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2>
<ds-submission-import-external-searchbar <ds-submission-import-external-searchbar
[initExternalSourceData]="routeData" [initExternalSourceData]="routeData"
(externalSourceData) = "getExternalsourceData($event)"> (externalSourceData) = "getExternalSourceData($event)">
</ds-submission-import-external-searchbar> </ds-submission-import-external-searchbar>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div *ngIf="routeData.sourceId !== ''" class="col-md-12"> <div *ngIf="routeData.sourceId !== ''" class="col-md-12">
<ng-container *ngVar="(entriesRD$ | async) as entriesRD"> <ng-container *ngVar="(entriesRD$ | async) as entriesRD">
<h3 *ngIf="entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}}</h3> <h3 *ngIf="entriesRD && entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}}</h3>
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn <ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn
[objects]="entriesRD" [objects]="entriesRD"
[selectionConfig]="{ repeatable: repeatable, listId: listId }" [selectionConfig]="{ repeatable: repeatable, listId: listId }"
@@ -20,7 +20,8 @@
[context]="context" [context]="context"
[importable]="true" [importable]="true"
[importConfig]="importConfig" [importConfig]="importConfig"
(importObject)="import($event)"> (importObject)="import($event)"
(pageChange)="paginationChange();">
</ds-viewable-collection> </ds-viewable-collection>
<ds-loading *ngIf="(isLoading$ | async)" <ds-loading *ngIf="(isLoading$ | async)"
message="{{'loading.search-results' | translate}}"></ds-loading> message="{{'loading.search-results' | translate}}"></ds-loading>

View File

@@ -1,21 +1,25 @@
import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, TestBed, ComponentFixture, inject } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { SubmissionImportExternalComponent } from './submission-import-external.component'; import { SubmissionImportExternalComponent } from './submission-import-external.component';
import { ExternalSourceService } from '../../core/data/external-source.service'; import { ExternalSourceService } from '../../core/data/external-source.service';
import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock'; import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { createTestComponent, createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList, createTestComponent } from '../../shared/testing/utils.test';
import { RouterStub } from '../../shared/testing/router.stub'; import { RouterStub } from '../../shared/testing/router.stub';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
@@ -23,16 +27,19 @@ describe('SubmissionImportExternalComponent test suite', () => {
let comp: SubmissionImportExternalComponent; let comp: SubmissionImportExternalComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<SubmissionImportExternalComponent>; let fixture: ComponentFixture<SubmissionImportExternalComponent>;
let scheduler: TestScheduler;
const ngbModal = jasmine.createSpyObj('modal', ['open']); const ngbModal = jasmine.createSpyObj('modal', ['open']);
const mockSearchOptions = of(new PaginatedSearchOptions({ const mockSearchOptions = observableOf(new PaginatedSearchOptions({
pagination: Object.assign(new PaginationComponentOptions(), { pagination: Object.assign(new PaginationComponentOptions(), {
pageSize: 10, pageSize: 10,
currentPage: 0 currentPage: 0
}) }),
query: 'test'
})); }));
const searchConfigServiceStub = { const searchConfigServiceStub = {
paginatedSearchOptions: mockSearchOptions paginatedSearchOptions: mockSearchOptions
}; };
const mockExternalSourceService: any = getMockExternalSourceService();
beforeEach(async (() => { beforeEach(async (() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -45,7 +52,7 @@ describe('SubmissionImportExternalComponent test suite', () => {
VarDirective VarDirective
], ],
providers: [ providers: [
{ provide: ExternalSourceService, useClass: getMockExternalSourceService }, { provide: ExternalSourceService, useValue: mockExternalSourceService },
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub }, { provide: SearchConfigurationService, useValue: searchConfigServiceStub },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
@@ -83,6 +90,8 @@ describe('SubmissionImportExternalComponent test suite', () => {
fixture = TestBed.createComponent(SubmissionImportExternalComponent); fixture = TestBed.createComponent(SubmissionImportExternalComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
scheduler = getTestScheduler();
mockExternalSourceService.getExternalSourceEntries.and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([])))
}); });
afterEach(() => { afterEach(() => {
@@ -102,25 +111,31 @@ describe('SubmissionImportExternalComponent test suite', () => {
}); });
it('Should init component properly (with route data)', () => { it('Should init component properly (with route data)', () => {
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); spyOn(compAsAny, 'retrieveExternalSources');
const searchOptions = new PaginatedSearchOptions({ spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('source'), observableOf('dummy'));
pagination: Object.assign(new PaginationComponentOptions(), {
pageSize: 10,
currentPage: 0
})
});
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('dummy'));
fixture.detectChanges(); fixture.detectChanges();
expect(comp.routeData).toEqual({ sourceId: 'dummy', query: 'dummy' }); expect(compAsAny.retrieveExternalSources).toHaveBeenCalledWith('source', 'dummy');
expect(comp.isLoading$.value).toBe(true); });
expect(comp.entriesRD$.value).toEqual(expectedEntries);
expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalledWith('dummy', searchOptions); it('Should call \'getExternalSourceEntries\' properly', () => {
comp.routeData = { sourceId: '', query: '' };
scheduler.schedule(() => compAsAny.retrieveExternalSources('orcidV2', 'test'));
scheduler.flush();
expect(comp.routeData).toEqual({ sourceId: 'orcidV2', query: 'test' });
expect(comp.isLoading$.value).toBe(false);
expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalled();
}); });
it('Should call \'router.navigate\'', () => { it('Should call \'router.navigate\'', () => {
comp.routeData = { sourceId: '', query: '' };
spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null);
compAsAny.router.navigate.and.returnValue( new Promise(() => {return;}))
const event = { sourceId: 'orcidV2', query: 'dummy' }; const event = { sourceId: 'orcidV2', query: 'dummy' };
comp.getExternalsourceData(event);
scheduler.schedule(() => comp.getExternalSourceData(event));
scheduler.flush();
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true }); expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true });
}); });

View File

@@ -1,22 +1,25 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { combineLatest, BehaviorSubject } from 'rxjs';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { filter, flatMap, take } from 'rxjs/operators';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceService } from '../../core/data/external-source.service'; import { ExternalSourceService } from '../../core/data/external-source.service';
import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { switchMap, filter, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { Context } from '../../core/shared/context.model'; import { Context } from '../../core/shared/context.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { getFinishedRemoteData } from '../../core/shared/operators';
/** /**
* This component allows to submit a new workspaceitem importing the data from an external source. * This component allows to submit a new workspaceitem importing the data from an external source.
@@ -27,7 +30,8 @@ import { PageInfo } from '../../core/shared/page-info.model';
templateUrl: './submission-import-external.component.html', templateUrl: './submission-import-external.component.html',
animations: [fadeIn] animations: [fadeIn]
}) })
export class SubmissionImportExternalComponent implements OnInit { export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
/** /**
* The external source search data from the routing service. * The external source search data from the routing service.
*/ */
@@ -35,11 +39,11 @@ export class SubmissionImportExternalComponent implements OnInit {
/** /**
* The displayed list of entries * The displayed list of entries
*/ */
public entriesRD$: BehaviorSubject<RemoteData<PaginatedList<ExternalSourceEntry>>>; public entriesRD$: BehaviorSubject<RemoteData<PaginatedList<ExternalSourceEntry>>> = new BehaviorSubject<RemoteData<PaginatedList<ExternalSourceEntry>>>(null);
/** /**
* TRUE if the REST service is called to retrieve the external source items * TRUE if the REST service is called to retrieve the external source items
*/ */
public isLoading$: BehaviorSubject<boolean>; public isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
/** /**
* Configuration to use for the import buttons * Configuration to use for the import buttons
*/ */
@@ -61,7 +65,7 @@ export class SubmissionImportExternalComponent implements OnInit {
*/ */
public initialPagination = Object.assign(new PaginationComponentOptions(), { public initialPagination = Object.assign(new PaginationComponentOptions(), {
id: 'submission-external-source-relation-list', id: 'submission-external-source-relation-list',
pageSize: 5 pageSize: 10
}); });
/** /**
* The context to displaying lists for * The context to displaying lists for
@@ -72,6 +76,11 @@ export class SubmissionImportExternalComponent implements OnInit {
*/ */
public modalRef: NgbModalRef; public modalRef: NgbModalRef;
/**
* The subscription to unsubscribe
*/
protected subs: Subscription[] = [];
/** /**
* Initialize the component variables. * Initialize the component variables.
* @param {SearchConfigurationService} searchConfigService * @param {SearchConfigurationService} searchConfigService
@@ -86,7 +95,8 @@ export class SubmissionImportExternalComponent implements OnInit {
private routeService: RouteService, private routeService: RouteService,
private router: Router, private router: Router,
private modalService: NgbModal, private modalService: NgbModal,
) { } ) {
}
/** /**
* Get the entries for the selected external source and set initial configuration. * Get the entries for the selected external source and set initial configuration.
@@ -102,41 +112,28 @@ export class SubmissionImportExternalComponent implements OnInit {
}; };
this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), []))); this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), [])));
this.isLoading$ = new BehaviorSubject(false); this.isLoading$ = new BehaviorSubject(false);
combineLatest( this.subs.push(combineLatest(
[ [
this.routeService.getQueryParameterValue('source'), this.routeService.getQueryParameterValue('source'),
this.routeService.getQueryParameterValue('query') this.routeService.getQueryParameterValue('query')
]).pipe( ]).pipe(
filter(([source, query]) => source && query && source !== '' && query !== ''),
filter(([source, query]) => source !== this.routeData.sourceId || query !== this.routeData.query),
switchMap(([source, query]) => {
this.routeData.sourceId = source;
this.routeData.query = query;
this.isLoading$.next(true);
return this.searchConfigService.paginatedSearchOptions.pipe(
switchMap((searchOptions: PaginatedSearchOptions) => {
return this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions);
}),
take(1) take(1)
) ).subscribe(([source, query]: [string, string]) => {
}), this.retrieveExternalSources(source, query);
).subscribe((rdData) => { }));
this.entriesRD$.next(rdData);
this.isLoading$.next(false);
});
} }
/** /**
* Get the data from the searchbar and changes the router data. * Get the data from the searchbar and changes the router data.
*/ */
public getExternalsourceData(event: ExternalSourceData): void { public getExternalSourceData(event: ExternalSourceData): void {
this.router.navigate( this.router.navigate(
[], [],
{ {
queryParams: { source: event.sourceId, query: event.query }, queryParams: { source: event.sourceId, query: event.query },
replaceUrl: true replaceUrl: true
} }
); ).then(() => this.retrieveExternalSources(event.sourceId, event.query));
} }
/** /**
@@ -150,4 +147,49 @@ export class SubmissionImportExternalComponent implements OnInit {
const modalComp = this.modalRef.componentInstance; const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry; modalComp.externalSourceEntry = entry;
} }
/**
* Retrieve external sources on pagination change
*/
paginationChange() {
this.retrieveExternalSources(this.routeData.sourceId, this.routeData.query);
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**
* Retrieve external source entries
*
* @param source The source tupe
* @param query The query string to search
*/
private retrieveExternalSources(source: string, query: string): void {
if (isNotEmpty(source) && isNotEmpty(query)) {
this.routeData.sourceId = source;
this.routeData.query = query;
this.isLoading$.next(true);
this.subs.push(
this.searchConfigService.paginatedSearchOptions.pipe(
filter((searchOptions) => searchOptions.query === query),
take(1),
flatMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe(
getFinishedRemoteData(),
take(1)
)),
take(1)
).subscribe((rdData) => {
this.entriesRD$.next(rdData);
this.isLoading$.next(false);
})
);
}
}
} }

View File

@@ -2931,6 +2931,8 @@
"submission.import-external.search.source.hint": "Pick an external source", "submission.import-external.search.source.hint": "Pick an external source",
"submission.import-external.source.arxiv": "arXiv",
"submission.import-external.source.loading": "Loading ...", "submission.import-external.source.loading": "Loading ...",
"submission.import-external.source.sherpaJournal": "SHERPA Journals", "submission.import-external.source.sherpaJournal": "SHERPA Journals",